Files
shopdb-flask/migrations/versions/7d18_supportteamcontacts.py
Cam Proudlock a9d0dfff4a Initial import: shopdb-flask multi-site asset platform
Flask + Vue rewrite of the classic-ASP shopdb, built as a plugin
framework so sister GE Aerospace sites can adopt it. Product version
0.7.0-pre-0.8.0, plugin contract 0.11.0, 966 backend tests.

Key entry points:
- docs/INSTALL-WINDOWS-IIS.md - tested Windows + IIS install runbook
  (own-site and subpath deployment methods)
- docs/PILOT-DEPLOY.md - production pilot: stand up + import + verify
- docs/IMPORT-API.md - schema-agnostic legacy data import contract
- scripts/site_imports/wjf/ - West Jefferson reference import loader
- docs/adr/ - twelve accepted architecture decision records
2026-07-14 07:58:51 -04:00

151 lines
5.9 KiB
Python

"""Support teams get contacts; drop the legacy appowners pair
Restructures the application support model: supportteams keeps teamname (now
unique) + teamurl (widened to TEXT for ServiceNow group deep links), gains a
child supportteamcontacts table, and sheds its single-owner appownerid FK and
the appowners table. Each legacy team's appowner is migrated into ONE contact.
applications.supportteamid is unchanged (it already points at supportteams).
Idempotent guards throughout so it is safe on a partially-migrated box.
Revision ID: 7d18_supportteamcontacts
Revises: 7d17_machines_rename
Create Date: 2026-07-11
"""
from alembic import op
import sqlalchemy as sa
revision = '7d18_supportteamcontacts'
down_revision = '7d17_machines_rename'
branch_labels = None
depends_on = None
def _has_table(insp, name):
return insp.has_table(name)
def _has_column(insp, table, column):
return column in [c['name'] for c in insp.get_columns(table)]
def _has_unique(insp, table, name):
return name in [u['name'] for u in insp.get_unique_constraints(table)]
def upgrade():
bind = op.get_bind()
insp = sa.inspect(bind)
# 1. New contacts table.
if not _has_table(insp, 'supportteamcontacts'):
op.create_table(
'supportteamcontacts',
sa.Column('contactid', sa.Integer(), primary_key=True),
sa.Column('supportteamid', sa.Integer(), nullable=False),
sa.Column('name', sa.String(length=100), nullable=False),
sa.Column('sso', sa.String(length=50), nullable=True),
sa.Column('sortorder', sa.Integer(), nullable=False,
server_default='0'),
sa.Column('createddate', sa.DateTime(), nullable=False),
sa.Column('modifieddate', sa.DateTime(), nullable=False),
sa.Column('isactive', sa.Boolean(), nullable=False,
server_default='1'),
sa.ForeignKeyConstraint(
['supportteamid'], ['supportteams.supportteamid'],
ondelete='CASCADE'),
)
# 2. Migrate each team's appowner into one contact.
if _has_table(insp, 'appowners') and \
_has_column(insp, 'supportteams', 'appownerid'):
bind.exec_driver_sql(
"INSERT INTO supportteamcontacts "
"(supportteamid, name, sso, sortorder, isactive, "
" createddate, modifieddate) "
"SELECT st.supportteamid, ao.appowner, ao.sso, 0, 1, "
" NOW(), NOW() "
"FROM supportteams st "
"JOIN appowners ao ON st.appownerid = ao.appownerid "
"WHERE st.appownerid IS NOT NULL")
# 3. teamname becomes unique.
if not _has_unique(insp, 'supportteams', 'uq_supportteams_teamname'):
op.create_unique_constraint(
'uq_supportteams_teamname', 'supportteams', ['teamname'])
# 4. Widen teamurl to TEXT.
op.alter_column('supportteams', 'teamurl',
existing_type=sa.String(length=255),
type_=sa.Text(), existing_nullable=True)
# 5. Drop the appownerid FK + column, then the appowners table.
if _has_column(insp, 'supportteams', 'appownerid'):
for fk in insp.get_foreign_keys('supportteams'):
if 'appownerid' in fk['constrained_columns'] and fk.get('name'):
bind.exec_driver_sql(
f"ALTER TABLE supportteams DROP FOREIGN KEY {fk['name']}")
op.drop_column('supportteams', 'appownerid')
bind.exec_driver_sql("DROP TABLE IF EXISTS appowners")
def downgrade():
bind = op.get_bind()
insp = sa.inspect(bind)
# Recreate appowners.
if not _has_table(insp, 'appowners'):
op.create_table(
'appowners',
sa.Column('appownerid', sa.Integer(), primary_key=True),
sa.Column('appowner', sa.String(length=100), nullable=False),
sa.Column('sso', sa.String(length=50), nullable=True),
sa.Column('email', sa.String(length=100), nullable=True),
sa.Column('createddate', sa.DateTime(), nullable=False),
sa.Column('modifieddate', sa.DateTime(), nullable=False),
sa.Column('isactive', sa.Boolean(), nullable=False,
server_default='1'),
)
# Re-add the appownerid FK column.
if not _has_column(insp, 'supportteams', 'appownerid'):
op.add_column('supportteams',
sa.Column('appownerid', sa.Integer(), nullable=True))
op.create_foreign_key(
'fk_supportteams_appownerid', 'supportteams', 'appowners',
['appownerid'], ['appownerid'])
# Best-effort data reversal: each team's first active contact -> an owner.
if _has_table(insp, 'supportteamcontacts'):
rows = bind.execute(sa.text(
"SELECT supportteamid, name, sso FROM supportteamcontacts "
"WHERE isactive = 1 ORDER BY supportteamid, sortorder, contactid"))
seen = set()
for row in rows:
if row.supportteamid in seen:
continue
seen.add(row.supportteamid)
result = bind.execute(sa.text(
"INSERT INTO appowners "
"(appowner, sso, isactive, createddate, modifieddate) "
"VALUES (:name, :sso, 1, NOW(), NOW())"),
{'name': row.name, 'sso': row.sso})
bind.execute(sa.text(
"UPDATE supportteams SET appownerid = :ownerid "
"WHERE supportteamid = :teamid"),
{'ownerid': result.lastrowid, 'teamid': row.supportteamid})
# Narrow teamurl back to VARCHAR(255).
op.alter_column('supportteams', 'teamurl',
existing_type=sa.Text(),
type_=sa.String(length=255), existing_nullable=True)
# Drop the unique constraint.
if _has_unique(insp, 'supportteams', 'uq_supportteams_teamname'):
op.drop_constraint('uq_supportteams_teamname', 'supportteams',
type_='unique')
op.drop_table('supportteamcontacts')