Files
JQC_multi_tenant/migrations/versions/phase52_user_ui_theme.py
T
2026-08-07 16:32:40 -04:00

73 lines
2.7 KiB
Python

"""phase52 — per-user web portal design preference
Adds to `users`:
ui_theme VARCHAR(16) NOT NULL DEFAULT 'classic'
'classic' renders the original top-navbar shell (layouts/classic.html),
'modern' renders the sidebar shell (layouts/modern.html). Every existing
account starts on 'classic', so the portal looks and behaves exactly as before
until a user opts in from the account menu.
MULTI-TENANT NOTE — why this differs from ST
--------------------------------------------
ST shipped this as phase48 (default 'classic') and then phase50, which flipped
the column default to 'modern' AND ran
UPDATE users SET ui_theme = 'modern' WHERE ui_theme = 'classic'
overwriting every saved preference. That was a defensible call for a
single-tenant deployment deciding for its own staff after its own A/B test.
It is NOT portable to MT. Here the same statement would run against EVERY
tenant database, flipping the entire UI for tenants who never saw the test and
never asked. So MT ships the phase48 semantics only: the column defaults to
'classic' and there is no backfill of any kind.
The default for accounts that have never chosen is config DEFAULT_UI_THEME
(app/__init__.py::resolve_ui_theme), which reads the environment. A stored
users.ui_theme always wins over it. To put a tenant on the modern design by
default, set DEFAULT_UI_THEME=modern for that tenant's process — no migration,
no overwritten preferences, and individual users can still switch either way.
Uses an INFORMATION_SCHEMA existence check — safe to re-run.
"""
revision = 'phase52_user_ui_theme'
down_revision = 'phase51_external_inspector'
branch_labels = None
depends_on = None
from alembic import op
import sqlalchemy as sa
def _column_exists(conn, table, column):
return conn.execute(sa.text(
"SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS "
"WHERE TABLE_SCHEMA = DATABASE() "
"AND TABLE_NAME = :t AND COLUMN_NAME = :c"
), {"t": table, "c": column}).scalar() > 0
def upgrade():
bind = op.get_bind()
if not _column_exists(bind, 'users', 'ui_theme'):
op.execute(sa.text(
"ALTER TABLE users "
"ADD COLUMN ui_theme VARCHAR(16) NOT NULL DEFAULT 'classic'"
))
# Idempotent repair for any row holding a value outside the known set (for
# example a partially-applied earlier run). This only ever touches NULL or
# invalid values — it does NOT reset a user's deliberate 'modern' choice.
op.execute(sa.text(
"UPDATE users SET ui_theme = 'classic' "
"WHERE ui_theme IS NULL OR ui_theme NOT IN ('classic', 'modern')"
))
def downgrade():
bind = op.get_bind()
if _column_exists(bind, 'users', 'ui_theme'):
op.execute(sa.text("ALTER TABLE users DROP COLUMN ui_theme"))