From 00d59ac8ae4a71bdb21628508d17de17341aa4c8 Mon Sep 17 00:00:00 2001 From: SowinskiBraeden Date: Sat, 27 Jun 2026 10:33:54 -0700 Subject: [PATCH] fix re-invites + promote/demote members --- config.py | 2 +- league_repositories.py | 26 +++++++ routes/leagues.py | 30 ++++++++ static/css/style.css | 77 ++++++++++++++++++ templates/league_settings.html | 137 +++++++++++++++++++++++++++------ 5 files changed, 247 insertions(+), 25 deletions(-) diff --git a/config.py b/config.py index f1541e0..0608d1c 100644 --- a/config.py +++ b/config.py @@ -9,7 +9,7 @@ DATA_PATH = BASE_DIR / "data" / "entries.csv" DEFAULT_DATABASE_URL = f"sqlite:///{BASE_DIR / 'data' / 'boker-dev.sqlite3'}" ELIGIBLE_MIN_SESSIONS = 3 -APP_VERSION = "2.5.5" +APP_VERSION = "2.5.6" def load_local_env(env_path: Path) -> None: diff --git a/league_repositories.py b/league_repositories.py index 3d4417a..117f4fc 100644 --- a/league_repositories.py +++ b/league_repositories.py @@ -302,6 +302,16 @@ def add_league_member( role: str, invited_by_user_id: str, ) -> LeagueMembership: + existing = LeagueMembership.query.filter_by( + league_id=league_id, + user_id=user_id, + ).one_or_none() + if existing is not None: + existing.role = role + existing.invited_by_user_id = invited_by_user_id or None + existing.disabled_at = None + return existing + membership = LeagueMembership( league_id=league_id, user_id=user_id, @@ -322,3 +332,19 @@ def remove_league_member(league_id: str, user_id: str) -> None: ).one_or_none() if membership and membership.role != "owner": membership.disabled_at = utc_now() + + +def set_league_member_role(league_id: str, user_id: str, role: str) -> LeagueMembership | None: + if role not in {"manager", "viewer"}: + raise ValueError(f"Unsupported member role: {role}") + + membership = LeagueMembership.query.filter_by( + league_id=league_id, + user_id=user_id, + disabled_at=None, + ).one_or_none() + if membership is None or membership.role == "owner": + return None + + membership.role = role + return membership diff --git a/routes/leagues.py b/routes/leagues.py index c1a83cd..9aee964 100644 --- a/routes/leagues.py +++ b/routes/leagues.py @@ -1262,6 +1262,36 @@ def remove_member(league_ref: str, user_id: str): return redirect(url_for("leagues.league_settings", league_ref=league.url_ref)) +@leagues_bp.post("/l//settings/members//role") +@login_required +def update_member_role(league_ref: str, user_id: str): + if not db_ready(): + flash("League database is not available.", "error") + return redirect(url_for("public.home")) + + from league_repositories import set_league_member_role + + league = require_league(league_ref, {"owner"}) + role = request.form.get("role", "").strip() + + if user_id == current_user_id(): + flash("You cannot change your own league role.", "error") + return redirect(url_for("leagues.league_settings", league_ref=league.url_ref)) + + if role not in {"manager", "viewer"}: + flash("Invalid member role.", "error") + return redirect(url_for("leagues.league_settings", league_ref=league.url_ref)) + + membership = set_league_member_role(league.id, user_id, role) + if membership is None: + flash("That member cannot be updated.", "error") + return redirect(url_for("leagues.league_settings", league_ref=league.url_ref)) + + db.session.commit() + flash(f"Member changed to {role}.", "success") + return redirect(url_for("leagues.league_settings", league_ref=league.url_ref)) + + @leagues_bp.post("/l//archive") @login_required def archive_league(league_ref: str): diff --git a/static/css/style.css b/static/css/style.css index ba2fc2c..29bc89c 100644 --- a/static/css/style.css +++ b/static/css/style.css @@ -3693,6 +3693,83 @@ select.control { cursor: pointer; } .member-row:last-child { border-bottom: none; } .member-row > div { min-width: 0; } .member-row strong { font: 600 14px/1.4 var(--font-ui); color: var(--text-1); } +.access-panel__grid { + display: grid; + grid-template-columns: minmax(320px, 0.82fr) minmax(320px, 420px); + min-height: 228px; + border-top: 1px solid var(--border); +} +.access-panel__members { + min-width: 0; +} +.access-panel__invite { + border-left: 1px solid var(--border); + padding: 18px 20px 20px; + gap: 14px; +} +.access-panel__invite-title { + margin: 0; + font: 700 16px/1.25 var(--font-ui); + color: var(--text-strong); +} +.access-panel__invite .btn { + justify-self: start; +} +.member-row { + align-items: flex-start; + padding: 14px 20px; +} +.member-row__identity { + display: grid; + gap: 6px; +} +.member-row__actions { + display: flex; + align-items: center; + justify-content: flex-end; + gap: 8px; + flex-wrap: wrap; +} +.member-role { + justify-self: start; + padding: 3px 8px; + border: 1px solid var(--border); + border-radius: 999px; + font: 700 10px/1 var(--font-ui); + letter-spacing: .04em; + text-transform: uppercase; + color: var(--muted); + background: var(--surface-subtle); +} +.member-role--owner { + color: var(--accent); + border-color: color-mix(in srgb, var(--accent) 34%, transparent); + background: color-mix(in srgb, var(--accent) 10%, transparent); +} +.member-role--manager { + color: var(--pos); + border-color: var(--pos-tint-bd); + background: var(--pos-tint); +} +.member-role--viewer { + color: var(--text-2); +} +@media (max-width: 760px) { + .access-panel__grid { + grid-template-columns: 1fr; + } + .access-panel__invite { + border-left: none; + border-top: 1px solid var(--border); + } + .member-row { + flex-direction: column; + align-items: stretch; + } + .member-row__actions { + justify-content: flex-start; + } +} .card-menu { position: relative; flex-shrink: 0; } .card-menu__trigger { diff --git a/templates/league_settings.html b/templates/league_settings.html index 4cb4985..8a30362 100644 --- a/templates/league_settings.html +++ b/templates/league_settings.html @@ -47,7 +47,7 @@ -
+

Access

@@ -55,40 +55,58 @@
{{ members|length }} member{{ 's' if members|length != 1 else '' }}
-
- {% for membership, member_user in members %} -
-
- {{ member_user.email }} - {{ membership.role }} +
+
+ {% for membership, member_user in members %} +
+
+ {{ member_user.email }} + {{ membership.role }} +
+ {% if membership.role != 'owner' and member_user.id != current_user_id %} +
+ {% if membership.role == 'viewer' %} + + {% elif membership.role == 'manager' %} + + {% endif %} + +
+ {% endif %}
- {% if membership.role != 'owner' %} -
- - -
- {% endif %} + {% endfor %}
- {% endfor %} -
-
- -

Invite by email

-
-
-
+ + +
@@ -111,6 +129,41 @@
+ + + +