85 lines
3.1 KiB
Python
85 lines
3.1 KiB
Python
"""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)
|