Aug 4 - Update code to follow up - MT12b
This commit is contained in:
@@ -0,0 +1,115 @@
|
||||
"""phase48 — scheduled follow-up: link a schedule back to its parent inspection
|
||||
|
||||
Ports single-tenant phase45 onto MT's `inspection_schedules` table. Adds:
|
||||
|
||||
parent_inspection_id INT NULL -- inspection this schedule is a follow-up of
|
||||
|
||||
Lets a follow-up be *planned for a later date* rather than started immediately —
|
||||
the deferred twin of "Re-inspect Now". A one-time schedule carrying this column
|
||||
is created via POST /api/v1/scheduled-inspections/follow-up; when the inspector
|
||||
eventually starts it, the resulting inspection inherits `parent_inspection_id`,
|
||||
so it lands as a true linked re-inspection: pre-filled from the parent, and
|
||||
clearing the parent's `follow_up_required` on submit.
|
||||
|
||||
Without the column the scheduled run would be an ordinary inspection — no link,
|
||||
no prefill, and the parent's follow-up flag would stay set forever.
|
||||
|
||||
Depends on phase45's `once` frequency: a follow-up is a single planned visit,
|
||||
not a recurrence.
|
||||
|
||||
NULL means "not a follow-up", which is what every existing row is, so there is
|
||||
no backfill and no schedule changes behaviour on deploy.
|
||||
|
||||
ON DELETE SET NULL: deleting the parent inspection must not cascade away a
|
||||
schedule the inspector still has to perform — it just stops being a follow-up.
|
||||
This mirrors `inspections.inspection_schedule_id`, which points the other way
|
||||
with the same rule, so neither side of the pair can delete the other's history.
|
||||
|
||||
Uses INFORMATION_SCHEMA existence checks — safe to re-run on every tenant DB.
|
||||
Additive only: nothing is renamed, retyped or dropped.
|
||||
"""
|
||||
|
||||
revision = 'phase48_schedule_parent_inspection'
|
||||
down_revision = 'phase47_schedule_end_date'
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
|
||||
_TABLE = 'inspection_schedules'
|
||||
_COLUMN = 'parent_inspection_id'
|
||||
_FK = 'fk_inspection_schedules_parent_inspection'
|
||||
_INDEX = 'ix_inspection_schedules_parent_inspection_id'
|
||||
|
||||
|
||||
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 _constraint_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"
|
||||
), {"t": table, "n": name}).scalar() > 0
|
||||
|
||||
|
||||
def _index_exists(conn, table, name):
|
||||
return conn.execute(sa.text(
|
||||
"SELECT COUNT(*) FROM INFORMATION_SCHEMA.STATISTICS "
|
||||
"WHERE TABLE_SCHEMA = DATABASE() "
|
||||
"AND TABLE_NAME = :t AND INDEX_NAME = :n"
|
||||
), {"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, _COLUMN):
|
||||
op.execute(sa.text(
|
||||
f"ALTER TABLE {_TABLE} ADD COLUMN {_COLUMN} INT NULL AFTER notes"
|
||||
))
|
||||
|
||||
# Named explicitly so the downgrade and the re-run check can find it. MySQL
|
||||
# auto-creates an index for a foreign key, but only when no usable index
|
||||
# exists; creating it first means the name is ours and predictable.
|
||||
if not _index_exists(bind, _TABLE, _INDEX):
|
||||
op.execute(sa.text(
|
||||
f"CREATE INDEX {_INDEX} ON {_TABLE} ({_COLUMN})"
|
||||
))
|
||||
|
||||
if not _constraint_exists(bind, _TABLE, _FK):
|
||||
op.execute(sa.text(
|
||||
f"ALTER TABLE {_TABLE} ADD CONSTRAINT {_FK} "
|
||||
f"FOREIGN KEY ({_COLUMN}) REFERENCES inspections(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 or index still referenced by one.
|
||||
if _constraint_exists(bind, _TABLE, _FK):
|
||||
op.execute(sa.text(f"ALTER TABLE {_TABLE} DROP FOREIGN KEY {_FK}"))
|
||||
if _index_exists(bind, _TABLE, _INDEX):
|
||||
op.execute(sa.text(f"DROP INDEX {_INDEX} ON {_TABLE}"))
|
||||
if _column_exists(bind, _TABLE, _COLUMN):
|
||||
op.execute(sa.text(f"ALTER TABLE {_TABLE} DROP COLUMN {_COLUMN}"))
|
||||
Reference in New Issue
Block a user