Aug 7 - Update: UI change - MT16
This commit is contained in:
@@ -28,8 +28,41 @@ limiter = Limiter(
|
||||
)
|
||||
|
||||
|
||||
# ── Web portal design: per-request template overrides (MT-16) ────────────────
|
||||
# A user on the 'modern' design gets templates/modern/<name>.html in place of
|
||||
# templates/<name>.html whenever that override exists; otherwise the normal
|
||||
# template is used and only the layout shell + CSS differ.
|
||||
#
|
||||
# The rewrite happens in get_template() (not in the loader) so Jinja's template
|
||||
# cache is keyed on the REWRITTEN name — a cached modern template can never be
|
||||
# served to a classic user, or vice versa. A loader-level swap would have that
|
||||
# bug, and in MT it would leak across tenants sharing a worker process.
|
||||
from flask.templating import Environment as _FlaskJinjaEnvironment
|
||||
|
||||
|
||||
class ThemedEnvironment(_FlaskJinjaEnvironment):
|
||||
"""Jinja environment that redirects template names to modern/<name>."""
|
||||
|
||||
# Populated once in create_app() by scanning templates/modern/.
|
||||
jqc_modern_templates: set = set()
|
||||
|
||||
def get_template(self, name, parent=None, globals=None):
|
||||
if (isinstance(name, str)
|
||||
and self.jqc_modern_templates
|
||||
and not name.startswith('modern/')):
|
||||
candidate = 'modern/' + name
|
||||
if candidate in self.jqc_modern_templates:
|
||||
from flask import g, has_request_context
|
||||
if has_request_context() and getattr(g, 'jqc_theme', 'classic') == 'modern':
|
||||
name = candidate
|
||||
return super().get_template(name, parent, globals)
|
||||
|
||||
|
||||
def create_app(config_name='default'):
|
||||
app = Flask(__name__)
|
||||
# Must be assigned BEFORE app.jinja_env is first touched (it is a cached
|
||||
# property), so the themed subclass is the one actually instantiated.
|
||||
app.jinja_environment = ThemedEnvironment
|
||||
app.config.from_object(config[config_name])
|
||||
|
||||
# Unwrap X-Forwarded-For / X-Forwarded-Proto set by Nginx so Flask sees
|
||||
@@ -127,6 +160,65 @@ def create_app(config_name='default'):
|
||||
from app.utils import storage as _storage
|
||||
app.jinja_env.globals['media_url'] = _storage.media_url
|
||||
|
||||
# ── Web portal design wiring (MT-16) ──────────────────────────────────
|
||||
# Index the modern/ override templates once at boot, so get_template()
|
||||
# never has to touch the filesystem per request.
|
||||
_modern_root = os.path.join(app.template_folder or 'templates', 'modern')
|
||||
if not os.path.isabs(_modern_root):
|
||||
_modern_root = os.path.join(app.root_path, _modern_root)
|
||||
_modern_set = set()
|
||||
if os.path.isdir(_modern_root):
|
||||
for _dirpath, _dirnames, _filenames in os.walk(_modern_root):
|
||||
for _fn in _filenames:
|
||||
if _fn.endswith('.html'):
|
||||
_rel = os.path.relpath(os.path.join(_dirpath, _fn), _modern_root)
|
||||
_modern_set.add('modern/' + _rel.replace(os.sep, '/'))
|
||||
ThemedEnvironment.jqc_modern_templates = _modern_set
|
||||
app.logger.info('UI themes | modern overrides indexed: %s', len(_modern_set))
|
||||
|
||||
from flask import g, request as _request
|
||||
|
||||
@app.before_request
|
||||
def resolve_ui_theme():
|
||||
"""Stash the active design on `g` for ThemedEnvironment.get_template()."""
|
||||
# The mobile API renders no templates and authenticates by JWT — skip it
|
||||
# so this never touches the Flask-Login session loader on API traffic.
|
||||
if _request.path.startswith('/api/'):
|
||||
# The API renders no templates; 'classic' here only means "never
|
||||
# rewrite a template name" (see ThemedEnvironment.get_template).
|
||||
g.jqc_theme = 'classic'
|
||||
return
|
||||
from flask_login import current_user as _cu
|
||||
# MT-16 — the fallback is configurable per deployment. It defaults to
|
||||
# 'classic' so an existing tenant's users see no change until they opt
|
||||
# in; a stored users.ui_theme always wins over the default.
|
||||
default = app.config.get('DEFAULT_UI_THEME', 'classic')
|
||||
theme = default
|
||||
try:
|
||||
if _cu.is_authenticated:
|
||||
theme = _cu.ui_theme or default
|
||||
except Exception: # DB column missing (migration not yet run)
|
||||
theme = default
|
||||
g.jqc_theme = theme if theme in ('classic', 'modern') else default
|
||||
|
||||
@app.context_processor
|
||||
def inject_ui_theme():
|
||||
"""Give base.html the shell to extend."""
|
||||
from app.utils.time_utils import now_eastern
|
||||
theme = getattr(g, 'jqc_theme',
|
||||
app.config.get('DEFAULT_UI_THEME', 'classic'))
|
||||
_now = now_eastern()
|
||||
return {
|
||||
'jqc_theme': theme,
|
||||
'jqc_layout': 'layouts/modern.html' if theme == 'modern'
|
||||
else 'layouts/classic.html',
|
||||
# Long-form date shown in the modern dashboard header. The day is
|
||||
# interpolated rather than formatted with '%-d' — that flag is a
|
||||
# glibc extension and raises ValueError on Windows, which would
|
||||
# 500 every page (this context processor runs on both themes).
|
||||
'now_display': f'{_now.strftime("%A, %B")} {_now.day}, {_now.year}',
|
||||
}
|
||||
|
||||
# ── Inject unread notification count into every template context ──────
|
||||
# This powers the red badge on the navbar bell icon without requiring
|
||||
# individual routes to pass the count manually.
|
||||
@@ -221,6 +313,7 @@ def create_app(config_name='default'):
|
||||
from app.routes import tenant_settings # MT-7 — tenant self-service
|
||||
from app.routes import signup # MT-8+ — public self-service signup
|
||||
from app.routes import landing # Public apex marketing/landing page
|
||||
from app.routes import ui # MT-16 — design switch + new pages
|
||||
from app.billing import bp as billing_bp # MT-8 — Stripe billing
|
||||
|
||||
app.register_blueprint(auth.bp)
|
||||
@@ -244,6 +337,7 @@ def create_app(config_name='default'):
|
||||
app.register_blueprint(tenant_settings.bp)
|
||||
app.register_blueprint(signup.bp)
|
||||
app.register_blueprint(landing.bp)
|
||||
app.register_blueprint(ui.bp)
|
||||
# Billing blueprint is CSRF-exempt: /billing/webhook receives raw POST from
|
||||
# Stripe and cannot carry a CSRF token. Subscribe/portal are GET redirects
|
||||
# which Flask-WTF does not protect anyway (CSRF only applies to unsafe methods).
|
||||
|
||||
Reference in New Issue
Block a user