fix trasnfer ownership
This commit is contained in:
1 parent
d1b9feab82
commit
964a0651f5
5 files changed
+121
-3
No files matched your search
@@ -9,7 +9,7 @@ DATA_PATH = BASE_DIR / "data" / "entries.csv"
|
|||||||
DEFAULT_DATABASE_URL = f"sqlite:///{BASE_DIR / 'data' / 'boker-dev.sqlite3'}"
|
DEFAULT_DATABASE_URL = f"sqlite:///{BASE_DIR / 'data' / 'boker-dev.sqlite3'}"
|
||||||
|
|
||||||
ELIGIBLE_MIN_SESSIONS = 3
|
ELIGIBLE_MIN_SESSIONS = 3
|
||||||
APP_VERSION = "2.5.7"
|
APP_VERSION = "2.5.9"
|
||||||
|
|
||||||
|
|
||||||
def load_local_env(env_path: Path) -> None:
|
def load_local_env(env_path: Path) -> None:
|
||||||
|
|||||||
@@ -348,3 +348,30 @@ def set_league_member_role(league_id: str, user_id: str, role: str) -> LeagueMem
|
|||||||
|
|
||||||
membership.role = role
|
membership.role = role
|
||||||
return membership
|
return membership
|
||||||
|
|
||||||
|
|
||||||
|
def transfer_league_ownership(
|
||||||
|
league: League,
|
||||||
|
current_owner_user_id: str,
|
||||||
|
new_owner_user_id: str,
|
||||||
|
) -> tuple[LeagueMembership, LeagueMembership] | None:
|
||||||
|
if current_owner_user_id == new_owner_user_id:
|
||||||
|
return None
|
||||||
|
|
||||||
|
current_owner = LeagueMembership.query.filter_by(
|
||||||
|
league_id=league.id,
|
||||||
|
user_id=current_owner_user_id,
|
||||||
|
disabled_at=None,
|
||||||
|
).one_or_none()
|
||||||
|
new_owner = LeagueMembership.query.filter_by(
|
||||||
|
league_id=league.id,
|
||||||
|
user_id=new_owner_user_id,
|
||||||
|
disabled_at=None,
|
||||||
|
).one_or_none()
|
||||||
|
if current_owner is None or current_owner.role != "owner" or new_owner is None:
|
||||||
|
return None
|
||||||
|
|
||||||
|
current_owner.role = "manager"
|
||||||
|
new_owner.role = "owner"
|
||||||
|
league.created_by_user_id = new_owner_user_id
|
||||||
|
return current_owner, new_owner
|
||||||
@@ -1295,6 +1295,34 @@ def update_member_role(league_ref: str, user_id: str):
|
|||||||
return redirect(url_for("leagues.league_settings", league_ref=league.url_ref))
|
return redirect(url_for("leagues.league_settings", league_ref=league.url_ref))
|
||||||
|
|
||||||
|
|
||||||
|
@leagues_bp.post("/l/<league_ref>/settings/transfer-ownership")
|
||||||
|
@login_required
|
||||||
|
def transfer_ownership(league_ref: str):
|
||||||
|
if not db_ready():
|
||||||
|
flash("League database is not available.", "error")
|
||||||
|
return redirect(url_for("public.home"))
|
||||||
|
|
||||||
|
from league_repositories import transfer_league_ownership
|
||||||
|
|
||||||
|
league = require_league(league_ref, {"owner"})
|
||||||
|
new_owner_user_id = request.form.get("new_owner_user_id", "").strip()
|
||||||
|
current_owner_user_id = current_user_id() or ""
|
||||||
|
|
||||||
|
if not new_owner_user_id:
|
||||||
|
flash("Choose a member to transfer ownership to.", "error")
|
||||||
|
return redirect(url_for("leagues.league_settings", league_ref=league.url_ref))
|
||||||
|
|
||||||
|
transfer = transfer_league_ownership(league, current_owner_user_id, new_owner_user_id)
|
||||||
|
if transfer is None:
|
||||||
|
flash("Ownership can only be transferred to an active member.", "error")
|
||||||
|
return redirect(url_for("leagues.league_settings", league_ref=league.url_ref))
|
||||||
|
|
||||||
|
_old_owner, _new_owner = transfer
|
||||||
|
db.session.commit()
|
||||||
|
flash("League ownership transferred. Your role is now manager.", "success")
|
||||||
|
return redirect(url_for("leagues.dashboard", league_ref=league.url_ref))
|
||||||
|
|
||||||
|
|
||||||
@leagues_bp.post("/l/<league_ref>/archive")
|
@leagues_bp.post("/l/<league_ref>/archive")
|
||||||
@login_required
|
@login_required
|
||||||
def archive_league(league_ref: str):
|
def archive_league(league_ref: str):
|
||||||
|
|||||||
@@ -1372,7 +1372,19 @@ td a:hover { color: var(--accent); }
|
|||||||
.btn--ghost:hover { border-color: var(--border-hi); color: var(--text-strong); }
|
.btn--ghost:hover { border-color: var(--border-hi); color: var(--text-strong); }
|
||||||
.btn--outline { background: var(--accent-tint); color: var(--accent); border: 1px solid var(--accent-chip-bd); }
|
.btn--outline { background: var(--accent-tint); color: var(--accent); border: 1px solid var(--accent-chip-bd); }
|
||||||
.btn--outline:hover { background: var(--accent-chip); }
|
.btn--outline:hover { background: var(--accent-chip); }
|
||||||
|
.btn--warning {
|
||||||
|
background: var(--warn-tint);
|
||||||
|
color: var(--warn);
|
||||||
|
border: 1px solid var(--warn-tint-bd);
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
.btn--warning:hover {
|
||||||
|
background: color-mix(in srgb, var(--warn) 18%, transparent);
|
||||||
|
border-color: color-mix(in srgb, var(--warn) 54%, transparent);
|
||||||
|
color: var(--warn);
|
||||||
|
}
|
||||||
.btn--sm { padding: 7px 13px; font-size: 13px; }
|
.btn--sm { padding: 7px 13px; font-size: 13px; }
|
||||||
|
.btn:disabled { opacity: .45; cursor: not-allowed; box-shadow: none; }
|
||||||
|
|
||||||
/* legacy button classes mapped to new */
|
/* legacy button classes mapped to new */
|
||||||
.primary-button {
|
.primary-button {
|
||||||
@@ -2300,12 +2312,26 @@ select.control { cursor: pointer; }
|
|||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
gap: 20px;
|
gap: 20px;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
|
padding: 4px 0 18px;
|
||||||
|
border-bottom: 1px solid var(--border);
|
||||||
|
}
|
||||||
|
.danger-row + .danger-row {
|
||||||
|
padding-top: 18px;
|
||||||
|
}
|
||||||
|
.danger-row:last-child {
|
||||||
|
padding-bottom: 0;
|
||||||
|
border-bottom: none;
|
||||||
}
|
}
|
||||||
.danger-row__title {
|
.danger-row__title {
|
||||||
display: block;
|
display: block;
|
||||||
color: var(--text-strong);
|
color: var(--text-strong);
|
||||||
margin-bottom: 4px;
|
margin-bottom: 4px;
|
||||||
}
|
}
|
||||||
|
.danger-row__btn {
|
||||||
|
width: 168px;
|
||||||
|
justify-content: center;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
/* Danger button */
|
/* Danger button */
|
||||||
.btn--danger {
|
.btn--danger {
|
||||||
@@ -5199,4 +5225,5 @@ select.control { cursor: pointer; }
|
|||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
color: var(--accent);
|
color: var(--accent);
|
||||||
transition: gap .15s;
|
transition: gap .15s;
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -117,18 +117,55 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="panel__body">
|
<div class="panel__body">
|
||||||
|
<div class="danger-row">
|
||||||
|
<div>
|
||||||
|
<strong class="danger-row__title">Transfer ownership</strong>
|
||||||
|
<p class="muted-text">Choose an existing member to become the league owner. Your role will change to manager.</p>
|
||||||
|
</div>
|
||||||
|
<button class="btn btn--warning btn--sm danger-row__btn" type="button" data-modal="modal-transfer-owner" {% if members|length <= 1 %}disabled{% endif %}>Transfer ownership</button>
|
||||||
|
</div>
|
||||||
<div class="danger-row">
|
<div class="danger-row">
|
||||||
<div>
|
<div>
|
||||||
<strong class="danger-row__title">Archive league</strong>
|
<strong class="danger-row__title">Archive league</strong>
|
||||||
<p class="muted-text">Removes this league from your dashboard. All players, sessions, and ledger data are preserved.</p>
|
<p class="muted-text">Removes this league from your dashboard. All players, sessions, and ledger data are preserved.</p>
|
||||||
</div>
|
</div>
|
||||||
<button class="btn btn--ghost btn--sm" type="button" data-modal="modal-archive">Archive league</button>
|
<button class="btn btn--warning btn--sm danger-row__btn" type="button" data-modal="modal-archive">Archive league</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="modal-backdrop" id="modal-transfer-owner" hidden>
|
||||||
|
<div class="modal-card" role="dialog" aria-modal="true" aria-labelledby="modal-transfer-owner-title">
|
||||||
|
<div class="modal-card__head">
|
||||||
|
<span class="kicker" style="margin:0;">Ownership</span>
|
||||||
|
<h2 class="panel__title" id="modal-transfer-owner-title">Transfer ownership</h2>
|
||||||
|
</div>
|
||||||
|
<p class="modal-card__text">The selected member will become the owner of <strong>{{ league.name }}</strong>. Your role will change to manager.</p>
|
||||||
|
<form method="post" action="{{ url_for('leagues.transfer_ownership', league_ref=league.url_ref) }}">
|
||||||
|
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
|
||||||
|
<div class="form-card" style="padding:0 20px 16px;gap:12px;">
|
||||||
|
<label>
|
||||||
|
<span>New owner</span>
|
||||||
|
<select name="new_owner_user_id" id="transfer-owner-select" required>
|
||||||
|
<option value="">Select a member</option>
|
||||||
|
{% for membership, member_user in members %}
|
||||||
|
{% if membership.role != 'owner' %}
|
||||||
|
<option value="{{ member_user.id }}">{{ member_user.email }} · {{ membership.role }}</option>
|
||||||
|
{% endif %}
|
||||||
|
{% endfor %}
|
||||||
|
</select>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div class="modal-card__actions">
|
||||||
|
<button class="btn btn--ghost btn--sm" type="button" data-close-modal>Cancel</button>
|
||||||
|
<button class="btn btn--primary btn--sm" type="submit">Transfer ownership</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="modal-backdrop" id="modal-member-role" hidden>
|
<div class="modal-backdrop" id="modal-member-role" hidden>
|
||||||
<div class="modal-card" role="dialog" aria-modal="true" aria-labelledby="modal-member-role-title">
|
<div class="modal-card" role="dialog" aria-modal="true" aria-labelledby="modal-member-role-title">
|
||||||
<div class="modal-card__head">
|
<div class="modal-card__head">
|
||||||
@@ -193,7 +230,6 @@
|
|||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
(function () {
|
(function () {
|
||||||
function openModal(id) {
|
function openModal(id) {
|
||||||
|
|||||||
Reference in new issue
Block a user