seperate delete from archive functions
This commit is contained in:
1 parent
42a9595966
commit
9921e61515
7 files changed
+206
-38
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'}"
|
||||
|
||||
ELIGIBLE_MIN_SESSIONS = 3
|
||||
APP_VERSION = "2.5.20"
|
||||
APP_VERSION = "2.5.21"
|
||||
|
||||
|
||||
def load_local_env(env_path: Path) -> None:
|
||||
|
||||
@@ -420,6 +420,16 @@ def delete_season(season: Season) -> None:
|
||||
db.session.delete(season)
|
||||
|
||||
|
||||
def delete_league(league: League) -> None:
|
||||
from db_models import LedgerEvent, LeagueMembership, Player, PokerSession, Season
|
||||
LedgerEvent.query.filter_by(league_id=league.id).delete(synchronize_session=False)
|
||||
PokerSession.query.filter_by(league_id=league.id).delete(synchronize_session=False)
|
||||
Player.query.filter_by(league_id=league.id).delete(synchronize_session=False)
|
||||
Season.query.filter_by(league_id=league.id).delete(synchronize_session=False)
|
||||
LeagueMembership.query.filter_by(league_id=league.id).delete(synchronize_session=False)
|
||||
db.session.delete(league)
|
||||
|
||||
|
||||
def auto_assign_sessions_to_seasons(league_id: str) -> int:
|
||||
"""Assign unassigned sessions to seasons based on date ranges.
|
||||
|
||||
|
||||
+43
-6
@@ -352,6 +352,42 @@ def update_password():
|
||||
return redirect(url_for("account.settings"))
|
||||
|
||||
|
||||
@account_bp.post("/disable")
|
||||
@login_required
|
||||
def disable_account():
|
||||
if not db_ready():
|
||||
flash("Account database is not available.", "error")
|
||||
return redirect(url_for("account.settings"))
|
||||
|
||||
from db_models import League, User, utc_now
|
||||
|
||||
user = db.session.get(User, current_user_id())
|
||||
if user is None:
|
||||
flash("User not found.", "error")
|
||||
return redirect(url_for("account.settings"))
|
||||
|
||||
confirm = request.form.get("confirm", "").strip()
|
||||
current_password = request.form.get("current_password", "")
|
||||
|
||||
if confirm != "DISABLE":
|
||||
flash("Confirmation text did not match.", "error")
|
||||
return redirect(url_for("account.settings"))
|
||||
|
||||
if not verify_password(user.password_hash, current_password):
|
||||
flash("Password is incorrect.", "error")
|
||||
return redirect(url_for("account.settings"))
|
||||
|
||||
owned_leagues = League.query.filter_by(created_by_user_id=user.id, archived_at=None).all()
|
||||
for league in owned_leagues:
|
||||
league.archived_at = utc_now()
|
||||
|
||||
user.disabled_at = utc_now()
|
||||
db.session.commit()
|
||||
log_user_out()
|
||||
flash("Your account has been disabled.", "success")
|
||||
return redirect(url_for("public.home"))
|
||||
|
||||
|
||||
@account_bp.post("/delete")
|
||||
@login_required
|
||||
def delete_account():
|
||||
@@ -359,7 +395,8 @@ def delete_account():
|
||||
flash("Account database is not available.", "error")
|
||||
return redirect(url_for("account.settings"))
|
||||
|
||||
from db_models import League, User, utc_now
|
||||
from db_models import League, LeagueMembership, User, utc_now
|
||||
from league_repositories import delete_league
|
||||
|
||||
user = db.session.get(User, current_user_id())
|
||||
if user is None:
|
||||
@@ -377,14 +414,14 @@ def delete_account():
|
||||
flash("Password is incorrect.", "error")
|
||||
return redirect(url_for("account.settings"))
|
||||
|
||||
owned_leagues = League.query.filter_by(created_by_user_id=user.id, archived_at=None).all()
|
||||
for league in owned_leagues:
|
||||
league.archived_at = utc_now()
|
||||
for league in League.query.filter_by(created_by_user_id=user.id).all():
|
||||
delete_league(league)
|
||||
|
||||
user.disabled_at = utc_now()
|
||||
LeagueMembership.query.filter_by(user_id=user.id).delete(synchronize_session=False)
|
||||
db.session.delete(user)
|
||||
db.session.commit()
|
||||
log_user_out()
|
||||
flash("Your account has been deleted.", "success")
|
||||
flash("Your account and all data have been permanently deleted.", "success")
|
||||
return redirect(url_for("public.home"))
|
||||
|
||||
|
||||
|
||||
@@ -1603,6 +1603,29 @@ def archive_league(league_ref: str):
|
||||
return redirect(url_for("leagues.index"))
|
||||
|
||||
|
||||
@leagues_bp.post("/l/<league_ref>/delete")
|
||||
@login_required
|
||||
def delete_league_route(league_ref: str):
|
||||
if not db_ready():
|
||||
flash("League database is not available.", "error")
|
||||
return redirect(url_for("public.home"))
|
||||
|
||||
from league_repositories import delete_league
|
||||
|
||||
league = require_league(league_ref, {"owner"})
|
||||
confirm_name = request.form.get("confirm_name", "").strip()
|
||||
|
||||
if confirm_name != league.name:
|
||||
flash("League name did not match. Deletion cancelled.", "error")
|
||||
return redirect(url_for("leagues.league_settings", league_ref=league_ref))
|
||||
|
||||
league_name = league.name
|
||||
delete_league(league)
|
||||
db.session.commit()
|
||||
flash(f'"{league_name}" and all its data have been permanently deleted.', "success")
|
||||
return redirect(url_for("leagues.index"))
|
||||
|
||||
|
||||
@leagues_bp.get("/l/<league_ref>/ledger/export")
|
||||
@login_required
|
||||
def export_ledger_csv(league_ref: str):
|
||||
|
||||
@@ -865,6 +865,17 @@ fieldset[disabled] button {
|
||||
box-shadow: 0 0 0 3px var(--accent-a22);
|
||||
}
|
||||
|
||||
/* Custom chevron for all themed selects */
|
||||
.form-card select,
|
||||
.field-select,
|
||||
.session-filter-select {
|
||||
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 12 12' fill='none'%3E%3Cpath d='M2 4l4 4 4-4' stroke='%23999' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E");
|
||||
background-repeat: no-repeat;
|
||||
background-position: right 10px center;
|
||||
padding-right: 30px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
/* ================================================================
|
||||
TABLE
|
||||
================================================================ */
|
||||
|
||||
@@ -67,16 +67,66 @@
|
||||
<div class="panel__body">
|
||||
<div class="danger-row">
|
||||
<div>
|
||||
<strong class="danger-row__title">Delete account</strong>
|
||||
<p class="muted-text">Permanently disables your account and archives all leagues you own. This cannot be undone.</p>
|
||||
<strong class="danger-row__title">Disable account</strong>
|
||||
<p class="muted-text">Disables your account and archives all leagues you own. Your data is preserved — you can sign up again with the same email to restore access.</p>
|
||||
</div>
|
||||
<button class="btn btn--ghost btn--sm" type="button" data-modal="modal-delete-account">Delete account</button>
|
||||
<button class="btn btn--ghost btn--sm" type="button" data-modal="modal-disable-account">Disable account</button>
|
||||
</div>
|
||||
<div class="danger-row">
|
||||
<div>
|
||||
<strong class="danger-row__title">Delete account</strong>
|
||||
<p class="muted-text">Permanently deletes your account and all leagues, sessions, and ledger data you own. This cannot be undone.</p>
|
||||
</div>
|
||||
<button class="btn btn--warning btn--sm danger-row__btn" type="button" data-modal="modal-delete-account">Delete account</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<!-- Disable account modal -->
|
||||
<div class="modal-backdrop" id="modal-disable-account" hidden>
|
||||
<div class="modal-card">
|
||||
<div class="modal-card__head">
|
||||
<span class="kicker" style="margin:0;">Account</span>
|
||||
<h2 class="panel__title">Disable account</h2>
|
||||
</div>
|
||||
<p class="modal-card__text">
|
||||
Your account will be disabled and all leagues you own will be archived. Your data is preserved — you can sign up again with the same email to restore access.<br><br>
|
||||
Type <strong>DISABLE</strong> and enter your password to confirm.
|
||||
</p>
|
||||
<form method="post" action="{{ url_for('account.disable_account') }}">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
|
||||
<div class="form-card" style="padding:0 20px 4px;gap:10px;">
|
||||
<input
|
||||
class="confirm-text-input"
|
||||
type="text"
|
||||
name="confirm"
|
||||
placeholder='Type "DISABLE"'
|
||||
autocomplete="off"
|
||||
data-match="DISABLE"
|
||||
data-submit="disable-confirm-btn"
|
||||
data-requires-password="disable-password-input"
|
||||
>
|
||||
<input
|
||||
class="confirm-password-input"
|
||||
id="disable-password-input"
|
||||
type="password"
|
||||
name="current_password"
|
||||
placeholder="Your password"
|
||||
autocomplete="current-password"
|
||||
data-submit="disable-confirm-btn"
|
||||
>
|
||||
</div>
|
||||
<div class="modal-card__actions">
|
||||
<button class="btn btn--ghost btn--sm" type="button" data-close-modal>Cancel</button>
|
||||
<button class="btn btn--sm btn--danger" id="disable-confirm-btn" type="submit" disabled>Disable my account</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Delete account modal -->
|
||||
<div class="modal-backdrop" id="modal-delete-account" hidden>
|
||||
<div class="modal-card">
|
||||
<div class="modal-card__head">
|
||||
@@ -84,36 +134,35 @@
|
||||
<h2 class="panel__title">Delete account</h2>
|
||||
</div>
|
||||
<p class="modal-card__text">
|
||||
Your account will be <strong>permanently disabled</strong> and all leagues you own will be archived. This cannot be undone.<br><br>
|
||||
Your account and all leagues, sessions, and ledger data you own will be <strong>permanently deleted</strong>. This cannot be undone.<br><br>
|
||||
Type <strong>DELETE</strong> and enter your password to confirm.
|
||||
</p>
|
||||
<form method="post" action="{{ url_for('account.delete_account') }}">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
|
||||
<div class="form-card" style="padding:0 20px 4px;gap:10px;">
|
||||
<input
|
||||
class="modal-confirm-input"
|
||||
class="confirm-text-input"
|
||||
type="text"
|
||||
name="confirm"
|
||||
placeholder='Type "DELETE"'
|
||||
autocomplete="off"
|
||||
data-match="DELETE"
|
||||
data-target="delete-confirm-btn"
|
||||
data-also-requires="delete-password-input"
|
||||
data-submit="delete-confirm-btn"
|
||||
data-requires-password="delete-password-input"
|
||||
>
|
||||
<input
|
||||
class="modal-password-input"
|
||||
class="confirm-password-input"
|
||||
id="delete-password-input"
|
||||
type="password"
|
||||
name="current_password"
|
||||
placeholder="Your password"
|
||||
autocomplete="current-password"
|
||||
data-target="delete-confirm-btn"
|
||||
data-confirm-input="modal-delete-account"
|
||||
data-submit="delete-confirm-btn"
|
||||
>
|
||||
</div>
|
||||
<div class="modal-card__actions">
|
||||
<button class="btn btn--ghost btn--sm" type="button" data-close-modal>Cancel</button>
|
||||
<button class="btn btn--sm btn--danger" id="delete-confirm-btn" type="submit" disabled>Delete my account</button>
|
||||
<button class="btn btn--sm btn--danger" id="delete-confirm-btn" type="submit" disabled>Permanently delete account</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
@@ -123,41 +172,41 @@
|
||||
(function () {
|
||||
function openModal(id) {
|
||||
const el = document.getElementById(id);
|
||||
if (el) {
|
||||
if (!el) return;
|
||||
el.removeAttribute('hidden');
|
||||
const inp = el.querySelector('.modal-confirm-input');
|
||||
if (inp) { inp.value = ''; inp.focus(); }
|
||||
const pwd = el.querySelector('.modal-password-input');
|
||||
const txt = el.querySelector('.confirm-text-input');
|
||||
const pwd = el.querySelector('.confirm-password-input');
|
||||
if (txt) { txt.value = ''; txt.focus(); }
|
||||
if (pwd) pwd.value = '';
|
||||
refreshDeleteBtn();
|
||||
}
|
||||
}
|
||||
function closeModal(el) {
|
||||
el.closest('.modal-backdrop').setAttribute('hidden', '');
|
||||
refreshModal(el);
|
||||
}
|
||||
|
||||
function refreshDeleteBtn() {
|
||||
const confirmInp = document.querySelector('#modal-delete-account .modal-confirm-input');
|
||||
const pwdInp = document.getElementById('delete-password-input');
|
||||
const btn = document.getElementById('delete-confirm-btn');
|
||||
if (!confirmInp || !pwdInp || !btn) return;
|
||||
btn.disabled = !(confirmInp.value === 'DELETE' && pwdInp.value.length >= 1);
|
||||
function refreshModal(modalEl) {
|
||||
const txt = modalEl.querySelector('.confirm-text-input');
|
||||
if (!txt) return;
|
||||
const btnId = txt.dataset.submit;
|
||||
const pwdId = txt.dataset.requiresPassword;
|
||||
const btn = document.getElementById(btnId);
|
||||
const pwd = pwdId ? document.getElementById(pwdId) : null;
|
||||
if (!btn) return;
|
||||
const textOk = txt.value === txt.dataset.match;
|
||||
const pwdOk = !pwd || pwd.value.length >= 1;
|
||||
btn.disabled = !(textOk && pwdOk);
|
||||
}
|
||||
|
||||
document.querySelectorAll('[data-modal]').forEach(trigger => {
|
||||
trigger.addEventListener('click', () => openModal(trigger.dataset.modal));
|
||||
});
|
||||
document.querySelectorAll('[data-close-modal]').forEach(btn => {
|
||||
btn.addEventListener('click', () => closeModal(btn));
|
||||
btn.addEventListener('click', () => btn.closest('.modal-backdrop').setAttribute('hidden', ''));
|
||||
});
|
||||
document.querySelectorAll('.modal-backdrop').forEach(backdrop => {
|
||||
backdrop.addEventListener('click', e => { if (e.target === backdrop) backdrop.setAttribute('hidden', ''); });
|
||||
});
|
||||
|
||||
const confirmInp = document.querySelector('#modal-delete-account .modal-confirm-input');
|
||||
const pwdInp = document.getElementById('delete-password-input');
|
||||
if (confirmInp) confirmInp.addEventListener('input', refreshDeleteBtn);
|
||||
if (pwdInp) pwdInp.addEventListener('input', refreshDeleteBtn);
|
||||
document.querySelectorAll('.confirm-text-input, .confirm-password-input').forEach(inp => {
|
||||
inp.addEventListener('input', () => refreshModal(inp.closest('.modal-backdrop')));
|
||||
});
|
||||
})();
|
||||
</script>
|
||||
|
||||
|
||||
@@ -131,6 +131,13 @@
|
||||
</div>
|
||||
<button class="btn btn--warning btn--sm danger-row__btn" type="button" data-modal="modal-archive">Archive league</button>
|
||||
</div>
|
||||
<div class="danger-row">
|
||||
<div>
|
||||
<strong class="danger-row__title">Delete league</strong>
|
||||
<p class="muted-text">Permanently deletes this league and all players, sessions, and ledger data. This cannot be undone.</p>
|
||||
</div>
|
||||
<button class="btn btn--warning btn--sm danger-row__btn" type="button" data-modal="modal-delete-league">Delete league</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -231,6 +238,37 @@
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-backdrop" id="modal-delete-league" hidden>
|
||||
<div class="modal-card">
|
||||
<div class="modal-card__head">
|
||||
<span class="kicker" style="margin:0;">Danger zone</span>
|
||||
<h2 class="panel__title">Delete league</h2>
|
||||
</div>
|
||||
<p class="modal-card__text">
|
||||
<strong>{{ league.name }}</strong> and all its players, sessions, and ledger data will be <strong>permanently deleted</strong>. This cannot be undone.<br><br>
|
||||
Type the league name to confirm:
|
||||
</p>
|
||||
<form method="post" action="{{ url_for('leagues.delete_league_route', league_ref=league.url_ref) }}">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
|
||||
<div class="form-card" style="padding:0 20px 4px;">
|
||||
<input
|
||||
class="modal-confirm-input"
|
||||
type="text"
|
||||
name="confirm_name"
|
||||
placeholder="{{ league.name }}"
|
||||
autocomplete="off"
|
||||
data-match="{{ league.name }}"
|
||||
data-target="delete-league-confirm-btn"
|
||||
>
|
||||
</div>
|
||||
<div class="modal-card__actions">
|
||||
<button class="btn btn--ghost btn--sm" type="button" data-close-modal>Cancel</button>
|
||||
<button class="btn btn--sm btn--danger" id="delete-league-confirm-btn" type="submit" disabled>Permanently delete</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
(function () {
|
||||
function openModal(id) {
|
||||
|
||||
Reference in new issue
Block a user