"""Helpers for mutating routes that should not force a full page reload. The site is server-rendered, and every mutating route used to end with ``return redirect(url_for(...))``, so each change reloaded the whole page: scroll reset, open dialogs closed, and chaining several edits meant waiting for a full render between each one. These helpers let a route answer an AJAX submit (the ``data-ajax`` forms in main.js) with a small JSON payload instead of a redirect, while the plain HTML form path keeps behaving exactly as before. The client then fetches the page again and swaps only the content region, so the page never reloads. The counterpart that performs the swap lives in app/static/js/main.js. """ from flask import get_flashed_messages, jsonify, redirect, request, session from app.models import AppSettings #: Key holding a coarse, monotonically increasing stamp for the whole dataset. #: It advances after every committed mutation so a stale client can be told #: (with a 409) that someone else has changed what it is looking at. REVISION_KEY = 'data_revision' def is_ajax_submit(): """Whether this POST came from the fetch-based form handler in main.js.""" return request.headers.get('X-Requested-With') == 'XMLHttpRequest' def current_revision(): """The current revision stamp, as an int (0 when nothing has been bumped).""" try: return int(AppSettings.get(REVISION_KEY, '0') or '0') except (TypeError, ValueError): return 0 def bump_revision(): """Advance the revision stamp after a committed change. Stored through AppSettings so it survives restarts and is shared by every worker, unlike a module-level counter. """ AppSettings.set(REVISION_KEY, str(current_revision() + 1)) def _flashed_success(): """Whether a success flash is pending, without consuming it. The HTML path still redirects and must leave the flash for the next page to render; peeking at the session is the only way to know without clearing it. Only a successful commit advances the revision -- a rejected edit must not make other users reload for nothing. """ flashes = session.get('_flashes', []) return any(category == 'success' for category, _message in flashes) def respond_after_change(redirect_url): """Answer a committed (or rejected) mutation. For a normal HTML form submit this is exactly the ``redirect(...)`` the routes used to return. For an AJAX submit it returns JSON telling the client where to fetch the refreshed fragment and which flash messages to show, so the page can update in place instead of reloading. """ if _flashed_success(): bump_revision() if is_ajax_submit(): messages = [ {'category': category, 'text': message} for category, message in get_flashed_messages(with_categories=True) ] return jsonify( { 'ok': True, 'redirect': redirect_url, 'messages': messages, 'revision': current_revision(), } ) return redirect(redirect_url)