"""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"))