Aug 5 - Add new design (switchable)
This commit is contained in:
@@ -27,8 +27,40 @@ limiter = Limiter(
|
||||
)
|
||||
|
||||
|
||||
# ── Design A/B test: per-request template overrides (phase48) ────────────────
|
||||
# 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.
|
||||
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])
|
||||
|
||||
# ── Reverse-proxy awareness (Nginx) ──────────────────────────────────────
|
||||
@@ -111,6 +143,54 @@ def create_app(config_name='default'):
|
||||
from app.utils import storage as _storage
|
||||
app.jinja_env.globals['media_url'] = _storage.media_url
|
||||
|
||||
# ── Design A/B test wiring (phase48) ──────────────────────────────────
|
||||
# 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
|
||||
|
||||
@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/'):
|
||||
g.jqc_theme = 'classic'
|
||||
return
|
||||
from flask_login import current_user as _cu
|
||||
theme = 'classic'
|
||||
try:
|
||||
if _cu.is_authenticated:
|
||||
theme = _cu.ui_theme or 'classic'
|
||||
except Exception: # DB column missing (migration not yet run)
|
||||
theme = 'classic'
|
||||
g.jqc_theme = theme if theme in ('classic', 'modern') else 'classic'
|
||||
|
||||
@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', 'classic')
|
||||
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.
|
||||
'now_display': now_eastern().strftime('%A, %B %-d, %Y'),
|
||||
}
|
||||
|
||||
# ── 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.
|
||||
@@ -183,6 +263,7 @@ def create_app(config_name='default'):
|
||||
from app.routes import devices # Admin device registry
|
||||
from app.routes import public # Public facility QR pages (no login)
|
||||
from app.routes import scheduled_inspections # Planned/recurring inspections
|
||||
from app.routes import ui # phase48 — design A/B test + new pages
|
||||
|
||||
app.register_blueprint(auth.bp)
|
||||
app.register_blueprint(dashboard.bp)
|
||||
@@ -201,6 +282,7 @@ def create_app(config_name='default'):
|
||||
app.register_blueprint(devices.bp)
|
||||
app.register_blueprint(public.bp)
|
||||
app.register_blueprint(scheduled_inspections.bp)
|
||||
app.register_blueprint(ui.bp)
|
||||
|
||||
# ── Mobile API (Phase 7 / Phase A / Phase B / Phase C) ───────────────────
|
||||
# The /api/v1 blueprint group uses JWT Bearer tokens — no CSRF cookies needed.
|
||||
|
||||
Reference in New Issue
Block a user