Jul 17 - Fill the gaps between Single-tenant mode and Multi-tenant mode - MT4
This commit is contained in:
@@ -0,0 +1,120 @@
|
||||
"""phase43 — plan semantics for inspection_schedules
|
||||
|
||||
Extends MT's `inspection_schedules` (phase34) with the single-tenant "plan"
|
||||
model (ST phase36_scheduled_inspections), rather than adding a second, competing
|
||||
scheduler table. Adds:
|
||||
|
||||
notes TEXT — free-text brief for the inspector
|
||||
last_completed_at DATETIME — when the last occurrence was fulfilled
|
||||
advance_notified BOOL — per-occurrence reminder de-dup flags, reset
|
||||
due_notified BOOL when a recurring schedule rolls forward
|
||||
overdue_notified BOOL
|
||||
mode ENUM — 'auto' = cron materialises the Inspection
|
||||
'plan' = inspector clicks Start (ST behaviour)
|
||||
|
||||
inspections.inspection_schedule_id — FK back to the originating schedule
|
||||
|
||||
`next_run_at` (phase34) is reused as the due datetime — ST's `next_due_date` by
|
||||
another name. No duplicate column, no renames (rule 7).
|
||||
|
||||
Existing rows keep working exactly as before: `mode` defaults to 'auto', which
|
||||
is the only behaviour that has ever existed in MT. Nothing flips to 'plan'
|
||||
unless someone chooses it in the form.
|
||||
|
||||
INFORMATION_SCHEMA-guarded throughout — safe to re-run on every tenant DB.
|
||||
"""
|
||||
|
||||
revision = 'phase43_schedule_plan_fields'
|
||||
down_revision = 'phase42_area_qr_token'
|
||||
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 _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 _index_exists(conn, table, index):
|
||||
return conn.execute(sa.text(
|
||||
"SELECT COUNT(*) FROM INFORMATION_SCHEMA.STATISTICS "
|
||||
"WHERE TABLE_SCHEMA = DATABASE() "
|
||||
"AND TABLE_NAME = :t AND INDEX_NAME = :i"
|
||||
), {"t": table, "i": index}).scalar() > 0
|
||||
|
||||
|
||||
_NEW_COLUMNS = (
|
||||
('notes', 'TEXT NULL'),
|
||||
('last_completed_at', 'DATETIME NULL'),
|
||||
('advance_notified', 'TINYINT(1) NOT NULL DEFAULT 0'),
|
||||
('due_notified', 'TINYINT(1) NOT NULL DEFAULT 0'),
|
||||
('overdue_notified', 'TINYINT(1) NOT NULL DEFAULT 0'),
|
||||
('mode', "ENUM('auto','plan') NOT NULL DEFAULT 'auto'"),
|
||||
)
|
||||
|
||||
|
||||
def upgrade():
|
||||
bind = op.get_bind()
|
||||
|
||||
for name, ddl in _NEW_COLUMNS:
|
||||
if not _column_exists(bind, 'inspection_schedules', name):
|
||||
op.execute(sa.text(
|
||||
f"ALTER TABLE inspection_schedules ADD COLUMN {name} {ddl}"
|
||||
))
|
||||
|
||||
# Link a materialised/started Inspection back to its schedule.
|
||||
if not _column_exists(bind, 'inspections', 'inspection_schedule_id'):
|
||||
op.execute(sa.text(
|
||||
"ALTER TABLE inspections ADD COLUMN inspection_schedule_id INT NULL"
|
||||
))
|
||||
if not _index_exists(bind, 'inspections', 'ix_inspections_inspection_schedule_id'):
|
||||
op.execute(sa.text(
|
||||
"CREATE INDEX ix_inspections_inspection_schedule_id "
|
||||
"ON inspections (inspection_schedule_id)"
|
||||
))
|
||||
if not _fk_exists(bind, 'inspections', 'fk_inspections_inspection_schedule'):
|
||||
# ON DELETE SET NULL: deleting a schedule must never delete completed
|
||||
# inspection history.
|
||||
op.execute(sa.text(
|
||||
"ALTER TABLE inspections "
|
||||
"ADD CONSTRAINT fk_inspections_inspection_schedule "
|
||||
"FOREIGN KEY (inspection_schedule_id) "
|
||||
"REFERENCES inspection_schedules (id) ON DELETE SET NULL"
|
||||
))
|
||||
|
||||
|
||||
def downgrade():
|
||||
bind = op.get_bind()
|
||||
|
||||
if _fk_exists(bind, 'inspections', 'fk_inspections_inspection_schedule'):
|
||||
op.execute(sa.text(
|
||||
"ALTER TABLE inspections DROP FOREIGN KEY fk_inspections_inspection_schedule"
|
||||
))
|
||||
if _index_exists(bind, 'inspections', 'ix_inspections_inspection_schedule_id'):
|
||||
op.execute(sa.text(
|
||||
"DROP INDEX ix_inspections_inspection_schedule_id ON inspections"
|
||||
))
|
||||
if _column_exists(bind, 'inspections', 'inspection_schedule_id'):
|
||||
op.execute(sa.text(
|
||||
"ALTER TABLE inspections DROP COLUMN inspection_schedule_id"
|
||||
))
|
||||
|
||||
for name, _ddl in reversed(_NEW_COLUMNS):
|
||||
if _column_exists(bind, 'inspection_schedules', name):
|
||||
op.execute(sa.text(
|
||||
f"ALTER TABLE inspection_schedules DROP COLUMN {name}"
|
||||
))
|
||||
Reference in New Issue
Block a user