Files
JQC_multi_tenant/migrations/versions/phase49_followup_requested_by.py
T

98 lines
3.5 KiB
Python

"""phase49 — follow-up request attribution (customer-raised follow-ups)
Ports single-tenant phase46 onto the multi-tenant chain. Adds to `inspections`:
follow_up_requested_by INT NULL FK → users(id) ON DELETE SET NULL
follow_up_requested_at DATETIME NULL
Customers can now request a follow-up re-inspection of a completed inspection at
their own facilities (previously admin/director only), so `follow_up_required`
alone is no longer enough — staff need to see WHO is waiting on the
re-inspection, and a client request must be visibly distinct from an internal
one. `flag_followup()` sets both columns; `clear_followup()` nulls them.
No backfill: legacy rows keep NULL, which the UI renders as an unattributed
follow-up exactly as it did before. FK is SET NULL so deleting a user never
deletes inspection history.
Revision id note
----------------
`alembic_version.version_num` is VARCHAR(32); the id below is 23 characters.
The filename stays descriptive — Alembic keys on the `revision` string.
Uses INFORMATION_SCHEMA checks — safe to re-run on every tenant DB. Additive
only: nothing is renamed, retyped or dropped.
"""
revision = 'phase49_followup_req_by'
down_revision = 'phase48_sched_parent_insp'
branch_labels = None
depends_on = None
from alembic import op
import sqlalchemy as sa
_TABLE = 'inspections'
_FK_NAME = 'fk_inspections_follow_up_requested_by'
def _table_exists(conn, table):
return conn.execute(sa.text(
"SELECT COUNT(*) FROM INFORMATION_SCHEMA.TABLES "
"WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = :t"
), {"t": table}).scalar() > 0
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 _fk_exists(conn, table, name):
return conn.execute(sa.text(
"SELECT COUNT(*) FROM INFORMATION_SCHEMA.TABLE_CONSTRAINTS "
"WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = :t "
"AND CONSTRAINT_NAME = :n AND CONSTRAINT_TYPE = 'FOREIGN KEY'"
), {"t": table, "n": name}).scalar() > 0
def upgrade():
bind = op.get_bind()
if not _table_exists(bind, _TABLE):
return
if not _column_exists(bind, _TABLE, 'follow_up_requested_by'):
op.execute(sa.text(
f"ALTER TABLE {_TABLE} "
f"ADD COLUMN follow_up_requested_by INT NULL AFTER follow_up_note"
))
if not _column_exists(bind, _TABLE, 'follow_up_requested_at'):
op.execute(sa.text(
f"ALTER TABLE {_TABLE} "
f"ADD COLUMN follow_up_requested_at DATETIME NULL "
f"AFTER follow_up_requested_by"
))
if not _fk_exists(bind, _TABLE, _FK_NAME):
op.execute(sa.text(
f"ALTER TABLE {_TABLE} ADD CONSTRAINT {_FK_NAME} "
f"FOREIGN KEY (follow_up_requested_by) REFERENCES users(id) "
f"ON DELETE SET NULL"
))
def downgrade():
bind = op.get_bind()
if not _table_exists(bind, _TABLE):
return
# FK first — MySQL refuses to drop a column still referenced by one.
if _fk_exists(bind, _TABLE, _FK_NAME):
op.execute(sa.text(f"ALTER TABLE {_TABLE} DROP FOREIGN KEY {_FK_NAME}"))
for col in ('follow_up_requested_at', 'follow_up_requested_by'):
if _column_exists(bind, _TABLE, col):
op.execute(sa.text(f"ALTER TABLE {_TABLE} DROP COLUMN {col}"))