Aug 5 - Add new design (switchable)
This commit is contained in:
@@ -0,0 +1,115 @@
|
||||
"""
|
||||
ui.py
|
||||
-----
|
||||
Web-portal design A/B test (phase48).
|
||||
|
||||
Routes
|
||||
POST /ui/theme switch_theme() — flip users.ui_theme classic ↔ modern
|
||||
GET /ui/about about() — About Us page (new, modern deck)
|
||||
GET /ui/support-center support_center() — Support hub of how-to cards (new)
|
||||
GET /ui/theme-votes theme_votes() — admin tally of which design users kept
|
||||
|
||||
Nothing here changes existing behaviour: the theme flag only selects which
|
||||
layout shell base.html extends.
|
||||
"""
|
||||
|
||||
import logging
|
||||
|
||||
from flask import (Blueprint, render_template, redirect, request,
|
||||
url_for, flash)
|
||||
from flask_login import login_required, current_user
|
||||
from sqlalchemy import func
|
||||
|
||||
from app import db
|
||||
from app.models.user import User
|
||||
from app.utils.audit import log_action, ACTION_UPDATE
|
||||
|
||||
bp = Blueprint('ui', __name__, url_prefix='/ui')
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
VALID_THEMES = ('classic', 'modern')
|
||||
|
||||
|
||||
def _safe_next(target: str | None) -> str:
|
||||
"""Only allow same-site relative redirects (open-redirect guard)."""
|
||||
if not target:
|
||||
return url_for('dashboard.index')
|
||||
if target.startswith('/') and not target.startswith('//'):
|
||||
return target
|
||||
return url_for('dashboard.index')
|
||||
|
||||
|
||||
# ── Design switch ─────────────────────────────────────────────────────────────
|
||||
|
||||
@bp.route('/theme', methods=['POST'])
|
||||
@login_required
|
||||
def switch_theme():
|
||||
"""Persist the user's design choice, then return them to the same page."""
|
||||
theme = (request.form.get('theme') or '').strip().lower()
|
||||
if theme not in VALID_THEMES:
|
||||
flash('Unknown design option.', 'warning')
|
||||
return redirect(_safe_next(request.form.get('next')))
|
||||
|
||||
previous = current_user.ui_theme or 'classic'
|
||||
if previous != theme:
|
||||
current_user.ui_theme = theme
|
||||
db.session.commit()
|
||||
|
||||
# log_action() commits internally — always AFTER the business commit.
|
||||
log_action(
|
||||
action = ACTION_UPDATE,
|
||||
entity_type = 'User',
|
||||
entity_id = current_user.id,
|
||||
entity_label = current_user.username,
|
||||
details = f'ui_theme={previous}→{theme}',
|
||||
)
|
||||
logger.info('UI | theme switch | user=%s | %s -> %s',
|
||||
current_user.username, previous, theme)
|
||||
flash('Now showing the {} design. You can switch back any time from '
|
||||
'the account menu.'.format('new' if theme == 'modern' else 'classic'),
|
||||
'info')
|
||||
|
||||
return redirect(_safe_next(request.form.get('next')))
|
||||
|
||||
|
||||
# ── New pages (linked from the modern sidebar) ───────────────────────────────
|
||||
|
||||
@bp.route('/about')
|
||||
@login_required
|
||||
def about():
|
||||
return render_template('ui/about.html')
|
||||
|
||||
|
||||
@bp.route('/support-center')
|
||||
@login_required
|
||||
def support_center():
|
||||
return render_template('ui/support_center.html')
|
||||
|
||||
|
||||
# ── Admin: which design are people actually keeping? ─────────────────────────
|
||||
|
||||
@bp.route('/theme-votes')
|
||||
@login_required
|
||||
def theme_votes():
|
||||
if current_user.role != 'admin':
|
||||
flash('You do not have permission to view the design vote tally.', 'danger')
|
||||
return redirect(url_for('dashboard.index'))
|
||||
|
||||
rows = (db.session.query(User.ui_theme, func.count(User.id))
|
||||
.filter(User.active == True) # noqa: E712 — SQL boolean
|
||||
.group_by(User.ui_theme)
|
||||
.all())
|
||||
tally = {t: 0 for t in VALID_THEMES}
|
||||
for theme, count in rows:
|
||||
tally[theme or 'classic'] = tally.get(theme or 'classic', 0) + count
|
||||
total = sum(tally.values())
|
||||
|
||||
by_role = (db.session.query(User.role, User.ui_theme, func.count(User.id))
|
||||
.filter(User.active == True) # noqa: E712
|
||||
.group_by(User.role, User.ui_theme)
|
||||
.order_by(User.role)
|
||||
.all())
|
||||
|
||||
return render_template('ui/theme_votes.html',
|
||||
tally=tally, total=total, by_role=by_role)
|
||||
Reference in New Issue
Block a user