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
This commit is contained in:
956
migrations/versions/68b3947ae14f_baseline_schema.py
Normal file
956
migrations/versions/68b3947ae14f_baseline_schema.py
Normal file
@@ -0,0 +1,956 @@
|
||||
"""baseline schema
|
||||
|
||||
Revision ID: 68b3947ae14f
|
||||
Revises:
|
||||
Create Date: 2026-05-08 17:53:08.342776
|
||||
|
||||
"""
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = '68b3947ae14f'
|
||||
down_revision = None
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade():
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.create_table('appowners',
|
||||
sa.Column('appownerid', sa.Integer(), nullable=False),
|
||||
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),
|
||||
sa.PrimaryKeyConstraint('appownerid')
|
||||
)
|
||||
op.create_table('assetstatuses',
|
||||
sa.Column('statusid', sa.Integer(), nullable=False),
|
||||
sa.Column('status', sa.String(length=50), nullable=False),
|
||||
sa.Column('description', sa.Text(), nullable=True),
|
||||
sa.Column('color', sa.String(length=20), nullable=True, comment='CSS color for UI'),
|
||||
sa.Column('createddate', sa.DateTime(), nullable=False),
|
||||
sa.Column('modifieddate', sa.DateTime(), nullable=False),
|
||||
sa.Column('isactive', sa.Boolean(), nullable=False),
|
||||
sa.PrimaryKeyConstraint('statusid'),
|
||||
sa.UniqueConstraint('status')
|
||||
)
|
||||
op.create_table('assettypes',
|
||||
sa.Column('assettypeid', sa.Integer(), nullable=False),
|
||||
sa.Column('assettype', sa.String(length=50), nullable=False, comment='Category name: equipment, computer, network_device, printer'),
|
||||
sa.Column('pluginname', sa.String(length=100), nullable=True, comment='Plugin that owns this type'),
|
||||
sa.Column('tablename', sa.String(length=100), nullable=True, comment='Extension table name for this type'),
|
||||
sa.Column('description', sa.Text(), nullable=True),
|
||||
sa.Column('icon', sa.String(length=50), nullable=True, comment='Icon name for UI'),
|
||||
sa.Column('createddate', sa.DateTime(), nullable=False),
|
||||
sa.Column('modifieddate', sa.DateTime(), nullable=False),
|
||||
sa.Column('isactive', sa.Boolean(), nullable=False),
|
||||
sa.PrimaryKeyConstraint('assettypeid'),
|
||||
sa.UniqueConstraint('assettype')
|
||||
)
|
||||
op.create_table('businessunits',
|
||||
sa.Column('businessunitid', sa.Integer(), nullable=False),
|
||||
sa.Column('businessunit', sa.String(length=100), nullable=False),
|
||||
sa.Column('code', sa.String(length=20), nullable=True, comment='Short code'),
|
||||
sa.Column('description', sa.Text(), nullable=True),
|
||||
sa.Column('parentid', sa.Integer(), nullable=True),
|
||||
sa.Column('createddate', sa.DateTime(), nullable=False),
|
||||
sa.Column('modifieddate', sa.DateTime(), nullable=False),
|
||||
sa.Column('isactive', sa.Boolean(), nullable=False),
|
||||
sa.ForeignKeyConstraint(['parentid'], ['businessunits.businessunitid'], ),
|
||||
sa.PrimaryKeyConstraint('businessunitid'),
|
||||
sa.UniqueConstraint('businessunit'),
|
||||
sa.UniqueConstraint('code')
|
||||
)
|
||||
op.create_table('communicationtypes',
|
||||
sa.Column('comtypeid', sa.Integer(), nullable=False),
|
||||
sa.Column('comtype', sa.String(length=50), nullable=False),
|
||||
sa.Column('description', sa.Text(), nullable=True),
|
||||
sa.Column('createddate', sa.DateTime(), nullable=False),
|
||||
sa.Column('modifieddate', sa.DateTime(), nullable=False),
|
||||
sa.Column('isactive', sa.Boolean(), nullable=False),
|
||||
sa.PrimaryKeyConstraint('comtypeid'),
|
||||
sa.UniqueConstraint('comtype')
|
||||
)
|
||||
op.create_table('computertypes',
|
||||
sa.Column('computertypeid', sa.Integer(), nullable=False),
|
||||
sa.Column('computertype', sa.String(length=100), nullable=False),
|
||||
sa.Column('description', sa.Text(), nullable=True),
|
||||
sa.Column('icon', sa.String(length=50), nullable=True, comment='Icon name for UI'),
|
||||
sa.Column('createddate', sa.DateTime(), nullable=False),
|
||||
sa.Column('modifieddate', sa.DateTime(), nullable=False),
|
||||
sa.Column('isactive', sa.Boolean(), nullable=False),
|
||||
sa.PrimaryKeyConstraint('computertypeid'),
|
||||
sa.UniqueConstraint('computertype')
|
||||
)
|
||||
op.create_table('equipmenttypes',
|
||||
sa.Column('equipmenttypeid', sa.Integer(), nullable=False),
|
||||
sa.Column('equipmenttype', sa.String(length=100), nullable=False),
|
||||
sa.Column('description', sa.Text(), nullable=True),
|
||||
sa.Column('icon', sa.String(length=50), nullable=True, comment='Icon name for UI'),
|
||||
sa.Column('createddate', sa.DateTime(), nullable=False),
|
||||
sa.Column('modifieddate', sa.DateTime(), nullable=False),
|
||||
sa.Column('isactive', sa.Boolean(), nullable=False),
|
||||
sa.PrimaryKeyConstraint('equipmenttypeid'),
|
||||
sa.UniqueConstraint('equipmenttype')
|
||||
)
|
||||
op.create_table('locations',
|
||||
sa.Column('locationid', sa.Integer(), nullable=False),
|
||||
sa.Column('locationname', sa.String(length=100), nullable=False),
|
||||
sa.Column('building', sa.String(length=100), nullable=True),
|
||||
sa.Column('floor', sa.String(length=50), nullable=True),
|
||||
sa.Column('room', sa.String(length=50), nullable=True),
|
||||
sa.Column('description', sa.Text(), nullable=True),
|
||||
sa.Column('mapimage', sa.String(length=500), nullable=True, comment='Path to floor map image'),
|
||||
sa.Column('mapwidth', sa.Integer(), nullable=True),
|
||||
sa.Column('mapheight', sa.Integer(), nullable=True),
|
||||
sa.Column('createddate', sa.DateTime(), nullable=False),
|
||||
sa.Column('modifieddate', sa.DateTime(), nullable=False),
|
||||
sa.Column('isactive', sa.Boolean(), nullable=False),
|
||||
sa.PrimaryKeyConstraint('locationid'),
|
||||
sa.UniqueConstraint('locationname')
|
||||
)
|
||||
op.create_table('machinestatuses',
|
||||
sa.Column('statusid', sa.Integer(), nullable=False),
|
||||
sa.Column('status', sa.String(length=50), nullable=False),
|
||||
sa.Column('description', sa.Text(), nullable=True),
|
||||
sa.Column('color', sa.String(length=20), nullable=True, comment='CSS color for UI'),
|
||||
sa.Column('createddate', sa.DateTime(), nullable=False),
|
||||
sa.Column('modifieddate', sa.DateTime(), nullable=False),
|
||||
sa.Column('isactive', sa.Boolean(), nullable=False),
|
||||
sa.PrimaryKeyConstraint('statusid'),
|
||||
sa.UniqueConstraint('status')
|
||||
)
|
||||
op.create_table('machinetypes',
|
||||
sa.Column('machinetypeid', sa.Integer(), nullable=False),
|
||||
sa.Column('machinetype', sa.String(length=100), nullable=False),
|
||||
sa.Column('category', sa.String(length=50), nullable=False, comment='Equipment, PC, Network, or Printer'),
|
||||
sa.Column('description', sa.Text(), nullable=True),
|
||||
sa.Column('icon', sa.String(length=50), nullable=True, comment='Icon name for UI'),
|
||||
sa.Column('createddate', sa.DateTime(), nullable=False),
|
||||
sa.Column('modifieddate', sa.DateTime(), nullable=False),
|
||||
sa.Column('isactive', sa.Boolean(), nullable=False),
|
||||
sa.PrimaryKeyConstraint('machinetypeid'),
|
||||
sa.UniqueConstraint('machinetype')
|
||||
)
|
||||
op.create_table('networkdevicetypes',
|
||||
sa.Column('networkdevicetypeid', sa.Integer(), nullable=False),
|
||||
sa.Column('networkdevicetype', sa.String(length=100), nullable=False),
|
||||
sa.Column('description', sa.Text(), nullable=True),
|
||||
sa.Column('icon', sa.String(length=50), nullable=True, comment='Icon name for UI'),
|
||||
sa.Column('createddate', sa.DateTime(), nullable=False),
|
||||
sa.Column('modifieddate', sa.DateTime(), nullable=False),
|
||||
sa.Column('isactive', sa.Boolean(), nullable=False),
|
||||
sa.PrimaryKeyConstraint('networkdevicetypeid'),
|
||||
sa.UniqueConstraint('networkdevicetype')
|
||||
)
|
||||
op.create_table('notificationtypes',
|
||||
sa.Column('notificationtypeid', sa.Integer(), nullable=False),
|
||||
sa.Column('typename', sa.String(length=50), nullable=False),
|
||||
sa.Column('typedescription', sa.Text(), nullable=True),
|
||||
sa.Column('typecolor', sa.String(length=20), nullable=True),
|
||||
sa.Column('isactive', sa.Boolean(), nullable=True),
|
||||
sa.PrimaryKeyConstraint('notificationtypeid')
|
||||
)
|
||||
op.create_table('operatingsystems',
|
||||
sa.Column('osid', sa.Integer(), nullable=False),
|
||||
sa.Column('osname', sa.String(length=100), nullable=False),
|
||||
sa.Column('osversion', sa.String(length=50), nullable=True),
|
||||
sa.Column('architecture', sa.String(length=20), nullable=True, comment='x86, x64, ARM'),
|
||||
sa.Column('endoflife', sa.Date(), nullable=True, comment='End of support date'),
|
||||
sa.Column('createddate', sa.DateTime(), nullable=False),
|
||||
sa.Column('modifieddate', sa.DateTime(), nullable=False),
|
||||
sa.Column('isactive', sa.Boolean(), nullable=False),
|
||||
sa.PrimaryKeyConstraint('osid'),
|
||||
sa.UniqueConstraint('osname', 'osversion', name='uq_os_name_version')
|
||||
)
|
||||
op.create_table('pctypes',
|
||||
sa.Column('pctypeid', sa.Integer(), nullable=False),
|
||||
sa.Column('pctype', sa.String(length=100), nullable=False),
|
||||
sa.Column('description', sa.Text(), nullable=True),
|
||||
sa.Column('createddate', sa.DateTime(), nullable=False),
|
||||
sa.Column('modifieddate', sa.DateTime(), nullable=False),
|
||||
sa.Column('isactive', sa.Boolean(), nullable=False),
|
||||
sa.PrimaryKeyConstraint('pctypeid'),
|
||||
sa.UniqueConstraint('pctype')
|
||||
)
|
||||
op.create_table('permissions',
|
||||
sa.Column('permissionid', sa.Integer(), nullable=False),
|
||||
sa.Column('name', sa.String(length=50), nullable=False),
|
||||
sa.Column('description', sa.String(length=255), nullable=True),
|
||||
sa.Column('category', sa.String(length=50), nullable=True),
|
||||
sa.PrimaryKeyConstraint('permissionid'),
|
||||
sa.UniqueConstraint('name')
|
||||
)
|
||||
op.create_table('printertypes',
|
||||
sa.Column('printertypeid', sa.Integer(), nullable=False),
|
||||
sa.Column('printertype', sa.String(length=100), nullable=False),
|
||||
sa.Column('description', sa.Text(), nullable=True),
|
||||
sa.Column('icon', sa.String(length=50), nullable=True, comment='Icon name for UI'),
|
||||
sa.Column('createddate', sa.DateTime(), nullable=False),
|
||||
sa.Column('modifieddate', sa.DateTime(), nullable=False),
|
||||
sa.Column('isactive', sa.Boolean(), nullable=False),
|
||||
sa.PrimaryKeyConstraint('printertypeid'),
|
||||
sa.UniqueConstraint('printertype')
|
||||
)
|
||||
op.create_table('relationshiptypes',
|
||||
sa.Column('relationshiptypeid', sa.Integer(), nullable=False),
|
||||
sa.Column('relationshiptype', sa.String(length=50), nullable=False),
|
||||
sa.Column('description', sa.Text(), nullable=True),
|
||||
sa.Column('createddate', sa.DateTime(), nullable=False),
|
||||
sa.Column('modifieddate', sa.DateTime(), nullable=False),
|
||||
sa.Column('isactive', sa.Boolean(), nullable=False),
|
||||
sa.PrimaryKeyConstraint('relationshiptypeid'),
|
||||
sa.UniqueConstraint('relationshiptype')
|
||||
)
|
||||
op.create_table('roles',
|
||||
sa.Column('roleid', sa.Integer(), nullable=False),
|
||||
sa.Column('rolename', sa.String(length=50), nullable=False),
|
||||
sa.Column('description', sa.Text(), nullable=True),
|
||||
sa.Column('createddate', sa.DateTime(), nullable=False),
|
||||
sa.Column('modifieddate', sa.DateTime(), nullable=False),
|
||||
sa.Column('isactive', sa.Boolean(), nullable=False),
|
||||
sa.PrimaryKeyConstraint('roleid'),
|
||||
sa.UniqueConstraint('rolename')
|
||||
)
|
||||
op.create_table('settings',
|
||||
sa.Column('settingid', sa.Integer(), autoincrement=True, nullable=False),
|
||||
sa.Column('key', sa.String(length=100), nullable=False),
|
||||
sa.Column('value', sa.Text(), nullable=True),
|
||||
sa.Column('valuetype', sa.String(length=20), nullable=True),
|
||||
sa.Column('category', sa.String(length=50), nullable=True),
|
||||
sa.Column('description', sa.String(length=255), nullable=True),
|
||||
sa.Column('createddate', sa.DateTime(), nullable=True),
|
||||
sa.Column('modifieddate', sa.DateTime(), nullable=True),
|
||||
sa.PrimaryKeyConstraint('settingid')
|
||||
)
|
||||
with op.batch_alter_table('settings', schema=None) as batch_op:
|
||||
batch_op.create_index(batch_op.f('ix_settings_key'), ['key'], unique=True)
|
||||
|
||||
op.create_table('usbdevicetypes',
|
||||
sa.Column('usbdevicetypeid', sa.Integer(), nullable=False),
|
||||
sa.Column('typename', sa.String(length=50), nullable=False),
|
||||
sa.Column('description', sa.Text(), nullable=True),
|
||||
sa.Column('icon', sa.String(length=50), nullable=True, comment='Icon name for UI'),
|
||||
sa.Column('createddate', sa.DateTime(), nullable=False),
|
||||
sa.Column('modifieddate', sa.DateTime(), nullable=False),
|
||||
sa.Column('isactive', sa.Boolean(), nullable=False),
|
||||
sa.PrimaryKeyConstraint('usbdevicetypeid'),
|
||||
sa.UniqueConstraint('typename')
|
||||
)
|
||||
op.create_table('users',
|
||||
sa.Column('userid', sa.Integer(), nullable=False),
|
||||
sa.Column('username', sa.String(length=100), nullable=False),
|
||||
sa.Column('email', sa.String(length=255), nullable=False),
|
||||
sa.Column('passwordhash', sa.String(length=255), nullable=False),
|
||||
sa.Column('firstname', sa.String(length=100), nullable=True),
|
||||
sa.Column('lastname', sa.String(length=100), nullable=True),
|
||||
sa.Column('lastlogindate', sa.DateTime(), nullable=True),
|
||||
sa.Column('failedlogins', sa.Integer(), nullable=True),
|
||||
sa.Column('lockeduntil', sa.DateTime(), nullable=True),
|
||||
sa.Column('createddate', sa.DateTime(), nullable=False),
|
||||
sa.Column('modifieddate', sa.DateTime(), nullable=False),
|
||||
sa.Column('isactive', sa.Boolean(), nullable=False),
|
||||
sa.PrimaryKeyConstraint('userid'),
|
||||
sa.UniqueConstraint('email')
|
||||
)
|
||||
with op.batch_alter_table('users', schema=None) as batch_op:
|
||||
batch_op.create_index(batch_op.f('ix_users_username'), ['username'], unique=True)
|
||||
|
||||
op.create_table('vendors',
|
||||
sa.Column('vendorid', sa.Integer(), nullable=False),
|
||||
sa.Column('vendor', sa.String(length=100), nullable=False),
|
||||
sa.Column('description', sa.Text(), nullable=True),
|
||||
sa.Column('website', sa.String(length=255), nullable=True),
|
||||
sa.Column('supportphone', sa.String(length=50), nullable=True),
|
||||
sa.Column('supportemail', sa.String(length=100), nullable=True),
|
||||
sa.Column('notes', sa.Text(), nullable=True),
|
||||
sa.Column('createddate', sa.DateTime(), nullable=False),
|
||||
sa.Column('modifieddate', sa.DateTime(), nullable=False),
|
||||
sa.Column('isactive', sa.Boolean(), nullable=False),
|
||||
sa.PrimaryKeyConstraint('vendorid'),
|
||||
sa.UniqueConstraint('vendor')
|
||||
)
|
||||
op.create_table('vlans',
|
||||
sa.Column('vlanid', sa.Integer(), nullable=False),
|
||||
sa.Column('vlannumber', sa.Integer(), nullable=False, comment='VLAN ID number'),
|
||||
sa.Column('name', sa.String(length=100), nullable=False, comment='VLAN name'),
|
||||
sa.Column('description', sa.Text(), nullable=True),
|
||||
sa.Column('vlantype', sa.String(length=50), nullable=True, comment='Type: data, voice, management, guest, etc.'),
|
||||
sa.Column('createddate', sa.DateTime(), nullable=False),
|
||||
sa.Column('modifieddate', sa.DateTime(), nullable=False),
|
||||
sa.Column('isactive', sa.Boolean(), nullable=False),
|
||||
sa.PrimaryKeyConstraint('vlanid'),
|
||||
sa.UniqueConstraint('vlannumber')
|
||||
)
|
||||
with op.batch_alter_table('vlans', schema=None) as batch_op:
|
||||
batch_op.create_index('idx_vlan_number', ['vlannumber'], unique=False)
|
||||
|
||||
op.create_table('assets',
|
||||
sa.Column('assetid', sa.Integer(), nullable=False),
|
||||
sa.Column('assetnumber', sa.String(length=50), nullable=False, comment='Business identifier (e.g., CMM01, G5QX1GT3ESF)'),
|
||||
sa.Column('name', sa.String(length=100), nullable=True, comment='Display name/alias'),
|
||||
sa.Column('serialnumber', sa.String(length=100), nullable=True, comment='Hardware serial number'),
|
||||
sa.Column('assettypeid', sa.Integer(), nullable=False),
|
||||
sa.Column('statusid', sa.Integer(), nullable=True, comment='In Use, Spare, Retired, etc.'),
|
||||
sa.Column('locationid', sa.Integer(), nullable=True),
|
||||
sa.Column('businessunitid', sa.Integer(), nullable=True),
|
||||
sa.Column('mapleft', sa.Integer(), nullable=True, comment='X coordinate on floor map'),
|
||||
sa.Column('maptop', sa.Integer(), nullable=True, comment='Y coordinate on floor map'),
|
||||
sa.Column('notes', sa.Text(), nullable=True),
|
||||
sa.Column('createddate', sa.DateTime(), nullable=False),
|
||||
sa.Column('modifieddate', sa.DateTime(), nullable=False),
|
||||
sa.Column('isactive', sa.Boolean(), nullable=False),
|
||||
sa.Column('deleteddate', sa.DateTime(), nullable=True),
|
||||
sa.Column('deletedby', sa.String(length=100), nullable=True),
|
||||
sa.Column('createdby', sa.String(length=100), nullable=True),
|
||||
sa.Column('modifiedby', sa.String(length=100), nullable=True),
|
||||
sa.ForeignKeyConstraint(['assettypeid'], ['assettypes.assettypeid'], ),
|
||||
sa.ForeignKeyConstraint(['businessunitid'], ['businessunits.businessunitid'], ),
|
||||
sa.ForeignKeyConstraint(['locationid'], ['locations.locationid'], ),
|
||||
sa.ForeignKeyConstraint(['statusid'], ['assetstatuses.statusid'], ),
|
||||
sa.PrimaryKeyConstraint('assetid')
|
||||
)
|
||||
with op.batch_alter_table('assets', schema=None) as batch_op:
|
||||
batch_op.create_index('idx_asset_active', ['isactive'], unique=False)
|
||||
batch_op.create_index('idx_asset_location', ['locationid'], unique=False)
|
||||
batch_op.create_index('idx_asset_status', ['statusid'], unique=False)
|
||||
batch_op.create_index('idx_asset_type_bu', ['assettypeid', 'businessunitid'], unique=False)
|
||||
batch_op.create_index(batch_op.f('ix_assets_assetnumber'), ['assetnumber'], unique=True)
|
||||
batch_op.create_index(batch_op.f('ix_assets_serialnumber'), ['serialnumber'], unique=False)
|
||||
|
||||
op.create_table('auditlogs',
|
||||
sa.Column('auditlogid', sa.Integer(), nullable=False),
|
||||
sa.Column('userid', sa.Integer(), nullable=True),
|
||||
sa.Column('username', sa.String(length=100), nullable=True),
|
||||
sa.Column('timestamp', sa.DateTime(), nullable=False),
|
||||
sa.Column('ipaddress', sa.String(length=45), nullable=True),
|
||||
sa.Column('useragent', sa.String(length=255), nullable=True),
|
||||
sa.Column('action', sa.String(length=20), nullable=False),
|
||||
sa.Column('entitytype', sa.String(length=50), nullable=False),
|
||||
sa.Column('entityid', sa.Integer(), nullable=True),
|
||||
sa.Column('entityname', sa.String(length=255), nullable=True),
|
||||
sa.Column('changes', sa.Text(), nullable=True),
|
||||
sa.Column('details', sa.Text(), nullable=True),
|
||||
sa.ForeignKeyConstraint(['userid'], ['users.userid'], ),
|
||||
sa.PrimaryKeyConstraint('auditlogid')
|
||||
)
|
||||
with op.batch_alter_table('auditlogs', schema=None) as batch_op:
|
||||
batch_op.create_index(batch_op.f('ix_auditlogs_action'), ['action'], unique=False)
|
||||
batch_op.create_index(batch_op.f('ix_auditlogs_entitytype'), ['entitytype'], unique=False)
|
||||
batch_op.create_index(batch_op.f('ix_auditlogs_timestamp'), ['timestamp'], unique=False)
|
||||
|
||||
op.create_table('models',
|
||||
sa.Column('modelnumberid', sa.Integer(), nullable=False),
|
||||
sa.Column('modelnumber', sa.String(length=100), nullable=False),
|
||||
sa.Column('machinetypeid', sa.Integer(), nullable=True),
|
||||
sa.Column('vendorid', sa.Integer(), nullable=True),
|
||||
sa.Column('description', sa.Text(), nullable=True),
|
||||
sa.Column('imageurl', sa.String(length=500), nullable=True, comment='URL to product image'),
|
||||
sa.Column('documentationurl', sa.String(length=500), nullable=True, comment='URL to documentation'),
|
||||
sa.Column('notes', sa.Text(), nullable=True),
|
||||
sa.Column('createddate', sa.DateTime(), nullable=False),
|
||||
sa.Column('modifieddate', sa.DateTime(), nullable=False),
|
||||
sa.Column('isactive', sa.Boolean(), nullable=False),
|
||||
sa.ForeignKeyConstraint(['machinetypeid'], ['machinetypes.machinetypeid'], ),
|
||||
sa.ForeignKeyConstraint(['vendorid'], ['vendors.vendorid'], ),
|
||||
sa.PrimaryKeyConstraint('modelnumberid'),
|
||||
sa.UniqueConstraint('modelnumber', 'vendorid', name='uq_model_vendor')
|
||||
)
|
||||
op.create_table('notifications',
|
||||
sa.Column('notificationid', sa.Integer(), nullable=False),
|
||||
sa.Column('notificationtypeid', sa.Integer(), nullable=True),
|
||||
sa.Column('businessunitid', sa.Integer(), nullable=True),
|
||||
sa.Column('appid', sa.Integer(), nullable=True),
|
||||
sa.Column('notification', sa.Text(), nullable=False, comment='The message content'),
|
||||
sa.Column('starttime', sa.DateTime(), nullable=True),
|
||||
sa.Column('endtime', sa.DateTime(), nullable=True),
|
||||
sa.Column('ticketnumber', sa.String(length=50), nullable=True),
|
||||
sa.Column('link', sa.String(length=500), nullable=True),
|
||||
sa.Column('isactive', sa.Boolean(), nullable=True),
|
||||
sa.Column('isshopfloor', sa.Boolean(), nullable=True),
|
||||
sa.Column('employeesso', sa.String(length=100), nullable=True),
|
||||
sa.Column('employeename', sa.String(length=100), nullable=True),
|
||||
sa.ForeignKeyConstraint(['notificationtypeid'], ['notificationtypes.notificationtypeid'], ),
|
||||
sa.PrimaryKeyConstraint('notificationid')
|
||||
)
|
||||
op.create_table('rolepermissions',
|
||||
sa.Column('roleid', sa.Integer(), nullable=False),
|
||||
sa.Column('permissionid', sa.Integer(), nullable=False),
|
||||
sa.ForeignKeyConstraint(['permissionid'], ['permissions.permissionid'], ),
|
||||
sa.ForeignKeyConstraint(['roleid'], ['roles.roleid'], ),
|
||||
sa.PrimaryKeyConstraint('roleid', 'permissionid')
|
||||
)
|
||||
op.create_table('subnets',
|
||||
sa.Column('subnetid', sa.Integer(), nullable=False),
|
||||
sa.Column('cidr', sa.String(length=18), nullable=False, comment='CIDR notation (e.g., 10.1.1.0/24)'),
|
||||
sa.Column('name', sa.String(length=100), nullable=False, comment='Subnet name'),
|
||||
sa.Column('description', sa.Text(), nullable=True),
|
||||
sa.Column('gatewayip', sa.String(length=15), nullable=True, comment='Default gateway IP address'),
|
||||
sa.Column('subnetmask', sa.String(length=15), nullable=True, comment='Subnet mask (e.g., 255.255.255.0)'),
|
||||
sa.Column('networkaddress', sa.String(length=15), nullable=True, comment='Network address (e.g., 10.1.1.0)'),
|
||||
sa.Column('broadcastaddress', sa.String(length=15), nullable=True, comment='Broadcast address (e.g., 10.1.1.255)'),
|
||||
sa.Column('vlanid', sa.Integer(), nullable=True),
|
||||
sa.Column('subnettype', sa.String(length=50), nullable=True, comment='Type: production, development, management, dmz, etc.'),
|
||||
sa.Column('locationid', sa.Integer(), nullable=True),
|
||||
sa.Column('dhcpenabled', sa.Boolean(), nullable=True, comment='DHCP enabled for this subnet'),
|
||||
sa.Column('dhcprangestart', sa.String(length=15), nullable=True, comment='DHCP range start IP'),
|
||||
sa.Column('dhcprangeend', sa.String(length=15), nullable=True, comment='DHCP range end IP'),
|
||||
sa.Column('dns1', sa.String(length=15), nullable=True, comment='Primary DNS server'),
|
||||
sa.Column('dns2', sa.String(length=15), nullable=True, comment='Secondary DNS server'),
|
||||
sa.Column('createddate', sa.DateTime(), nullable=False),
|
||||
sa.Column('modifieddate', sa.DateTime(), nullable=False),
|
||||
sa.Column('isactive', sa.Boolean(), nullable=False),
|
||||
sa.ForeignKeyConstraint(['locationid'], ['locations.locationid'], ),
|
||||
sa.ForeignKeyConstraint(['vlanid'], ['vlans.vlanid'], ),
|
||||
sa.PrimaryKeyConstraint('subnetid'),
|
||||
sa.UniqueConstraint('cidr')
|
||||
)
|
||||
with op.batch_alter_table('subnets', schema=None) as batch_op:
|
||||
batch_op.create_index('idx_subnet_cidr', ['cidr'], unique=False)
|
||||
batch_op.create_index('idx_subnet_location', ['locationid'], unique=False)
|
||||
batch_op.create_index('idx_subnet_vlan', ['vlanid'], unique=False)
|
||||
|
||||
op.create_table('supportteams',
|
||||
sa.Column('supportteamid', sa.Integer(), nullable=False),
|
||||
sa.Column('teamname', sa.String(length=100), nullable=False),
|
||||
sa.Column('teamurl', sa.String(length=255), nullable=True),
|
||||
sa.Column('appownerid', sa.Integer(), nullable=True),
|
||||
sa.Column('createddate', sa.DateTime(), nullable=False),
|
||||
sa.Column('modifieddate', sa.DateTime(), nullable=False),
|
||||
sa.Column('isactive', sa.Boolean(), nullable=False),
|
||||
sa.ForeignKeyConstraint(['appownerid'], ['appowners.appownerid'], ),
|
||||
sa.PrimaryKeyConstraint('supportteamid')
|
||||
)
|
||||
op.create_table('usbdevices',
|
||||
sa.Column('usbdeviceid', sa.Integer(), nullable=False),
|
||||
sa.Column('serialnumber', sa.String(length=100), nullable=False),
|
||||
sa.Column('label', sa.String(length=100), nullable=True, comment='Human-readable label'),
|
||||
sa.Column('assetnumber', sa.String(length=50), nullable=True, comment='Optional asset tag'),
|
||||
sa.Column('usbdevicetypeid', sa.Integer(), nullable=True),
|
||||
sa.Column('capacitygb', sa.Integer(), nullable=True, comment='Capacity in GB'),
|
||||
sa.Column('vendorid', sa.String(length=10), nullable=True, comment='USB Vendor ID (hex)'),
|
||||
sa.Column('productid', sa.String(length=10), nullable=True, comment='USB Product ID (hex)'),
|
||||
sa.Column('manufacturer', sa.String(length=100), nullable=True),
|
||||
sa.Column('productname', sa.String(length=100), nullable=True),
|
||||
sa.Column('ischeckedout', sa.Boolean(), nullable=True),
|
||||
sa.Column('currentuserid', sa.String(length=50), nullable=True, comment='SSO of current user'),
|
||||
sa.Column('currentusername', sa.String(length=100), nullable=True, comment='Name of current user'),
|
||||
sa.Column('currentcheckoutdate', sa.DateTime(), nullable=True),
|
||||
sa.Column('storagelocation', sa.String(length=200), nullable=True, comment='Where device is stored when not checked out'),
|
||||
sa.Column('pin', sa.String(length=50), nullable=True, comment='PIN for encrypted devices'),
|
||||
sa.Column('notes', sa.Text(), nullable=True),
|
||||
sa.Column('createddate', sa.DateTime(), nullable=False),
|
||||
sa.Column('modifieddate', sa.DateTime(), nullable=False),
|
||||
sa.Column('isactive', sa.Boolean(), nullable=False),
|
||||
sa.Column('createdby', sa.String(length=100), nullable=True),
|
||||
sa.Column('modifiedby', sa.String(length=100), nullable=True),
|
||||
sa.ForeignKeyConstraint(['usbdevicetypeid'], ['usbdevicetypes.usbdevicetypeid'], ),
|
||||
sa.PrimaryKeyConstraint('usbdeviceid'),
|
||||
sa.UniqueConstraint('serialnumber')
|
||||
)
|
||||
with op.batch_alter_table('usbdevices', schema=None) as batch_op:
|
||||
batch_op.create_index('idx_usb_checkedout', ['ischeckedout'], unique=False)
|
||||
batch_op.create_index('idx_usb_currentuser', ['currentuserid'], unique=False)
|
||||
batch_op.create_index('idx_usb_serial', ['serialnumber'], unique=False)
|
||||
batch_op.create_index('idx_usb_type', ['usbdevicetypeid'], unique=False)
|
||||
|
||||
op.create_table('userroles',
|
||||
sa.Column('userid', sa.Integer(), nullable=False),
|
||||
sa.Column('roleid', sa.Integer(), nullable=False),
|
||||
sa.ForeignKeyConstraint(['roleid'], ['roles.roleid'], ),
|
||||
sa.ForeignKeyConstraint(['userid'], ['users.userid'], ),
|
||||
sa.PrimaryKeyConstraint('userid', 'roleid')
|
||||
)
|
||||
op.create_table('applications',
|
||||
sa.Column('appid', sa.Integer(), nullable=False),
|
||||
sa.Column('appname', sa.String(length=100), nullable=False),
|
||||
sa.Column('appdescription', sa.String(length=255), nullable=True),
|
||||
sa.Column('supportteamid', sa.Integer(), nullable=True),
|
||||
sa.Column('isinstallable', sa.Boolean(), nullable=True),
|
||||
sa.Column('applicationnotes', sa.Text(), nullable=True),
|
||||
sa.Column('installpath', sa.String(length=255), nullable=True),
|
||||
sa.Column('applicationlink', sa.String(length=512), nullable=True),
|
||||
sa.Column('documentationpath', sa.String(length=512), nullable=True),
|
||||
sa.Column('ishidden', sa.Boolean(), nullable=True),
|
||||
sa.Column('isprinter', sa.Boolean(), nullable=True),
|
||||
sa.Column('islicenced', sa.Boolean(), nullable=True),
|
||||
sa.Column('image', sa.String(length=255), nullable=True),
|
||||
sa.Column('createddate', sa.DateTime(), nullable=False),
|
||||
sa.Column('modifieddate', sa.DateTime(), nullable=False),
|
||||
sa.Column('isactive', sa.Boolean(), nullable=False),
|
||||
sa.ForeignKeyConstraint(['supportteamid'], ['supportteams.supportteamid'], ),
|
||||
sa.PrimaryKeyConstraint('appid'),
|
||||
sa.UniqueConstraint('appname')
|
||||
)
|
||||
op.create_table('assetrelationships',
|
||||
sa.Column('relationshipid', sa.Integer(), nullable=False),
|
||||
sa.Column('sourceassetid', sa.Integer(), nullable=False),
|
||||
sa.Column('targetassetid', sa.Integer(), nullable=False),
|
||||
sa.Column('relationshiptypeid', sa.Integer(), nullable=False),
|
||||
sa.Column('notes', sa.Text(), nullable=True),
|
||||
sa.Column('createddate', sa.DateTime(), nullable=False),
|
||||
sa.Column('modifieddate', sa.DateTime(), nullable=False),
|
||||
sa.Column('isactive', sa.Boolean(), nullable=False),
|
||||
sa.ForeignKeyConstraint(['relationshiptypeid'], ['relationshiptypes.relationshiptypeid'], ),
|
||||
sa.ForeignKeyConstraint(['sourceassetid'], ['assets.assetid'], ),
|
||||
sa.ForeignKeyConstraint(['targetassetid'], ['assets.assetid'], ),
|
||||
sa.PrimaryKeyConstraint('relationshipid'),
|
||||
sa.UniqueConstraint('sourceassetid', 'targetassetid', 'relationshiptypeid', name='uq_asset_relationship')
|
||||
)
|
||||
with op.batch_alter_table('assetrelationships', schema=None) as batch_op:
|
||||
batch_op.create_index('idx_asset_rel_source', ['sourceassetid'], unique=False)
|
||||
batch_op.create_index('idx_asset_rel_target', ['targetassetid'], unique=False)
|
||||
|
||||
op.create_table('computers',
|
||||
sa.Column('computerid', sa.Integer(), nullable=False),
|
||||
sa.Column('assetid', sa.Integer(), nullable=False),
|
||||
sa.Column('computertypeid', sa.Integer(), nullable=True),
|
||||
sa.Column('hostname', sa.String(length=100), nullable=True, comment='Network hostname'),
|
||||
sa.Column('osid', sa.Integer(), nullable=True),
|
||||
sa.Column('loggedinuser', sa.String(length=100), nullable=True),
|
||||
sa.Column('lastreporteddate', sa.DateTime(), nullable=True),
|
||||
sa.Column('lastboottime', sa.DateTime(), nullable=True),
|
||||
sa.Column('isvnc', sa.Boolean(), nullable=True, comment='VNC remote access enabled'),
|
||||
sa.Column('iswinrm', sa.Boolean(), nullable=True, comment='WinRM enabled'),
|
||||
sa.Column('isshopfloor', sa.Boolean(), nullable=True, comment='Shopfloor PC (vs office PC)'),
|
||||
sa.Column('createddate', sa.DateTime(), nullable=False),
|
||||
sa.Column('modifieddate', sa.DateTime(), nullable=False),
|
||||
sa.Column('isactive', sa.Boolean(), nullable=False),
|
||||
sa.ForeignKeyConstraint(['assetid'], ['assets.assetid'], ondelete='CASCADE'),
|
||||
sa.ForeignKeyConstraint(['computertypeid'], ['computertypes.computertypeid'], ),
|
||||
sa.ForeignKeyConstraint(['osid'], ['operatingsystems.osid'], ),
|
||||
sa.PrimaryKeyConstraint('computerid')
|
||||
)
|
||||
with op.batch_alter_table('computers', schema=None) as batch_op:
|
||||
batch_op.create_index('idx_computer_hostname', ['hostname'], unique=False)
|
||||
batch_op.create_index('idx_computer_os', ['osid'], unique=False)
|
||||
batch_op.create_index('idx_computer_type', ['computertypeid'], unique=False)
|
||||
batch_op.create_index(batch_op.f('ix_computers_assetid'), ['assetid'], unique=True)
|
||||
batch_op.create_index(batch_op.f('ix_computers_hostname'), ['hostname'], unique=False)
|
||||
|
||||
op.create_table('equipment',
|
||||
sa.Column('equipmentid', sa.Integer(), nullable=False),
|
||||
sa.Column('assetid', sa.Integer(), nullable=False),
|
||||
sa.Column('equipmenttypeid', sa.Integer(), nullable=True),
|
||||
sa.Column('vendorid', sa.Integer(), nullable=True),
|
||||
sa.Column('modelnumberid', sa.Integer(), nullable=True),
|
||||
sa.Column('requiresmanualconfig', sa.Boolean(), nullable=True, comment='Multi-PC machine needs manual configuration'),
|
||||
sa.Column('islocationonly', sa.Boolean(), nullable=True, comment='Virtual location marker (not actual equipment)'),
|
||||
sa.Column('lastmaintenancedate', sa.DateTime(), nullable=True),
|
||||
sa.Column('nextmaintenancedate', sa.DateTime(), nullable=True),
|
||||
sa.Column('maintenanceintervaldays', sa.Integer(), nullable=True),
|
||||
sa.Column('controllervendorid', sa.Integer(), nullable=True, comment='Controller vendor (e.g., FANUC)'),
|
||||
sa.Column('controllermodelid', sa.Integer(), nullable=True, comment='Controller model (e.g., 31B)'),
|
||||
sa.Column('createddate', sa.DateTime(), nullable=False),
|
||||
sa.Column('modifieddate', sa.DateTime(), nullable=False),
|
||||
sa.Column('isactive', sa.Boolean(), nullable=False),
|
||||
sa.ForeignKeyConstraint(['assetid'], ['assets.assetid'], ondelete='CASCADE'),
|
||||
sa.ForeignKeyConstraint(['controllermodelid'], ['models.modelnumberid'], ),
|
||||
sa.ForeignKeyConstraint(['controllervendorid'], ['vendors.vendorid'], ),
|
||||
sa.ForeignKeyConstraint(['equipmenttypeid'], ['equipmenttypes.equipmenttypeid'], ),
|
||||
sa.ForeignKeyConstraint(['modelnumberid'], ['models.modelnumberid'], ),
|
||||
sa.ForeignKeyConstraint(['vendorid'], ['vendors.vendorid'], ),
|
||||
sa.PrimaryKeyConstraint('equipmentid')
|
||||
)
|
||||
with op.batch_alter_table('equipment', schema=None) as batch_op:
|
||||
batch_op.create_index('idx_equipment_type', ['equipmenttypeid'], unique=False)
|
||||
batch_op.create_index('idx_equipment_vendor', ['vendorid'], unique=False)
|
||||
batch_op.create_index(batch_op.f('ix_equipment_assetid'), ['assetid'], unique=True)
|
||||
|
||||
op.create_table('machines',
|
||||
sa.Column('machineid', sa.Integer(), nullable=False),
|
||||
sa.Column('machinenumber', sa.String(length=50), nullable=False, comment='Business identifier (e.g., CMM01, G5QX1GT3ESF)'),
|
||||
sa.Column('alias', sa.String(length=100), nullable=True, comment='Friendly name'),
|
||||
sa.Column('hostname', sa.String(length=100), nullable=True, comment='Network hostname (for PCs)'),
|
||||
sa.Column('serialnumber', sa.String(length=100), nullable=True, comment='Hardware serial number'),
|
||||
sa.Column('machinetypeid', sa.Integer(), nullable=False),
|
||||
sa.Column('pctypeid', sa.Integer(), nullable=True, comment='Set for PCs, NULL for equipment'),
|
||||
sa.Column('businessunitid', sa.Integer(), nullable=True),
|
||||
sa.Column('modelnumberid', sa.Integer(), nullable=True),
|
||||
sa.Column('vendorid', sa.Integer(), nullable=True),
|
||||
sa.Column('statusid', sa.Integer(), nullable=True, comment='In Use, Spare, Retired, etc.'),
|
||||
sa.Column('locationid', sa.Integer(), nullable=True),
|
||||
sa.Column('mapleft', sa.Integer(), nullable=True, comment='X coordinate on floor map'),
|
||||
sa.Column('maptop', sa.Integer(), nullable=True, comment='Y coordinate on floor map'),
|
||||
sa.Column('islocationonly', sa.Boolean(), nullable=True, comment='Virtual location marker (not actual machine)'),
|
||||
sa.Column('osid', sa.Integer(), nullable=True),
|
||||
sa.Column('loggedinuser', sa.String(length=100), nullable=True),
|
||||
sa.Column('lastreporteddate', sa.DateTime(), nullable=True),
|
||||
sa.Column('lastboottime', sa.DateTime(), nullable=True),
|
||||
sa.Column('isvnc', sa.Boolean(), nullable=True, comment='VNC remote access enabled'),
|
||||
sa.Column('iswinrm', sa.Boolean(), nullable=True, comment='WinRM enabled'),
|
||||
sa.Column('isshopfloor', sa.Boolean(), nullable=True, comment='Shopfloor PC'),
|
||||
sa.Column('requiresmanualconfig', sa.Boolean(), nullable=True, comment='Multi-PC machine needs manual configuration'),
|
||||
sa.Column('notes', sa.Text(), nullable=True),
|
||||
sa.Column('createddate', sa.DateTime(), nullable=False),
|
||||
sa.Column('modifieddate', sa.DateTime(), nullable=False),
|
||||
sa.Column('isactive', sa.Boolean(), nullable=False),
|
||||
sa.Column('deleteddate', sa.DateTime(), nullable=True),
|
||||
sa.Column('deletedby', sa.String(length=100), nullable=True),
|
||||
sa.Column('createdby', sa.String(length=100), nullable=True),
|
||||
sa.Column('modifiedby', sa.String(length=100), nullable=True),
|
||||
sa.ForeignKeyConstraint(['businessunitid'], ['businessunits.businessunitid'], ),
|
||||
sa.ForeignKeyConstraint(['locationid'], ['locations.locationid'], ),
|
||||
sa.ForeignKeyConstraint(['machinetypeid'], ['machinetypes.machinetypeid'], ),
|
||||
sa.ForeignKeyConstraint(['modelnumberid'], ['models.modelnumberid'], ),
|
||||
sa.ForeignKeyConstraint(['osid'], ['operatingsystems.osid'], ),
|
||||
sa.ForeignKeyConstraint(['pctypeid'], ['pctypes.pctypeid'], ),
|
||||
sa.ForeignKeyConstraint(['statusid'], ['machinestatuses.statusid'], ),
|
||||
sa.ForeignKeyConstraint(['vendorid'], ['vendors.vendorid'], ),
|
||||
sa.PrimaryKeyConstraint('machineid')
|
||||
)
|
||||
with op.batch_alter_table('machines', schema=None) as batch_op:
|
||||
batch_op.create_index('idx_machine_active', ['isactive'], unique=False)
|
||||
batch_op.create_index('idx_machine_hostname', ['hostname'], unique=False)
|
||||
batch_op.create_index('idx_machine_location', ['locationid'], unique=False)
|
||||
batch_op.create_index('idx_machine_type_bu', ['machinetypeid', 'businessunitid'], unique=False)
|
||||
batch_op.create_index(batch_op.f('ix_machines_hostname'), ['hostname'], unique=False)
|
||||
batch_op.create_index(batch_op.f('ix_machines_machinenumber'), ['machinenumber'], unique=True)
|
||||
batch_op.create_index(batch_op.f('ix_machines_serialnumber'), ['serialnumber'], unique=False)
|
||||
|
||||
op.create_table('networkdevices',
|
||||
sa.Column('networkdeviceid', sa.Integer(), nullable=False),
|
||||
sa.Column('assetid', sa.Integer(), nullable=False),
|
||||
sa.Column('networkdevicetypeid', sa.Integer(), nullable=True),
|
||||
sa.Column('vendorid', sa.Integer(), nullable=True),
|
||||
sa.Column('hostname', sa.String(length=100), nullable=True, comment='Network hostname'),
|
||||
sa.Column('firmwareversion', sa.String(length=100), nullable=True),
|
||||
sa.Column('portcount', sa.Integer(), nullable=True, comment='Number of ports (for switches)'),
|
||||
sa.Column('ispoe', sa.Boolean(), nullable=True, comment='Power over Ethernet capable'),
|
||||
sa.Column('ismanaged', sa.Boolean(), nullable=True, comment='Managed device (SNMP, web interface, etc.)'),
|
||||
sa.Column('rackunit', sa.String(length=20), nullable=True, comment='Rack unit position (e.g., U1, U5)'),
|
||||
sa.Column('createddate', sa.DateTime(), nullable=False),
|
||||
sa.Column('modifieddate', sa.DateTime(), nullable=False),
|
||||
sa.Column('isactive', sa.Boolean(), nullable=False),
|
||||
sa.ForeignKeyConstraint(['assetid'], ['assets.assetid'], ondelete='CASCADE'),
|
||||
sa.ForeignKeyConstraint(['networkdevicetypeid'], ['networkdevicetypes.networkdevicetypeid'], ),
|
||||
sa.ForeignKeyConstraint(['vendorid'], ['vendors.vendorid'], ),
|
||||
sa.PrimaryKeyConstraint('networkdeviceid')
|
||||
)
|
||||
with op.batch_alter_table('networkdevices', schema=None) as batch_op:
|
||||
batch_op.create_index('idx_netdev_hostname', ['hostname'], unique=False)
|
||||
batch_op.create_index('idx_netdev_type', ['networkdevicetypeid'], unique=False)
|
||||
batch_op.create_index('idx_netdev_vendor', ['vendorid'], unique=False)
|
||||
batch_op.create_index(batch_op.f('ix_networkdevices_assetid'), ['assetid'], unique=True)
|
||||
batch_op.create_index(batch_op.f('ix_networkdevices_hostname'), ['hostname'], unique=False)
|
||||
|
||||
op.create_table('printers',
|
||||
sa.Column('printerid', sa.Integer(), nullable=False),
|
||||
sa.Column('assetid', sa.Integer(), nullable=False),
|
||||
sa.Column('printertypeid', sa.Integer(), nullable=True),
|
||||
sa.Column('vendorid', sa.Integer(), nullable=True),
|
||||
sa.Column('modelnumberid', sa.Integer(), nullable=True),
|
||||
sa.Column('hostname', sa.String(length=100), nullable=True, comment='Network hostname'),
|
||||
sa.Column('windowsname', sa.String(length=255), nullable=True, comment='Windows printer name (e.g., \\\\server\\printer)'),
|
||||
sa.Column('sharename', sa.String(length=100), nullable=True, comment='CSF/share name'),
|
||||
sa.Column('iscsf', sa.Boolean(), nullable=True, comment='Is CSF printer'),
|
||||
sa.Column('installpath', sa.String(length=255), nullable=True, comment='Driver install path'),
|
||||
sa.Column('pin', sa.String(length=20), nullable=True),
|
||||
sa.Column('iscolor', sa.Boolean(), nullable=True, comment='Color capable'),
|
||||
sa.Column('isduplex', sa.Boolean(), nullable=True, comment='Duplex capable'),
|
||||
sa.Column('isnetwork', sa.Boolean(), nullable=True, comment='Network connected'),
|
||||
sa.Column('createddate', sa.DateTime(), nullable=False),
|
||||
sa.Column('modifieddate', sa.DateTime(), nullable=False),
|
||||
sa.Column('isactive', sa.Boolean(), nullable=False),
|
||||
sa.ForeignKeyConstraint(['assetid'], ['assets.assetid'], ondelete='CASCADE'),
|
||||
sa.ForeignKeyConstraint(['modelnumberid'], ['models.modelnumberid'], ),
|
||||
sa.ForeignKeyConstraint(['printertypeid'], ['printertypes.printertypeid'], ),
|
||||
sa.ForeignKeyConstraint(['vendorid'], ['vendors.vendorid'], ),
|
||||
sa.PrimaryKeyConstraint('printerid')
|
||||
)
|
||||
with op.batch_alter_table('printers', schema=None) as batch_op:
|
||||
batch_op.create_index('idx_printer_hostname', ['hostname'], unique=False)
|
||||
batch_op.create_index('idx_printer_type', ['printertypeid'], unique=False)
|
||||
batch_op.create_index('idx_printer_windowsname', ['windowsname'], unique=False)
|
||||
batch_op.create_index(batch_op.f('ix_printers_assetid'), ['assetid'], unique=True)
|
||||
batch_op.create_index(batch_op.f('ix_printers_hostname'), ['hostname'], unique=False)
|
||||
|
||||
op.create_table('usbcheckouts',
|
||||
sa.Column('checkoutid', sa.Integer(), nullable=False),
|
||||
sa.Column('usbdeviceid', sa.Integer(), nullable=True),
|
||||
sa.Column('machineid', sa.Integer(), nullable=False),
|
||||
sa.Column('sso', sa.String(length=20), nullable=False, comment='SSO of user'),
|
||||
sa.Column('checkoutname', sa.String(length=100), nullable=True, comment='Name of user'),
|
||||
sa.Column('checkouttime', sa.DateTime(), nullable=False),
|
||||
sa.Column('checkintime', sa.DateTime(), nullable=True),
|
||||
sa.Column('checkoutreason', sa.Text(), nullable=True, comment='Reason for checkout'),
|
||||
sa.Column('checkinnotes', sa.Text(), nullable=True),
|
||||
sa.Column('waswiped', sa.Boolean(), nullable=True, comment='Was device wiped after return'),
|
||||
sa.Column('createddate', sa.DateTime(), nullable=False),
|
||||
sa.Column('modifieddate', sa.DateTime(), nullable=False),
|
||||
sa.Column('isactive', sa.Boolean(), nullable=False),
|
||||
sa.ForeignKeyConstraint(['usbdeviceid'], ['usbdevices.usbdeviceid'], ondelete='CASCADE'),
|
||||
sa.PrimaryKeyConstraint('checkoutid')
|
||||
)
|
||||
op.create_table('appversions',
|
||||
sa.Column('appversionid', sa.Integer(), nullable=False),
|
||||
sa.Column('appid', sa.Integer(), nullable=False),
|
||||
sa.Column('version', sa.String(length=50), nullable=False),
|
||||
sa.Column('releasedate', sa.Date(), nullable=True),
|
||||
sa.Column('notes', sa.String(length=255), nullable=True),
|
||||
sa.Column('dateadded', sa.DateTime(), nullable=True),
|
||||
sa.Column('isactive', sa.Boolean(), nullable=True),
|
||||
sa.ForeignKeyConstraint(['appid'], ['applications.appid'], ),
|
||||
sa.PrimaryKeyConstraint('appversionid'),
|
||||
sa.UniqueConstraint('appid', 'version', name='uq_app_version')
|
||||
)
|
||||
op.create_table('communications',
|
||||
sa.Column('communicationid', sa.Integer(), nullable=False),
|
||||
sa.Column('assetid', sa.Integer(), nullable=True, comment='FK to assets table (new architecture)'),
|
||||
sa.Column('machineid', sa.Integer(), nullable=True, comment='DEPRECATED: FK to machines table - use assetid instead'),
|
||||
sa.Column('comtypeid', sa.Integer(), nullable=False),
|
||||
sa.Column('ipaddress', sa.String(length=50), nullable=True),
|
||||
sa.Column('subnetmask', sa.String(length=50), nullable=True),
|
||||
sa.Column('gateway', sa.String(length=50), nullable=True),
|
||||
sa.Column('dns1', sa.String(length=50), nullable=True),
|
||||
sa.Column('dns2', sa.String(length=50), nullable=True),
|
||||
sa.Column('macaddress', sa.String(length=50), nullable=True),
|
||||
sa.Column('isdhcp', sa.Boolean(), nullable=True),
|
||||
sa.Column('comport', sa.String(length=20), nullable=True),
|
||||
sa.Column('baudrate', sa.Integer(), nullable=True),
|
||||
sa.Column('databits', sa.Integer(), nullable=True),
|
||||
sa.Column('stopbits', sa.String(length=10), nullable=True),
|
||||
sa.Column('parity', sa.String(length=20), nullable=True),
|
||||
sa.Column('flowcontrol', sa.String(length=20), nullable=True),
|
||||
sa.Column('port', sa.Integer(), nullable=True),
|
||||
sa.Column('username', sa.String(length=100), nullable=True),
|
||||
sa.Column('pathname', sa.String(length=255), nullable=True),
|
||||
sa.Column('pathname2', sa.String(length=255), nullable=True, comment='Secondary path for dualpath'),
|
||||
sa.Column('isprimary', sa.Boolean(), nullable=True, comment='Primary communication method'),
|
||||
sa.Column('ismachinenetwork', sa.Boolean(), nullable=True, comment='On machine network vs office network'),
|
||||
sa.Column('notes', sa.Text(), nullable=True),
|
||||
sa.Column('createddate', sa.DateTime(), nullable=False),
|
||||
sa.Column('modifieddate', sa.DateTime(), nullable=False),
|
||||
sa.Column('isactive', sa.Boolean(), nullable=False),
|
||||
sa.ForeignKeyConstraint(['assetid'], ['assets.assetid'], ),
|
||||
sa.ForeignKeyConstraint(['comtypeid'], ['communicationtypes.comtypeid'], ),
|
||||
sa.ForeignKeyConstraint(['machineid'], ['machines.machineid'], ),
|
||||
sa.PrimaryKeyConstraint('communicationid')
|
||||
)
|
||||
with op.batch_alter_table('communications', schema=None) as batch_op:
|
||||
batch_op.create_index('idx_comm_asset', ['assetid'], unique=False)
|
||||
batch_op.create_index('idx_comm_ip', ['ipaddress'], unique=False)
|
||||
batch_op.create_index('idx_comm_machine', ['machineid'], unique=False)
|
||||
batch_op.create_index(batch_op.f('ix_communications_assetid'), ['assetid'], unique=False)
|
||||
|
||||
op.create_table('knowledgebase',
|
||||
sa.Column('linkid', sa.Integer(), nullable=False),
|
||||
sa.Column('appid', sa.Integer(), nullable=True),
|
||||
sa.Column('shortdescription', sa.String(length=500), nullable=False),
|
||||
sa.Column('linkurl', sa.String(length=2000), nullable=True),
|
||||
sa.Column('keywords', sa.String(length=500), nullable=True),
|
||||
sa.Column('clicks', sa.Integer(), nullable=True),
|
||||
sa.Column('lastupdated', sa.DateTime(), nullable=True),
|
||||
sa.Column('createddate', sa.DateTime(), nullable=False),
|
||||
sa.Column('modifieddate', sa.DateTime(), nullable=False),
|
||||
sa.Column('isactive', sa.Boolean(), nullable=False),
|
||||
sa.ForeignKeyConstraint(['appid'], ['applications.appid'], ),
|
||||
sa.PrimaryKeyConstraint('linkid')
|
||||
)
|
||||
op.create_table('machinerelationships',
|
||||
sa.Column('relationshipid', sa.Integer(), nullable=False),
|
||||
sa.Column('parentmachineid', sa.Integer(), nullable=False),
|
||||
sa.Column('childmachineid', sa.Integer(), nullable=False),
|
||||
sa.Column('relationshiptypeid', sa.Integer(), nullable=False),
|
||||
sa.Column('notes', sa.Text(), nullable=True),
|
||||
sa.Column('createddate', sa.DateTime(), nullable=False),
|
||||
sa.Column('modifieddate', sa.DateTime(), nullable=False),
|
||||
sa.Column('isactive', sa.Boolean(), nullable=False),
|
||||
sa.ForeignKeyConstraint(['childmachineid'], ['machines.machineid'], ),
|
||||
sa.ForeignKeyConstraint(['parentmachineid'], ['machines.machineid'], ),
|
||||
sa.ForeignKeyConstraint(['relationshiptypeid'], ['relationshiptypes.relationshiptypeid'], ),
|
||||
sa.PrimaryKeyConstraint('relationshipid'),
|
||||
sa.UniqueConstraint('parentmachineid', 'childmachineid', 'relationshiptypeid', name='uq_machine_relationship')
|
||||
)
|
||||
op.create_table('printerdata',
|
||||
sa.Column('id', sa.Integer(), nullable=False),
|
||||
sa.Column('machineid', sa.Integer(), nullable=False),
|
||||
sa.Column('windowsname', sa.String(length=255), nullable=True, comment='Windows printer name (e.g., \\\\server\\printer)'),
|
||||
sa.Column('sharename', sa.String(length=100), nullable=True, comment='CSF/share name'),
|
||||
sa.Column('iscsf', sa.Boolean(), nullable=True, comment='Is CSF printer'),
|
||||
sa.Column('installpath', sa.String(length=255), nullable=True, comment='Driver install path'),
|
||||
sa.Column('pin', sa.String(length=20), nullable=True),
|
||||
sa.Column('createddate', sa.DateTime(), nullable=False),
|
||||
sa.Column('modifieddate', sa.DateTime(), nullable=False),
|
||||
sa.Column('isactive', sa.Boolean(), nullable=False),
|
||||
sa.ForeignKeyConstraint(['machineid'], ['machines.machineid'], ondelete='CASCADE'),
|
||||
sa.PrimaryKeyConstraint('id')
|
||||
)
|
||||
with op.batch_alter_table('printerdata', schema=None) as batch_op:
|
||||
batch_op.create_index('idx_printerdata_windowsname', ['windowsname'], unique=False)
|
||||
batch_op.create_index(batch_op.f('ix_printerdata_machineid'), ['machineid'], unique=True)
|
||||
|
||||
op.create_table('computerinstalledapps',
|
||||
sa.Column('id', sa.Integer(), nullable=False),
|
||||
sa.Column('computerid', sa.Integer(), nullable=False),
|
||||
sa.Column('appid', sa.Integer(), nullable=False),
|
||||
sa.Column('appversionid', sa.Integer(), nullable=True),
|
||||
sa.Column('isactive', sa.Boolean(), nullable=False),
|
||||
sa.Column('installeddate', sa.DateTime(), nullable=True),
|
||||
sa.ForeignKeyConstraint(['appid'], ['applications.appid'], ),
|
||||
sa.ForeignKeyConstraint(['appversionid'], ['appversions.appversionid'], ),
|
||||
sa.ForeignKeyConstraint(['computerid'], ['computers.computerid'], ondelete='CASCADE'),
|
||||
sa.PrimaryKeyConstraint('id'),
|
||||
sa.UniqueConstraint('computerid', 'appid', name='uq_computer_app')
|
||||
)
|
||||
with op.batch_alter_table('computerinstalledapps', schema=None) as batch_op:
|
||||
batch_op.create_index('idx_compapp_app', ['appid'], unique=False)
|
||||
batch_op.create_index('idx_compapp_computer', ['computerid'], unique=False)
|
||||
|
||||
op.create_table('installedapps',
|
||||
sa.Column('id', sa.Integer(), nullable=False),
|
||||
sa.Column('machineid', sa.Integer(), nullable=False),
|
||||
sa.Column('appid', sa.Integer(), nullable=False),
|
||||
sa.Column('appversionid', sa.Integer(), nullable=True),
|
||||
sa.Column('isactive', sa.Boolean(), nullable=False),
|
||||
sa.Column('installeddate', sa.DateTime(), nullable=True),
|
||||
sa.ForeignKeyConstraint(['appid'], ['applications.appid'], ),
|
||||
sa.ForeignKeyConstraint(['appversionid'], ['appversions.appversionid'], ),
|
||||
sa.ForeignKeyConstraint(['machineid'], ['machines.machineid'], ),
|
||||
sa.PrimaryKeyConstraint('id'),
|
||||
sa.UniqueConstraint('machineid', 'appid', name='uq_machine_app')
|
||||
)
|
||||
# ### end Alembic commands ###
|
||||
|
||||
|
||||
def downgrade():
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.drop_table('installedapps')
|
||||
with op.batch_alter_table('computerinstalledapps', schema=None) as batch_op:
|
||||
batch_op.drop_index('idx_compapp_computer')
|
||||
batch_op.drop_index('idx_compapp_app')
|
||||
|
||||
op.drop_table('computerinstalledapps')
|
||||
with op.batch_alter_table('printerdata', schema=None) as batch_op:
|
||||
batch_op.drop_index(batch_op.f('ix_printerdata_machineid'))
|
||||
batch_op.drop_index('idx_printerdata_windowsname')
|
||||
|
||||
op.drop_table('printerdata')
|
||||
op.drop_table('machinerelationships')
|
||||
op.drop_table('knowledgebase')
|
||||
with op.batch_alter_table('communications', schema=None) as batch_op:
|
||||
batch_op.drop_index(batch_op.f('ix_communications_assetid'))
|
||||
batch_op.drop_index('idx_comm_machine')
|
||||
batch_op.drop_index('idx_comm_ip')
|
||||
batch_op.drop_index('idx_comm_asset')
|
||||
|
||||
op.drop_table('communications')
|
||||
op.drop_table('appversions')
|
||||
op.drop_table('usbcheckouts')
|
||||
with op.batch_alter_table('printers', schema=None) as batch_op:
|
||||
batch_op.drop_index(batch_op.f('ix_printers_hostname'))
|
||||
batch_op.drop_index(batch_op.f('ix_printers_assetid'))
|
||||
batch_op.drop_index('idx_printer_windowsname')
|
||||
batch_op.drop_index('idx_printer_type')
|
||||
batch_op.drop_index('idx_printer_hostname')
|
||||
|
||||
op.drop_table('printers')
|
||||
with op.batch_alter_table('networkdevices', schema=None) as batch_op:
|
||||
batch_op.drop_index(batch_op.f('ix_networkdevices_hostname'))
|
||||
batch_op.drop_index(batch_op.f('ix_networkdevices_assetid'))
|
||||
batch_op.drop_index('idx_netdev_vendor')
|
||||
batch_op.drop_index('idx_netdev_type')
|
||||
batch_op.drop_index('idx_netdev_hostname')
|
||||
|
||||
op.drop_table('networkdevices')
|
||||
with op.batch_alter_table('machines', schema=None) as batch_op:
|
||||
batch_op.drop_index(batch_op.f('ix_machines_serialnumber'))
|
||||
batch_op.drop_index(batch_op.f('ix_machines_machinenumber'))
|
||||
batch_op.drop_index(batch_op.f('ix_machines_hostname'))
|
||||
batch_op.drop_index('idx_machine_type_bu')
|
||||
batch_op.drop_index('idx_machine_location')
|
||||
batch_op.drop_index('idx_machine_hostname')
|
||||
batch_op.drop_index('idx_machine_active')
|
||||
|
||||
op.drop_table('machines')
|
||||
with op.batch_alter_table('equipment', schema=None) as batch_op:
|
||||
batch_op.drop_index(batch_op.f('ix_equipment_assetid'))
|
||||
batch_op.drop_index('idx_equipment_vendor')
|
||||
batch_op.drop_index('idx_equipment_type')
|
||||
|
||||
op.drop_table('equipment')
|
||||
with op.batch_alter_table('computers', schema=None) as batch_op:
|
||||
batch_op.drop_index(batch_op.f('ix_computers_hostname'))
|
||||
batch_op.drop_index(batch_op.f('ix_computers_assetid'))
|
||||
batch_op.drop_index('idx_computer_type')
|
||||
batch_op.drop_index('idx_computer_os')
|
||||
batch_op.drop_index('idx_computer_hostname')
|
||||
|
||||
op.drop_table('computers')
|
||||
with op.batch_alter_table('assetrelationships', schema=None) as batch_op:
|
||||
batch_op.drop_index('idx_asset_rel_target')
|
||||
batch_op.drop_index('idx_asset_rel_source')
|
||||
|
||||
op.drop_table('assetrelationships')
|
||||
op.drop_table('applications')
|
||||
op.drop_table('userroles')
|
||||
with op.batch_alter_table('usbdevices', schema=None) as batch_op:
|
||||
batch_op.drop_index('idx_usb_type')
|
||||
batch_op.drop_index('idx_usb_serial')
|
||||
batch_op.drop_index('idx_usb_currentuser')
|
||||
batch_op.drop_index('idx_usb_checkedout')
|
||||
|
||||
op.drop_table('usbdevices')
|
||||
op.drop_table('supportteams')
|
||||
with op.batch_alter_table('subnets', schema=None) as batch_op:
|
||||
batch_op.drop_index('idx_subnet_vlan')
|
||||
batch_op.drop_index('idx_subnet_location')
|
||||
batch_op.drop_index('idx_subnet_cidr')
|
||||
|
||||
op.drop_table('subnets')
|
||||
op.drop_table('rolepermissions')
|
||||
op.drop_table('notifications')
|
||||
op.drop_table('models')
|
||||
with op.batch_alter_table('auditlogs', schema=None) as batch_op:
|
||||
batch_op.drop_index(batch_op.f('ix_auditlogs_timestamp'))
|
||||
batch_op.drop_index(batch_op.f('ix_auditlogs_entitytype'))
|
||||
batch_op.drop_index(batch_op.f('ix_auditlogs_action'))
|
||||
|
||||
op.drop_table('auditlogs')
|
||||
with op.batch_alter_table('assets', schema=None) as batch_op:
|
||||
batch_op.drop_index(batch_op.f('ix_assets_serialnumber'))
|
||||
batch_op.drop_index(batch_op.f('ix_assets_assetnumber'))
|
||||
batch_op.drop_index('idx_asset_type_bu')
|
||||
batch_op.drop_index('idx_asset_status')
|
||||
batch_op.drop_index('idx_asset_location')
|
||||
batch_op.drop_index('idx_asset_active')
|
||||
|
||||
op.drop_table('assets')
|
||||
with op.batch_alter_table('vlans', schema=None) as batch_op:
|
||||
batch_op.drop_index('idx_vlan_number')
|
||||
|
||||
op.drop_table('vlans')
|
||||
op.drop_table('vendors')
|
||||
with op.batch_alter_table('users', schema=None) as batch_op:
|
||||
batch_op.drop_index(batch_op.f('ix_users_username'))
|
||||
|
||||
op.drop_table('users')
|
||||
op.drop_table('usbdevicetypes')
|
||||
with op.batch_alter_table('settings', schema=None) as batch_op:
|
||||
batch_op.drop_index(batch_op.f('ix_settings_key'))
|
||||
|
||||
op.drop_table('settings')
|
||||
op.drop_table('roles')
|
||||
op.drop_table('relationshiptypes')
|
||||
op.drop_table('printertypes')
|
||||
op.drop_table('permissions')
|
||||
op.drop_table('pctypes')
|
||||
op.drop_table('operatingsystems')
|
||||
op.drop_table('notificationtypes')
|
||||
op.drop_table('networkdevicetypes')
|
||||
op.drop_table('machinetypes')
|
||||
op.drop_table('machinestatuses')
|
||||
op.drop_table('locations')
|
||||
op.drop_table('equipmenttypes')
|
||||
op.drop_table('computertypes')
|
||||
op.drop_table('communicationtypes')
|
||||
op.drop_table('businessunits')
|
||||
op.drop_table('assettypes')
|
||||
op.drop_table('assetstatuses')
|
||||
op.drop_table('appowners')
|
||||
# ### end Alembic commands ###
|
||||
147
migrations/versions/7a01_adr001_position_contract.py
Normal file
147
migrations/versions/7a01_adr001_position_contract.py
Normal file
@@ -0,0 +1,147 @@
|
||||
"""ADR-001 position contract surface
|
||||
|
||||
Rename Asset.mapleft -> Asset.mapx, Asset.maptop -> Asset.mapy. Add
|
||||
Location.mapx / Location.mapy. Add AssetRelationship.label,
|
||||
AssetRelationship.inheritsposition, RelationshipType.propagatesthroughid.
|
||||
Seed the three canonical relationship types (partof, controls, connectedto)
|
||||
with controls.propagatesthroughid -> partof.
|
||||
|
||||
Revision ID: 7a01_adr001_position
|
||||
Revises: 68b3947ae14f
|
||||
Create Date: 2026-05-30
|
||||
|
||||
"""
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
|
||||
revision = '7a01_adr001_position'
|
||||
down_revision = '68b3947ae14f'
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade():
|
||||
# ---- Asset.mapleft/maptop -> mapx/mapy --------------------------------
|
||||
with op.batch_alter_table('assets') as batch_op:
|
||||
batch_op.alter_column('mapleft', new_column_name='mapx',
|
||||
existing_type=sa.Integer(), existing_nullable=True,
|
||||
comment='X coordinate on floor map (ADR-001)')
|
||||
batch_op.alter_column('maptop', new_column_name='mapy',
|
||||
existing_type=sa.Integer(), existing_nullable=True,
|
||||
comment='Y coordinate on floor map (ADR-001)')
|
||||
|
||||
# ---- Location: add mapx/mapy for the fallback path --------------------
|
||||
with op.batch_alter_table('locations') as batch_op:
|
||||
batch_op.add_column(sa.Column('mapx', sa.Integer(), nullable=True,
|
||||
comment='Default X coordinate for assets at this location'))
|
||||
batch_op.add_column(sa.Column('mapy', sa.Integer(), nullable=True,
|
||||
comment='Default Y coordinate for assets at this location'))
|
||||
|
||||
# ---- RelationshipType: add propagatesthroughid self-FK ----------------
|
||||
with op.batch_alter_table('relationshiptypes') as batch_op:
|
||||
batch_op.add_column(sa.Column('propagatesthroughid', sa.Integer(),
|
||||
nullable=True,
|
||||
comment='Sibling-propagation rail per ADR-001'))
|
||||
batch_op.create_foreign_key(
|
||||
'fk_relationshiptype_propagation',
|
||||
'relationshiptypes',
|
||||
['propagatesthroughid'],
|
||||
['relationshiptypeid'],
|
||||
)
|
||||
|
||||
# ---- AssetRelationship: add label + inheritsposition ------------------
|
||||
with op.batch_alter_table('assetrelationships') as batch_op:
|
||||
batch_op.add_column(sa.Column('label', sa.String(length=200), nullable=True,
|
||||
comment='Free-text relationship description (ADR-001)'))
|
||||
batch_op.add_column(sa.Column('inheritsposition', sa.Boolean(),
|
||||
nullable=False, server_default='1',
|
||||
comment='If true, resolved-position walk follows this edge (ADR-001)'))
|
||||
|
||||
# ---- Seed three canonical relationship types --------------------------
|
||||
# Idempotent: insert only if name is not already present. Then update
|
||||
# controls.propagatesthroughid to point at partof.
|
||||
relationshiptypes = sa.table(
|
||||
'relationshiptypes',
|
||||
sa.column('relationshiptype', sa.String),
|
||||
sa.column('description', sa.Text),
|
||||
sa.column('propagatesthroughid', sa.Integer),
|
||||
sa.column('isactive', sa.Boolean),
|
||||
)
|
||||
|
||||
conn = op.get_bind()
|
||||
dialect_name = conn.dialect.name
|
||||
|
||||
# MySQL default collation is case-insensitive, which makes a plain
|
||||
# `WHERE relationshiptype = 'controls'` match a legacy `Controls` row
|
||||
# and silently skip the insert (then accidentally wire propagation onto
|
||||
# the wrong row). Force binary comparison on MySQL so the three ADR
|
||||
# types are always distinct from legacy capitalized names. SQLite +
|
||||
# PostgreSQL are case-sensitive by default; the plain comparison is
|
||||
# safe there.
|
||||
def _eq(col_expr, value):
|
||||
if dialect_name == 'mysql':
|
||||
return f"BINARY {col_expr} = '{value}'"
|
||||
return f"{col_expr} = '{value}'"
|
||||
|
||||
for rt in (
|
||||
('partof', 'Composition / sub-assembly (ADR-001)'),
|
||||
('controls', 'Operational authority over another asset (ADR-001)'),
|
||||
('connectedto', 'Network or data link without authority (ADR-001)'),
|
||||
):
|
||||
existing = conn.execute(sa.text(
|
||||
f"SELECT relationshiptypeid FROM relationshiptypes WHERE {_eq('relationshiptype', rt[0])}"
|
||||
)).first()
|
||||
if not existing:
|
||||
conn.execute(relationshiptypes.insert().values(
|
||||
relationshiptype=rt[0],
|
||||
description=rt[1],
|
||||
propagatesthroughid=None,
|
||||
isactive=True,
|
||||
))
|
||||
|
||||
partof_row = conn.execute(sa.text(
|
||||
f"SELECT relationshiptypeid FROM relationshiptypes WHERE {_eq('relationshiptype', 'partof')}"
|
||||
)).first()
|
||||
if partof_row:
|
||||
conn.execute(sa.text(
|
||||
f"UPDATE relationshiptypes SET propagatesthroughid = :p WHERE {_eq('relationshiptype', 'controls')}"
|
||||
), {'p': partof_row[0]})
|
||||
|
||||
|
||||
def downgrade():
|
||||
# Revert in reverse order. Drops the seeded ADR types if no rows
|
||||
# reference them; otherwise leaves them in place to avoid FK violations.
|
||||
conn = op.get_bind()
|
||||
|
||||
with op.batch_alter_table('assetrelationships') as batch_op:
|
||||
batch_op.drop_column('inheritsposition')
|
||||
batch_op.drop_column('label')
|
||||
|
||||
with op.batch_alter_table('relationshiptypes') as batch_op:
|
||||
batch_op.drop_constraint('fk_relationshiptype_propagation', type_='foreignkey')
|
||||
batch_op.drop_column('propagatesthroughid')
|
||||
|
||||
with op.batch_alter_table('locations') as batch_op:
|
||||
batch_op.drop_column('mapy')
|
||||
batch_op.drop_column('mapx')
|
||||
|
||||
with op.batch_alter_table('assets') as batch_op:
|
||||
batch_op.alter_column('mapx', new_column_name='mapleft',
|
||||
existing_type=sa.Integer(), existing_nullable=True)
|
||||
batch_op.alter_column('mapy', new_column_name='maptop',
|
||||
existing_type=sa.Integer(), existing_nullable=True)
|
||||
|
||||
# Best-effort: drop the seeded types if nothing references them.
|
||||
for name in ('connectedto', 'controls', 'partof'):
|
||||
try:
|
||||
conn.execute(sa.text(
|
||||
"DELETE FROM relationshiptypes "
|
||||
"WHERE relationshiptype = :n "
|
||||
"AND relationshiptypeid NOT IN ("
|
||||
" SELECT relationshiptypeid FROM assetrelationships "
|
||||
" UNION SELECT relationshiptypeid FROM machinerelationships"
|
||||
")"
|
||||
), {'n': name})
|
||||
except Exception:
|
||||
pass
|
||||
44
migrations/versions/7b02_gaugelabreference.py
Normal file
44
migrations/versions/7b02_gaugelabreference.py
Normal file
@@ -0,0 +1,44 @@
|
||||
"""Add Asset.gaugelabreference and Asset.maintenancereference
|
||||
|
||||
Adds dedicated external-reference columns to assets, distinct from
|
||||
assetnumber. Equipment commonly carries an authoritative gauge lab reference
|
||||
and a maintenance system reference; assetnumber stays the generic asset tag
|
||||
and name the layperson label.
|
||||
|
||||
Revision ID: 7b02_gaugelabref
|
||||
Revises: 7a01_adr001_position
|
||||
Create Date: 2026-06-25
|
||||
|
||||
"""
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
|
||||
revision = '7b02_gaugelabref'
|
||||
down_revision = '7a01_adr001_position'
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade():
|
||||
with op.batch_alter_table('assets') as batch_op:
|
||||
batch_op.add_column(sa.Column(
|
||||
'gaugelabreference', sa.String(length=50), nullable=True,
|
||||
comment='Gauge lab asset reference (authoritative tag the gauge '
|
||||
'lab assigns to equipment); distinct from assetnumber'))
|
||||
batch_op.add_column(sa.Column(
|
||||
'maintenancereference', sa.String(length=50), nullable=True,
|
||||
comment='Maintenance system asset reference; distinct from '
|
||||
'assetnumber'))
|
||||
batch_op.create_index('ix_assets_gaugelabreference',
|
||||
['gaugelabreference'])
|
||||
batch_op.create_index('ix_assets_maintenancereference',
|
||||
['maintenancereference'])
|
||||
|
||||
|
||||
def downgrade():
|
||||
with op.batch_alter_table('assets') as batch_op:
|
||||
batch_op.drop_index('ix_assets_maintenancereference')
|
||||
batch_op.drop_index('ix_assets_gaugelabreference')
|
||||
batch_op.drop_column('maintenancereference')
|
||||
batch_op.drop_column('gaugelabreference')
|
||||
47
migrations/versions/7c01_drop_legacy_machine.py
Normal file
47
migrations/versions/7c01_drop_legacy_machine.py
Normal file
@@ -0,0 +1,47 @@
|
||||
"""Drop the legacy Machine instance layer
|
||||
|
||||
Retires the Machine model (ADR-001): the asset/computer model is now the
|
||||
single source of truth. Drops machines + its PC/status lookups + the legacy
|
||||
relationship and installed-app tables + the legacy printer extension, and
|
||||
removes the deprecated communications.machineid column. machinetypes is kept
|
||||
(still referenced by models.machinetypeid).
|
||||
|
||||
Idempotent (IF EXISTS) so it is safe even though the live drop was applied
|
||||
directly during the cutover.
|
||||
|
||||
Revision ID: 7c01_drop_legacy_machine
|
||||
Revises: 7b02_gaugelabref
|
||||
Create Date: 2026-06-26
|
||||
|
||||
"""
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
|
||||
revision = '7c01_drop_legacy_machine'
|
||||
down_revision = '7b02_gaugelabref'
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
_TABLES = ['printerdata', 'installedapps', 'machinerelationships',
|
||||
'machines', 'pctypes', 'machinestatuses']
|
||||
|
||||
|
||||
def upgrade():
|
||||
bind = op.get_bind()
|
||||
bind.exec_driver_sql("SET FOREIGN_KEY_CHECKS=0")
|
||||
insp = sa.inspect(bind)
|
||||
if 'machineid' in [c['name'] for c in insp.get_columns('communications')]:
|
||||
for fk in insp.get_foreign_keys('communications'):
|
||||
if 'machineid' in fk['constrained_columns'] and fk.get('name'):
|
||||
bind.exec_driver_sql(
|
||||
f"ALTER TABLE communications DROP FOREIGN KEY {fk['name']}")
|
||||
op.drop_column('communications', 'machineid')
|
||||
for t in _TABLES:
|
||||
bind.exec_driver_sql(f"DROP TABLE IF EXISTS {t}")
|
||||
bind.exec_driver_sql("SET FOREIGN_KEY_CHECKS=1")
|
||||
|
||||
|
||||
def downgrade():
|
||||
# The Machine layer is retired; recreating it is out of scope.
|
||||
raise NotImplementedError("Legacy Machine layer cannot be restored")
|
||||
29
migrations/versions/7c02_application_isrequired.py
Normal file
29
migrations/versions/7c02_application_isrequired.py
Normal file
@@ -0,0 +1,29 @@
|
||||
"""Add applications.isrequired
|
||||
|
||||
Backs the software-compliance report, which compares required applications
|
||||
against what is installed per PC. There was no such flag before, so the report
|
||||
errored.
|
||||
|
||||
Revision ID: 7c02_app_isrequired
|
||||
Revises: 7c01_drop_legacy_machine
|
||||
Create Date: 2026-06-26
|
||||
|
||||
"""
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
|
||||
revision = '7c02_app_isrequired'
|
||||
down_revision = '7c01_drop_legacy_machine'
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade():
|
||||
op.add_column('applications', sa.Column(
|
||||
'isrequired', sa.Boolean(), nullable=True, server_default=sa.false(),
|
||||
comment='Required on all PCs (drives the software-compliance report)'))
|
||||
|
||||
|
||||
def downgrade():
|
||||
op.drop_column('applications', 'isrequired')
|
||||
56
migrations/versions/7c03_locationtypes.py
Normal file
56
migrations/versions/7c03_locationtypes.py
Normal file
@@ -0,0 +1,56 @@
|
||||
"""Add locationtypes + locations tree (ADR-001)
|
||||
|
||||
Creates the locationtypes lookup and extends locations with locationtypeid +
|
||||
parentlocationid (self-FK), so sites can classify locations and build a
|
||||
location tree (sections, cells, sub-cells, operations, etc.). Seeds the
|
||||
canonical location types.
|
||||
|
||||
Revision ID: 7c03_locationtypes
|
||||
Revises: 7c02_app_isrequired
|
||||
Create Date: 2026-06-26
|
||||
|
||||
"""
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
|
||||
revision = '7c03_locationtypes'
|
||||
down_revision = '7c02_app_isrequired'
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
_TYPES = ['section', 'cell', 'subcell', 'operation', 'meetingroom', 'lab',
|
||||
'office', 'storage', 'hallway', 'networkcloset', 'building']
|
||||
|
||||
|
||||
def upgrade():
|
||||
op.create_table(
|
||||
'locationtypes',
|
||||
sa.Column('locationtypeid', sa.Integer(), primary_key=True),
|
||||
sa.Column('locationtype', sa.String(length=50), nullable=False, unique=True),
|
||||
sa.Column('description', sa.Text(), nullable=True),
|
||||
sa.Column('createddate', sa.DateTime(), nullable=True),
|
||||
sa.Column('modifieddate', sa.DateTime(), nullable=True),
|
||||
sa.Column('isactive', sa.Boolean(), nullable=True),
|
||||
)
|
||||
with op.batch_alter_table('locations') as batch_op:
|
||||
batch_op.add_column(sa.Column('locationtypeid', sa.Integer(), nullable=True))
|
||||
batch_op.add_column(sa.Column('parentlocationid', sa.Integer(), nullable=True))
|
||||
batch_op.create_foreign_key('fk_locations_locationtype', 'locationtypes',
|
||||
['locationtypeid'], ['locationtypeid'])
|
||||
batch_op.create_foreign_key('fk_locations_parent', 'locations',
|
||||
['parentlocationid'], ['locationid'])
|
||||
|
||||
lt = sa.table('locationtypes',
|
||||
sa.column('locationtype', sa.String),
|
||||
sa.column('isactive', sa.Boolean))
|
||||
op.bulk_insert(lt, [{'locationtype': t, 'isactive': True} for t in _TYPES])
|
||||
|
||||
|
||||
def downgrade():
|
||||
with op.batch_alter_table('locations') as batch_op:
|
||||
batch_op.drop_constraint('fk_locations_parent', type_='foreignkey')
|
||||
batch_op.drop_constraint('fk_locations_locationtype', type_='foreignkey')
|
||||
batch_op.drop_column('parentlocationid')
|
||||
batch_op.drop_column('locationtypeid')
|
||||
op.drop_table('locationtypes')
|
||||
85
migrations/versions/7c04_fold_plugin_schema.py
Normal file
85
migrations/versions/7c04_fold_plugin_schema.py
Normal file
@@ -0,0 +1,85 @@
|
||||
"""Fold remaining plugin schema into the core chain
|
||||
|
||||
Deploys run `flask db upgrade` (core chain) only, so the core chain must
|
||||
reproduce the full bundled schema. The core baseline already creates the
|
||||
plugin tables as of baseline time; this adds the post-baseline additions that
|
||||
previously lived only in per-plugin migrations (which deploys don't run):
|
||||
|
||||
- modelsupplies (printer model -> toner/drum/waste part numbers)
|
||||
- computers.vendorid + computers.modelnumberid (PC make/model)
|
||||
- computerinstalledapps.installedversion (collector version string)
|
||||
|
||||
Idempotent: skips anything already present, so it is a no-op on the live DB
|
||||
(which already has these) and creates them on a fresh deploy.
|
||||
|
||||
Revision ID: 7c04_fold_plugin_schema
|
||||
Revises: 7c03_locationtypes
|
||||
Create Date: 2026-06-26
|
||||
|
||||
"""
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
|
||||
revision = '7c04_fold_plugin_schema'
|
||||
down_revision = '7c03_locationtypes'
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade():
|
||||
bind = op.get_bind()
|
||||
insp = sa.inspect(bind)
|
||||
tables = set(insp.get_table_names())
|
||||
|
||||
if 'modelsupplies' not in tables:
|
||||
op.create_table(
|
||||
'modelsupplies',
|
||||
sa.Column('modelsupplyid', sa.Integer(), primary_key=True),
|
||||
sa.Column('modelnumberid', sa.Integer(),
|
||||
sa.ForeignKey('models.modelnumberid'), nullable=False),
|
||||
sa.Column('supplytype', sa.String(length=20), nullable=False,
|
||||
server_default='toner'),
|
||||
sa.Column('color', sa.String(length=20), nullable=False,
|
||||
server_default='none'),
|
||||
sa.Column('capacitytier', sa.String(length=20), nullable=False,
|
||||
server_default='standard'),
|
||||
sa.Column('partnumber', sa.String(length=50), nullable=False),
|
||||
sa.Column('marketingname', sa.String(length=120), nullable=True),
|
||||
sa.Column('pageyield', sa.Integer(), nullable=True),
|
||||
sa.Column('notes', sa.Text(), nullable=True),
|
||||
sa.Column('createddate', sa.DateTime(), nullable=True),
|
||||
sa.Column('modifieddate', sa.DateTime(), nullable=True),
|
||||
sa.Column('isactive', sa.Boolean(), nullable=True),
|
||||
sa.UniqueConstraint('modelnumberid', 'partnumber',
|
||||
name='uq_modelsupply_part'),
|
||||
)
|
||||
op.create_index('idx_modelsupplies_modelnumberid', 'modelsupplies',
|
||||
['modelnumberid'])
|
||||
|
||||
comp_cols = {c['name'] for c in insp.get_columns('computers')}
|
||||
with op.batch_alter_table('computers') as batch_op:
|
||||
if 'vendorid' not in comp_cols:
|
||||
batch_op.add_column(sa.Column('vendorid', sa.Integer(), nullable=True))
|
||||
batch_op.create_foreign_key('fk_computers_vendor', 'vendors',
|
||||
['vendorid'], ['vendorid'])
|
||||
if 'modelnumberid' not in comp_cols:
|
||||
batch_op.add_column(sa.Column('modelnumberid', sa.Integer(), nullable=True))
|
||||
batch_op.create_foreign_key('fk_computers_model', 'models',
|
||||
['modelnumberid'], ['modelnumberid'])
|
||||
|
||||
cia_cols = {c['name'] for c in insp.get_columns('computerinstalledapps')}
|
||||
if 'installedversion' not in cia_cols:
|
||||
op.add_column('computerinstalledapps',
|
||||
sa.Column('installedversion', sa.String(length=100),
|
||||
nullable=True))
|
||||
|
||||
|
||||
def downgrade():
|
||||
op.drop_table('modelsupplies')
|
||||
with op.batch_alter_table('computers') as batch_op:
|
||||
batch_op.drop_constraint('fk_computers_model', type_='foreignkey')
|
||||
batch_op.drop_constraint('fk_computers_vendor', type_='foreignkey')
|
||||
batch_op.drop_column('modelnumberid')
|
||||
batch_op.drop_column('vendorid')
|
||||
op.drop_column('computerinstalledapps', 'installedversion')
|
||||
37
migrations/versions/7d01_dashboarddefaults.py
Normal file
37
migrations/versions/7d01_dashboarddefaults.py
Normal file
@@ -0,0 +1,37 @@
|
||||
"""Add dashboarddefaults (visitor-IP -> business-unit mapping)
|
||||
|
||||
A shopfloor / lobby kiosk resolves which business unit to display from the
|
||||
display PC's IP. Powers the visitor-location lookup.
|
||||
|
||||
Revision ID: 7d01_dashboarddefaults
|
||||
Revises: 7c04_fold_plugin_schema
|
||||
Create Date: 2026-06-27
|
||||
|
||||
"""
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
|
||||
revision = '7d01_dashboarddefaults'
|
||||
down_revision = '7c04_fold_plugin_schema'
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade():
|
||||
op.create_table(
|
||||
'dashboarddefaults',
|
||||
sa.Column('dashboarddefaultid', sa.Integer(), primary_key=True),
|
||||
sa.Column('ipaddress', sa.String(length=50), nullable=False, unique=True),
|
||||
sa.Column('businessunitid', sa.Integer(), nullable=False),
|
||||
sa.Column('description', sa.String(length=255), nullable=True),
|
||||
sa.Column('createddate', sa.DateTime(), nullable=True),
|
||||
sa.Column('modifieddate', sa.DateTime(), nullable=True),
|
||||
sa.Column('isactive', sa.Boolean(), nullable=True),
|
||||
sa.ForeignKeyConstraint(['businessunitid'], ['businessunits.businessunitid'],
|
||||
name='fk_dashboarddefaults_businessunit'),
|
||||
)
|
||||
|
||||
|
||||
def downgrade():
|
||||
op.drop_table('dashboarddefaults')
|
||||
37
migrations/versions/7d02_widen_notification_employee_cols.py
Normal file
37
migrations/versions/7d02_widen_notification_employee_cols.py
Normal file
@@ -0,0 +1,37 @@
|
||||
"""Widen notifications.employeesso / employeename to TEXT
|
||||
|
||||
Recognition and recertification notifications comma-join every listed
|
||||
employee's SSO and name into a single column. VARCHAR(100) truncated the list
|
||||
at ~11 people, dropping names off the shopfloor grid. TEXT removes the cap.
|
||||
|
||||
Revision ID: 7d02_widen_notification_employee_cols
|
||||
Revises: 7d01_dashboarddefaults
|
||||
Create Date: 2026-07-07
|
||||
|
||||
"""
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
|
||||
revision = '7d02_widen_notification_employee_cols'
|
||||
down_revision = '7d01_dashboarddefaults'
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade():
|
||||
op.alter_column('notifications', 'employeesso',
|
||||
existing_type=sa.String(length=100), type_=sa.Text(),
|
||||
existing_nullable=True)
|
||||
op.alter_column('notifications', 'employeename',
|
||||
existing_type=sa.String(length=100), type_=sa.Text(),
|
||||
existing_nullable=True)
|
||||
|
||||
|
||||
def downgrade():
|
||||
op.alter_column('notifications', 'employeesso',
|
||||
existing_type=sa.Text(), type_=sa.String(length=100),
|
||||
existing_nullable=True)
|
||||
op.alter_column('notifications', 'employeename',
|
||||
existing_type=sa.Text(), type_=sa.String(length=100),
|
||||
existing_nullable=True)
|
||||
53
migrations/versions/7d03_notificationtype_expiry.py
Normal file
53
migrations/versions/7d03_notificationtype_expiry.py
Normal file
@@ -0,0 +1,53 @@
|
||||
"""Per-type auto-expiry rule on notificationtypes
|
||||
|
||||
Makes the shopfloor auto-expiry window configurable per notification type
|
||||
instead of hardcoding recognition (8 AM Eastern) and recertification (14 days)
|
||||
in the API. Adds:
|
||||
expirymode 'none' | 'duration' | 'dailytime'
|
||||
expirydays days for 'duration'
|
||||
expiryhour hour (Eastern) for 'dailytime'
|
||||
expiryminute minute (Eastern) for 'dailytime'
|
||||
|
||||
Seeds the two existing rule-bearing types so behavior is unchanged:
|
||||
Recognition (typecolor 'recognition') -> dailytime 08:00 Eastern
|
||||
Recertification (typecolor 'recertification') -> duration 14 days
|
||||
|
||||
Revision ID: 7d03_notificationtype_expiry
|
||||
Revises: 7d02_widen_notification_employee_cols
|
||||
Create Date: 2026-07-08
|
||||
|
||||
"""
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
|
||||
revision = '7d03_notificationtype_expiry'
|
||||
down_revision = '7d02_widen_notification_employee_cols'
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade():
|
||||
op.add_column('notificationtypes', sa.Column('expirymode', sa.String(length=20), nullable=True, server_default='none'))
|
||||
op.add_column('notificationtypes', sa.Column('expirydays', sa.Integer(), nullable=True))
|
||||
op.add_column('notificationtypes', sa.Column('expiryhour', sa.SmallInteger(), nullable=True))
|
||||
op.add_column('notificationtypes', sa.Column('expiryminute', sa.SmallInteger(), nullable=True, server_default='0'))
|
||||
|
||||
# preserve current behavior for the two rule-bearing types
|
||||
op.execute(
|
||||
"UPDATE notificationtypes SET expirymode='dailytime', expiryhour=8, expiryminute=0 "
|
||||
"WHERE typecolor='recognition'"
|
||||
)
|
||||
op.execute(
|
||||
"UPDATE notificationtypes SET expirymode='duration', expirydays=14 "
|
||||
"WHERE typecolor='recertification'"
|
||||
)
|
||||
# everything else: explicit 'none' (indefinite unless the creator sets an end time)
|
||||
op.execute("UPDATE notificationtypes SET expirymode='none' WHERE expirymode IS NULL")
|
||||
|
||||
|
||||
def downgrade():
|
||||
op.drop_column('notificationtypes', 'expiryminute')
|
||||
op.drop_column('notificationtypes', 'expiryhour')
|
||||
op.drop_column('notificationtypes', 'expirydays')
|
||||
op.drop_column('notificationtypes', 'expirymode')
|
||||
43
migrations/versions/7d04_notificationtype_display.py
Normal file
43
migrations/versions/7d04_notificationtype_display.py
Normal file
@@ -0,0 +1,43 @@
|
||||
"""Add data-driven shopfloor display behavior to notification types
|
||||
|
||||
Replaces the hardcoded recognition/training/recertification logic with per-type
|
||||
columns: split one card per employee, show employee photo, and the display
|
||||
style (standard rows / carousel / grid / banner). Seeds the built-in special
|
||||
types by their typecolor keyword.
|
||||
|
||||
Revision ID: 7d04_notificationtype_display
|
||||
Revises: 7d03_notificationtype_expiry
|
||||
Create Date: 2026-07-08
|
||||
|
||||
"""
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
|
||||
revision = '7d04_notificationtype_display'
|
||||
down_revision = '7d03_notificationtype_expiry'
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade():
|
||||
op.add_column('notificationtypes', sa.Column('splitperemployee', sa.Boolean(), nullable=True, server_default='0'))
|
||||
op.add_column('notificationtypes', sa.Column('showemployeephoto', sa.Boolean(), nullable=True, server_default='0'))
|
||||
op.add_column('notificationtypes', sa.Column('displaystyle', sa.String(length=20), nullable=True, server_default='standard'))
|
||||
|
||||
conn = op.get_bind()
|
||||
conn.execute(sa.text(
|
||||
"UPDATE notificationtypes SET splitperemployee=1, showemployeephoto=1, displaystyle='carousel' "
|
||||
"WHERE typecolor='recognition'"))
|
||||
conn.execute(sa.text(
|
||||
"UPDATE notificationtypes SET splitperemployee=1, showemployeephoto=1, displaystyle='grid' "
|
||||
"WHERE typecolor='recertification'"))
|
||||
conn.execute(sa.text(
|
||||
"UPDATE notificationtypes SET splitperemployee=1, showemployeephoto=1, displaystyle='carousel' "
|
||||
"WHERE typecolor='training'"))
|
||||
|
||||
|
||||
def downgrade():
|
||||
op.drop_column('notificationtypes', 'displaystyle')
|
||||
op.drop_column('notificationtypes', 'showemployeephoto')
|
||||
op.drop_column('notificationtypes', 'splitperemployee')
|
||||
76
migrations/versions/7d05_pc_access_protocols.py
Normal file
76
migrations/versions/7d05_pc_access_protocols.py
Normal file
@@ -0,0 +1,76 @@
|
||||
"""PC access protocols: catalog + per-PC links, retire isvnc/iswinrm
|
||||
|
||||
Adds an admin-managed protocol catalog (accessprotocols) and a per-PC link
|
||||
table (computeraccess). Seeds VNC/WinRM/RDP, migrates each PC's isvnc/iswinrm
|
||||
into computeraccess rows, then drops the two boolean columns.
|
||||
|
||||
Revision ID: 7d05_pc_access_protocols
|
||||
Revises: 7d04_notificationtype_display
|
||||
Create Date: 2026-07-08
|
||||
|
||||
"""
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
|
||||
revision = '7d05_pc_access_protocols'
|
||||
down_revision = '7d04_notificationtype_display'
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade():
|
||||
op.create_table(
|
||||
'accessprotocols',
|
||||
sa.Column('protocolid', sa.Integer(), primary_key=True),
|
||||
sa.Column('name', sa.String(length=50), nullable=False),
|
||||
sa.Column('scheme', sa.String(length=20), nullable=False),
|
||||
sa.Column('defaultport', sa.Integer(), nullable=True),
|
||||
sa.Column('linktemplate', sa.String(length=255), nullable=False),
|
||||
sa.Column('isactive', sa.Boolean(), nullable=False, server_default='1'),
|
||||
sa.UniqueConstraint('name', name='uq_accessprotocol_name'),
|
||||
)
|
||||
op.create_table(
|
||||
'computeraccess',
|
||||
sa.Column('id', sa.Integer(), primary_key=True),
|
||||
sa.Column('computerid', sa.Integer(), nullable=False),
|
||||
sa.Column('protocolid', sa.Integer(), nullable=False),
|
||||
sa.Column('portoverride', sa.Integer(), nullable=True),
|
||||
sa.Column('isactive', sa.Boolean(), nullable=False, server_default='1'),
|
||||
sa.ForeignKeyConstraint(['computerid'], ['computers.computerid'], ondelete='CASCADE'),
|
||||
sa.ForeignKeyConstraint(['protocolid'], ['accessprotocols.protocolid']),
|
||||
sa.UniqueConstraint('computerid', 'protocolid', name='uq_computer_protocol'),
|
||||
)
|
||||
op.create_index('idx_compaccess_computer', 'computeraccess', ['computerid'])
|
||||
|
||||
conn = op.get_bind()
|
||||
# Seed the protocol catalog. exec_driver_sql so the ':' / '{}' in the
|
||||
# templates are not parsed as bind params.
|
||||
conn.exec_driver_sql(
|
||||
"INSERT INTO accessprotocols (name, scheme, defaultport, linktemplate, isactive) VALUES "
|
||||
"('VNC','vnc',5900,'vnc://{host}:{port}',1),"
|
||||
"('WinRM','https',5986,'https://{host}:{port}/wsman',1),"
|
||||
"('RDP','rdp',3389,'rdp://{host}:{port}',1)"
|
||||
)
|
||||
# Migrate the old booleans into per-PC access rows.
|
||||
conn.exec_driver_sql(
|
||||
"INSERT INTO computeraccess (computerid, protocolid, isactive) "
|
||||
"SELECT c.computerid, p.protocolid, 1 FROM computers c "
|
||||
"JOIN accessprotocols p ON p.name='VNC' WHERE c.isvnc = 1"
|
||||
)
|
||||
conn.exec_driver_sql(
|
||||
"INSERT INTO computeraccess (computerid, protocolid, isactive) "
|
||||
"SELECT c.computerid, p.protocolid, 1 FROM computers c "
|
||||
"JOIN accessprotocols p ON p.name='WinRM' WHERE c.iswinrm = 1"
|
||||
)
|
||||
|
||||
op.drop_column('computers', 'isvnc')
|
||||
op.drop_column('computers', 'iswinrm')
|
||||
|
||||
|
||||
def downgrade():
|
||||
op.add_column('computers', sa.Column('iswinrm', sa.Boolean(), nullable=True))
|
||||
op.add_column('computers', sa.Column('isvnc', sa.Boolean(), nullable=True))
|
||||
op.drop_index('idx_compaccess_computer', table_name='computeraccess')
|
||||
op.drop_table('computeraccess')
|
||||
op.drop_table('accessprotocols')
|
||||
46
migrations/versions/7d06_typecolor_to_hex.py
Normal file
46
migrations/versions/7d06_typecolor_to_hex.py
Normal file
@@ -0,0 +1,46 @@
|
||||
"""Give the built-in notification types real hex colors
|
||||
|
||||
Recognition/Recertification/Training stored keyword typecolors ('recognition'
|
||||
etc.) that static color maps had to translate. Every other type already stores
|
||||
a hex. This converts the three to hex so color is fully data-driven and the
|
||||
static maps can go away. Their special behavior now rides on displaystyle /
|
||||
splitperemployee / showemployeephoto, not the color keyword.
|
||||
|
||||
Revision ID: 7d06_typecolor_to_hex
|
||||
Revises: 7d05_pc_access_protocols
|
||||
Create Date: 2026-07-08
|
||||
|
||||
"""
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
|
||||
revision = '7d06_typecolor_to_hex'
|
||||
down_revision = '7d05_pc_access_protocols'
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
KEYWORD_HEX = {
|
||||
'recognition': '#ffc107',
|
||||
'recertification': '#0d6efd',
|
||||
'training': '#17a2b8',
|
||||
}
|
||||
|
||||
|
||||
def upgrade():
|
||||
conn = op.get_bind()
|
||||
for keyword, hexcolor in KEYWORD_HEX.items():
|
||||
conn.execute(
|
||||
sa.text("UPDATE notificationtypes SET typecolor = :hex WHERE typecolor = :kw"),
|
||||
{'hex': hexcolor, 'kw': keyword}
|
||||
)
|
||||
|
||||
|
||||
def downgrade():
|
||||
conn = op.get_bind()
|
||||
for keyword, hexcolor in KEYWORD_HEX.items():
|
||||
conn.execute(
|
||||
sa.text("UPDATE notificationtypes SET typecolor = :kw WHERE typecolor = :hex"),
|
||||
{'kw': keyword, 'hex': hexcolor}
|
||||
)
|
||||
42
migrations/versions/7d07_assettype_color.py
Normal file
42
migrations/versions/7d07_assettype_color.py
Normal file
@@ -0,0 +1,42 @@
|
||||
"""Add color to asset types (data-driven map/type colors)
|
||||
|
||||
Gives AssetType a color column (like AssetStatus) so the map's top-level type
|
||||
colors come from data instead of the hardcoded map in mapColors.js. Seeds the
|
||||
four base types with the exact colors that were hardcoded, so nothing changes
|
||||
visually until someone edits them.
|
||||
|
||||
Revision ID: 7d07_assettype_color
|
||||
Revises: 7d06_typecolor_to_hex
|
||||
Create Date: 2026-07-08
|
||||
|
||||
"""
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
|
||||
revision = '7d07_assettype_color'
|
||||
down_revision = '7d06_typecolor_to_hex'
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
SEED = {
|
||||
'equipment': '#F44336',
|
||||
'computer': '#2196F3',
|
||||
'printer': '#4CAF50',
|
||||
'network_device': '#FF9800',
|
||||
}
|
||||
|
||||
|
||||
def upgrade():
|
||||
op.add_column('assettypes', sa.Column('color', sa.String(length=20), nullable=True))
|
||||
conn = op.get_bind()
|
||||
for assettype, hexcolor in SEED.items():
|
||||
conn.execute(
|
||||
sa.text("UPDATE assettypes SET color = :c WHERE assettype = :t AND (color IS NULL OR color = '')"),
|
||||
{'c': hexcolor, 't': assettype}
|
||||
)
|
||||
|
||||
|
||||
def downgrade():
|
||||
op.drop_column('assettypes', 'color')
|
||||
36
migrations/versions/7d08_tvslides.py
Normal file
36
migrations/versions/7d08_tvslides.py
Normal file
@@ -0,0 +1,36 @@
|
||||
"""Slide manager: tvslides table
|
||||
|
||||
Backing table for the slides plugin (lobby display + shopfloor screensaver
|
||||
playlists). Image files live on disk; this holds order + per-slide duration.
|
||||
|
||||
Revision ID: 7d08_tvslides
|
||||
Revises: 7d07_assettype_color
|
||||
Create Date: 2026-07-08
|
||||
|
||||
"""
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
|
||||
revision = '7d08_tvslides'
|
||||
down_revision = '7d07_assettype_color'
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade():
|
||||
op.create_table(
|
||||
'tvslides',
|
||||
sa.Column('slideid', sa.Integer(), primary_key=True),
|
||||
sa.Column('surface', sa.String(length=20), nullable=False),
|
||||
sa.Column('filename', sa.String(length=255), nullable=False),
|
||||
sa.Column('sortorder', sa.Integer(), nullable=False, server_default='0'),
|
||||
sa.Column('seconds', sa.Integer(), nullable=False, server_default='0'),
|
||||
sa.Column('uploadeddate', sa.DateTime(), nullable=True),
|
||||
)
|
||||
op.create_index('idx_tvslide_surface', 'tvslides', ['surface'])
|
||||
|
||||
|
||||
def downgrade():
|
||||
op.drop_index('idx_tvslide_surface', table_name='tvslides')
|
||||
op.drop_table('tvslides')
|
||||
31
migrations/versions/7d09_subtype_colors.py
Normal file
31
migrations/versions/7d09_subtype_colors.py
Normal file
@@ -0,0 +1,31 @@
|
||||
"""Add color to asset subtype tables (data-driven map colors)
|
||||
|
||||
The map colors equipment/computer/network/printer subtypes round-robin from a
|
||||
palette. Give each subtype table a color column so a site can set stable, chosen
|
||||
colors that the map reads instead of the auto-assigned palette.
|
||||
|
||||
Revision ID: 7d09_subtype_colors
|
||||
Revises: 7d08_tvslides
|
||||
Create Date: 2026-07-09
|
||||
|
||||
"""
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
|
||||
revision = '7d09_subtype_colors'
|
||||
down_revision = '7d08_tvslides'
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
TABLES = ['equipmenttypes', 'computertypes', 'networkdevicetypes', 'printertypes']
|
||||
|
||||
|
||||
def upgrade():
|
||||
for table in TABLES:
|
||||
op.add_column(table, sa.Column('color', sa.String(length=20), nullable=True))
|
||||
|
||||
|
||||
def downgrade():
|
||||
for table in TABLES:
|
||||
op.drop_column(table, 'color')
|
||||
26
migrations/versions/7d10_relationshiptype_color.py
Normal file
26
migrations/versions/7d10_relationshiptype_color.py
Normal file
@@ -0,0 +1,26 @@
|
||||
"""Add color to relationship types
|
||||
|
||||
Lets sites color relationship types (Controls, Contains, Stored At...) so the
|
||||
asset relationship graph/badges are visually distinct, like statuses.
|
||||
|
||||
Revision ID: 7d10_relationshiptype_color
|
||||
Revises: 7d09_subtype_colors
|
||||
Create Date: 2026-07-09
|
||||
|
||||
"""
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
|
||||
revision = '7d10_relationshiptype_color'
|
||||
down_revision = '7d09_subtype_colors'
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade():
|
||||
op.add_column('relationshiptypes', sa.Column('color', sa.String(length=20), nullable=True))
|
||||
|
||||
|
||||
def downgrade():
|
||||
op.drop_column('relationshiptypes', 'color')
|
||||
23
migrations/versions/7d11_locationtype_color.py
Normal file
23
migrations/versions/7d11_locationtype_color.py
Normal file
@@ -0,0 +1,23 @@
|
||||
"""Add color to location types
|
||||
|
||||
Revision ID: 7d11_locationtype_color
|
||||
Revises: 7d10_relationshiptype_color
|
||||
Create Date: 2026-07-09
|
||||
|
||||
"""
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
|
||||
revision = '7d11_locationtype_color'
|
||||
down_revision = '7d10_relationshiptype_color'
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade():
|
||||
op.add_column('locationtypes', sa.Column('color', sa.String(length=20), nullable=True))
|
||||
|
||||
|
||||
def downgrade():
|
||||
op.drop_column('locationtypes', 'color')
|
||||
26
migrations/versions/7d12_drop_computer_isshopfloor.py
Normal file
26
migrations/versions/7d12_drop_computer_isshopfloor.py
Normal file
@@ -0,0 +1,26 @@
|
||||
"""Drop computers.isshopfloor (redundant with the Shopfloor PC type)
|
||||
|
||||
The flag was never populated (0 rows) and duplicated the 'Shopfloor' computer
|
||||
type. Shopfloor classification now lives entirely in computertype.
|
||||
|
||||
Revision ID: 7d12_drop_isshopfloor
|
||||
Revises: 7d11_locationtype_color
|
||||
Create Date: 2026-07-09
|
||||
|
||||
"""
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
|
||||
revision = '7d12_drop_isshopfloor'
|
||||
down_revision = '7d11_locationtype_color'
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade():
|
||||
op.drop_column('computers', 'isshopfloor')
|
||||
|
||||
|
||||
def downgrade():
|
||||
op.add_column('computers', sa.Column('isshopfloor', sa.Boolean(), nullable=True, server_default='0'))
|
||||
32
migrations/versions/7d13_printerdrivers.py
Normal file
32
migrations/versions/7d13_printerdrivers.py
Normal file
@@ -0,0 +1,32 @@
|
||||
"""Printer drivers: named SMB/HTTP links to driver packages
|
||||
|
||||
Revision ID: 7d13_printerdrivers
|
||||
Revises: 7d12_drop_isshopfloor
|
||||
Create Date: 2026-07-09
|
||||
|
||||
"""
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
|
||||
revision = '7d13_printerdrivers'
|
||||
down_revision = '7d12_drop_isshopfloor'
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade():
|
||||
op.create_table(
|
||||
'printerdrivers',
|
||||
sa.Column('driverid', sa.Integer(), primary_key=True),
|
||||
sa.Column('name', sa.String(length=150), nullable=False),
|
||||
sa.Column('location', sa.String(length=500), nullable=False),
|
||||
sa.Column('description', sa.Text(), nullable=True),
|
||||
sa.Column('modelnumberid', sa.Integer(), nullable=True),
|
||||
sa.Column('isactive', sa.Boolean(), nullable=False, server_default='1'),
|
||||
sa.ForeignKeyConstraint(['modelnumberid'], ['models.modelnumberid']),
|
||||
)
|
||||
|
||||
|
||||
def downgrade():
|
||||
op.drop_table('printerdrivers')
|
||||
48
migrations/versions/7d14_customfields.py
Normal file
48
migrations/versions/7d14_customfields.py
Normal file
@@ -0,0 +1,48 @@
|
||||
"""Custom fields: site-defined extra attributes per asset type
|
||||
|
||||
Revision ID: 7d14_customfields
|
||||
Revises: 7d13_printerdrivers
|
||||
Create Date: 2026-07-09
|
||||
|
||||
"""
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
|
||||
revision = '7d14_customfields'
|
||||
down_revision = '7d13_printerdrivers'
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade():
|
||||
op.create_table(
|
||||
'customfields',
|
||||
sa.Column('fieldid', sa.Integer(), primary_key=True),
|
||||
sa.Column('assettypeid', sa.Integer(), nullable=False),
|
||||
sa.Column('fieldkey', sa.String(length=50), nullable=False),
|
||||
sa.Column('label', sa.String(length=150), nullable=False),
|
||||
sa.Column('datatype', sa.String(length=20), nullable=False, server_default='text'),
|
||||
sa.Column('options', sa.Text(), nullable=True),
|
||||
sa.Column('showondetail', sa.Boolean(), nullable=False, server_default='1'),
|
||||
sa.Column('showonform', sa.Boolean(), nullable=False, server_default='1'),
|
||||
sa.Column('sortorder', sa.Integer(), nullable=False, server_default='0'),
|
||||
sa.Column('isactive', sa.Boolean(), nullable=False, server_default='1'),
|
||||
sa.ForeignKeyConstraint(['assettypeid'], ['assettypes.assettypeid']),
|
||||
sa.UniqueConstraint('assettypeid', 'fieldkey', name='uq_customfield_type_key'),
|
||||
)
|
||||
op.create_table(
|
||||
'customfieldvalues',
|
||||
sa.Column('valueid', sa.Integer(), primary_key=True),
|
||||
sa.Column('fieldid', sa.Integer(), nullable=False),
|
||||
sa.Column('assetid', sa.Integer(), nullable=False),
|
||||
sa.Column('value', sa.Text(), nullable=True),
|
||||
sa.ForeignKeyConstraint(['fieldid'], ['customfields.fieldid'], ondelete='CASCADE'),
|
||||
sa.ForeignKeyConstraint(['assetid'], ['assets.assetid'], ondelete='CASCADE'),
|
||||
sa.UniqueConstraint('fieldid', 'assetid', name='uq_customfieldvalue_field_asset'),
|
||||
)
|
||||
|
||||
|
||||
def downgrade():
|
||||
op.drop_table('customfieldvalues')
|
||||
op.drop_table('customfields')
|
||||
45
migrations/versions/7d15_warranties.py
Normal file
45
migrations/versions/7d15_warranties.py
Normal file
@@ -0,0 +1,45 @@
|
||||
"""Warranty plugin: warranties + warrantyassets
|
||||
|
||||
Revision ID: 7d15_warranties
|
||||
Revises: 7d14_customfields
|
||||
Create Date: 2026-07-09
|
||||
|
||||
"""
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
|
||||
revision = '7d15_warranties'
|
||||
down_revision = '7d14_customfields'
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade():
|
||||
op.create_table(
|
||||
'warranties',
|
||||
sa.Column('warrantyid', sa.Integer(), primary_key=True),
|
||||
sa.Column('vendor', sa.String(length=100), nullable=False),
|
||||
sa.Column('servicetag', sa.String(length=100), nullable=True),
|
||||
sa.Column('provider', sa.String(length=20), nullable=False, server_default='manual'),
|
||||
sa.Column('servicelevel', sa.String(length=150), nullable=True),
|
||||
sa.Column('startdate', sa.Date(), nullable=True),
|
||||
sa.Column('enddate', sa.Date(), nullable=True),
|
||||
sa.Column('lastcheckeddate', sa.DateTime(), nullable=True),
|
||||
sa.Column('notes', sa.Text(), nullable=True),
|
||||
sa.Column('isactive', sa.Boolean(), nullable=False, server_default='1'),
|
||||
)
|
||||
op.create_table(
|
||||
'warrantyassets',
|
||||
sa.Column('id', sa.Integer(), primary_key=True),
|
||||
sa.Column('warrantyid', sa.Integer(), nullable=False),
|
||||
sa.Column('assetid', sa.Integer(), nullable=False),
|
||||
sa.ForeignKeyConstraint(['warrantyid'], ['warranties.warrantyid'], ondelete='CASCADE'),
|
||||
sa.ForeignKeyConstraint(['assetid'], ['assets.assetid'], ondelete='CASCADE'),
|
||||
sa.UniqueConstraint('warrantyid', 'assetid', name='uq_warrantyasset_warranty_asset'),
|
||||
)
|
||||
|
||||
|
||||
def downgrade():
|
||||
op.drop_table('warrantyassets')
|
||||
op.drop_table('warranties')
|
||||
31
migrations/versions/7d16_directoryemployees.py
Normal file
31
migrations/versions/7d16_directoryemployees.py
Normal file
@@ -0,0 +1,31 @@
|
||||
"""Self-hosted employee directory table
|
||||
|
||||
Revision ID: 7d16_directoryemployees
|
||||
Revises: 7d15_warranties
|
||||
Create Date: 2026-07-10
|
||||
|
||||
"""
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
|
||||
revision = '7d16_directoryemployees'
|
||||
down_revision = '7d15_warranties'
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade():
|
||||
op.create_table(
|
||||
'directoryemployees',
|
||||
sa.Column('sso', sa.Integer(), primary_key=True, autoincrement=False),
|
||||
sa.Column('firstname', sa.String(length=100), nullable=False),
|
||||
sa.Column('lastname', sa.String(length=100), nullable=False),
|
||||
sa.Column('team', sa.String(length=100), nullable=True),
|
||||
sa.Column('role', sa.String(length=100), nullable=True),
|
||||
sa.Column('picture', sa.String(length=255), nullable=True),
|
||||
)
|
||||
|
||||
|
||||
def downgrade():
|
||||
op.drop_table('directoryemployees')
|
||||
205
migrations/versions/7d17_machines_rename.py
Normal file
205
migrations/versions/7d17_machines_rename.py
Normal file
@@ -0,0 +1,205 @@
|
||||
"""Equipment -> machines rename, core half
|
||||
|
||||
Part 1 of the approved equipment -> machines rename (the plugin half lives in
|
||||
plugins/machines/migrations/versions/0002_rename_from_equipment.py):
|
||||
|
||||
1. machinetypes -> modeltypes (+ machinetypeid -> modeltypeid,
|
||||
machinetype -> modeltype, models.machinetypeid -> models.modeltypeid).
|
||||
The table types the vendor MODELS catalog, not machine instances, so
|
||||
the new name is role-accurate and frees "machinetypes" for the plugin.
|
||||
2. Data flips: assettypes 'equipment' -> 'machine' (incl pluginname and
|
||||
tablename), auditlogs.entitytype 'Equipment' -> 'Machine', settings keys
|
||||
identifier_*_equipment_enabled -> identifier_*_machine_enabled,
|
||||
search_equipment_enabled -> search_machine_enabled, and permission
|
||||
rows equipment.* -> machines.* (role links survive).
|
||||
3. alembic_version_equipment -> alembic_version_machines (if present) so
|
||||
the renamed plugin's migration chain resumes seamlessly.
|
||||
|
||||
Idempotent via inspector guards, following the 7c01 pattern. MySQL is the
|
||||
only dialect the core chain runs against in practice.
|
||||
|
||||
Revision ID: 7d17_machines_rename
|
||||
Revises: 7d16_directoryemployees
|
||||
Create Date: 2026-07-11
|
||||
|
||||
"""
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
|
||||
revision = '7d17_machines_rename'
|
||||
down_revision = '7d16_directoryemployees'
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def _fk_names(insp, table, referred_table):
|
||||
# find FK constraint names on table pointing at referred_table
|
||||
return [fk['name'] for fk in insp.get_foreign_keys(table)
|
||||
if fk.get('referred_table') == referred_table and fk.get('name')]
|
||||
|
||||
|
||||
def upgrade():
|
||||
bind = op.get_bind()
|
||||
insp = sa.inspect(bind)
|
||||
tables = set(insp.get_table_names())
|
||||
|
||||
# -- 1. machinetypes -> modeltypes ------------------------------------
|
||||
if 'machinetypes' in tables and 'modeltypes' not in tables:
|
||||
bind.exec_driver_sql("SET FOREIGN_KEY_CHECKS=0")
|
||||
|
||||
# drop the models -> machinetypes FK and its index before renaming
|
||||
for name in _fk_names(insp, 'models', 'machinetypes'):
|
||||
bind.exec_driver_sql(f"ALTER TABLE models DROP FOREIGN KEY {name}")
|
||||
model_indexes = {ix['name'] for ix in insp.get_indexes('models')}
|
||||
if 'machinetypeid' in model_indexes:
|
||||
bind.exec_driver_sql("ALTER TABLE models DROP KEY machinetypeid")
|
||||
|
||||
bind.exec_driver_sql("RENAME TABLE machinetypes TO modeltypes")
|
||||
bind.exec_driver_sql(
|
||||
"ALTER TABLE modeltypes "
|
||||
"CHANGE machinetypeid modeltypeid INT NOT NULL AUTO_INCREMENT, "
|
||||
"CHANGE machinetype modeltype VARCHAR(100) NOT NULL, "
|
||||
"DROP KEY machinetype, "
|
||||
"ADD UNIQUE KEY modeltype (modeltype)"
|
||||
)
|
||||
bind.exec_driver_sql(
|
||||
"ALTER TABLE models "
|
||||
"CHANGE machinetypeid modeltypeid INT NULL, "
|
||||
"ADD KEY modeltypeid (modeltypeid), "
|
||||
"ADD CONSTRAINT fk_models_modeltypeid "
|
||||
"FOREIGN KEY (modeltypeid) REFERENCES modeltypes (modeltypeid)"
|
||||
)
|
||||
bind.exec_driver_sql("SET FOREIGN_KEY_CHECKS=1")
|
||||
|
||||
# -- 2. data flips ------------------------------------------------------
|
||||
if 'assettypes' in tables:
|
||||
bind.exec_driver_sql(
|
||||
"UPDATE assettypes SET assettype='machine', pluginname='machines', "
|
||||
"tablename='machines' WHERE assettype='equipment'"
|
||||
)
|
||||
if 'auditlogs' in tables:
|
||||
bind.exec_driver_sql(
|
||||
"UPDATE auditlogs SET entitytype='Machine' "
|
||||
"WHERE entitytype='Equipment'"
|
||||
)
|
||||
if 'settings' in tables:
|
||||
_rename_setting_keys(bind, old_marker='_equipment_',
|
||||
new_marker='_machine_')
|
||||
if 'permissions' in tables:
|
||||
_rename_permissions(bind, old_prefix='equipment.',
|
||||
new_prefix='machines.',
|
||||
old_category='equipment', new_category='machines')
|
||||
|
||||
# -- 3. plugin version table --------------------------------------------
|
||||
if ('alembic_version_equipment' in tables
|
||||
and 'alembic_version_machines' not in tables):
|
||||
bind.exec_driver_sql(
|
||||
"RENAME TABLE alembic_version_equipment TO alembic_version_machines"
|
||||
)
|
||||
|
||||
|
||||
def _rename_setting_keys(bind, old_marker, new_marker):
|
||||
"""Rename settings keys embedding the assettype value, collision-safe.
|
||||
|
||||
Covers identifier_<name>_equipment_enabled and search_equipment_enabled.
|
||||
If a row with the new key already exists (fresh seed ran on new code),
|
||||
the old row is dropped instead of renamed.
|
||||
"""
|
||||
# broad fetch; the marker replace below filters non-matches
|
||||
rows = bind.exec_driver_sql(
|
||||
"SELECT settingid, `key` FROM settings"
|
||||
).fetchall()
|
||||
for settingid, key in rows:
|
||||
newkey = key.replace(old_marker, new_marker)
|
||||
if newkey == key:
|
||||
continue
|
||||
exists = bind.exec_driver_sql(
|
||||
"SELECT 1 FROM settings WHERE `key` = %s", (newkey,)
|
||||
).fetchone()
|
||||
if exists:
|
||||
bind.exec_driver_sql(
|
||||
"DELETE FROM settings WHERE settingid = %s", (settingid,))
|
||||
else:
|
||||
bind.exec_driver_sql(
|
||||
"UPDATE settings SET `key` = %s WHERE settingid = %s",
|
||||
(newkey, settingid))
|
||||
|
||||
|
||||
def _rename_permissions(bind, old_prefix, new_prefix, old_category,
|
||||
new_category):
|
||||
"""Rename permission rows in place so role links survive, collision-safe."""
|
||||
rows = bind.exec_driver_sql(
|
||||
"SELECT permissionid, name FROM permissions"
|
||||
).fetchall()
|
||||
for permissionid, name in rows:
|
||||
if not name.startswith(old_prefix):
|
||||
continue
|
||||
newname = new_prefix + name[len(old_prefix):]
|
||||
exists = bind.exec_driver_sql(
|
||||
"SELECT 1 FROM permissions WHERE name = %s", (newname,)
|
||||
).fetchone()
|
||||
if exists:
|
||||
bind.exec_driver_sql(
|
||||
"DELETE FROM permissions WHERE permissionid = %s",
|
||||
(permissionid,))
|
||||
else:
|
||||
bind.exec_driver_sql(
|
||||
"UPDATE permissions SET name = %s, category = %s "
|
||||
"WHERE permissionid = %s",
|
||||
(newname, new_category, permissionid))
|
||||
|
||||
|
||||
def downgrade():
|
||||
bind = op.get_bind()
|
||||
insp = sa.inspect(bind)
|
||||
tables = set(insp.get_table_names())
|
||||
|
||||
if ('alembic_version_machines' in tables
|
||||
and 'alembic_version_equipment' not in tables):
|
||||
bind.exec_driver_sql(
|
||||
"RENAME TABLE alembic_version_machines TO alembic_version_equipment"
|
||||
)
|
||||
|
||||
if 'settings' in tables:
|
||||
_rename_setting_keys(bind, old_marker='_machine_',
|
||||
new_marker='_equipment_')
|
||||
if 'permissions' in tables:
|
||||
_rename_permissions(bind, old_prefix='machines.',
|
||||
new_prefix='equipment.',
|
||||
old_category='machines', new_category='equipment')
|
||||
if 'auditlogs' in tables:
|
||||
bind.exec_driver_sql(
|
||||
"UPDATE auditlogs SET entitytype='Equipment' "
|
||||
"WHERE entitytype='Machine'"
|
||||
)
|
||||
if 'assettypes' in tables:
|
||||
bind.exec_driver_sql(
|
||||
"UPDATE assettypes SET assettype='equipment', "
|
||||
"pluginname='equipment', tablename='equipment' "
|
||||
"WHERE assettype='machine'"
|
||||
)
|
||||
|
||||
if 'modeltypes' in tables and 'machinetypes' not in tables:
|
||||
bind.exec_driver_sql("SET FOREIGN_KEY_CHECKS=0")
|
||||
for name in _fk_names(insp, 'models', 'modeltypes'):
|
||||
bind.exec_driver_sql(f"ALTER TABLE models DROP FOREIGN KEY {name}")
|
||||
model_indexes = {ix['name'] for ix in insp.get_indexes('models')}
|
||||
if 'modeltypeid' in model_indexes:
|
||||
bind.exec_driver_sql("ALTER TABLE models DROP KEY modeltypeid")
|
||||
bind.exec_driver_sql("RENAME TABLE modeltypes TO machinetypes")
|
||||
bind.exec_driver_sql(
|
||||
"ALTER TABLE machinetypes "
|
||||
"CHANGE modeltypeid machinetypeid INT NOT NULL AUTO_INCREMENT, "
|
||||
"CHANGE modeltype machinetype VARCHAR(100) NOT NULL, "
|
||||
"DROP KEY modeltype, "
|
||||
"ADD UNIQUE KEY machinetype (machinetype)"
|
||||
)
|
||||
bind.exec_driver_sql(
|
||||
"ALTER TABLE models "
|
||||
"CHANGE modeltypeid machinetypeid INT NULL, "
|
||||
"ADD KEY machinetypeid (machinetypeid), "
|
||||
"ADD CONSTRAINT models_ibfk_1 "
|
||||
"FOREIGN KEY (machinetypeid) REFERENCES machinetypes (machinetypeid)"
|
||||
)
|
||||
bind.exec_driver_sql("SET FOREIGN_KEY_CHECKS=1")
|
||||
150
migrations/versions/7d18_supportteamcontacts.py
Normal file
150
migrations/versions/7d18_supportteamcontacts.py
Normal file
@@ -0,0 +1,150 @@
|
||||
"""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')
|
||||
70
migrations/versions/7d19_relationshiptype_directional.py
Normal file
70
migrations/versions/7d19_relationshiptype_directional.py
Normal file
@@ -0,0 +1,70 @@
|
||||
"""Relationship types get isdirectional; flip symmetric legacy types
|
||||
|
||||
Adds relationshiptypes.isdirectional (default true). The asset relationships
|
||||
card uses it: directional types keep a source->target reading, symmetric types
|
||||
(physical links, network connections) collapse both stored direction rows into
|
||||
one direction-blind "connected" entry per peer.
|
||||
|
||||
Seeds isdirectional=false for the connection-like types present pre-1.0:
|
||||
Dualpath, connectedto, Cluster Member, Serial Cable, Direct Ethernet, USB,
|
||||
WiFi. Everything else (controls, Controlled By, Backup For, Master-Slave,
|
||||
partof, defaultprinter) stays directional.
|
||||
|
||||
Idempotent guards so it is safe on a partially-migrated box.
|
||||
|
||||
Revision ID: 7d19_relationshiptype_directional
|
||||
Revises: 7d18_supportteamcontacts
|
||||
Create Date: 2026-07-11
|
||||
|
||||
"""
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
|
||||
revision = '7d19_relationshiptype_directional'
|
||||
down_revision = '7d18_supportteamcontacts'
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
# names seeded symmetric (isdirectional=false)
|
||||
SYMMETRIC_TYPES = [
|
||||
'Dualpath',
|
||||
'connectedto',
|
||||
'Cluster Member',
|
||||
'Serial Cable',
|
||||
'Direct Ethernet',
|
||||
'USB',
|
||||
'WiFi',
|
||||
]
|
||||
|
||||
|
||||
def _has_column(insp, table, column):
|
||||
return column in [c['name'] for c in insp.get_columns(table)]
|
||||
|
||||
|
||||
def upgrade():
|
||||
bind = op.get_bind()
|
||||
insp = sa.inspect(bind)
|
||||
|
||||
if not _has_column(insp, 'relationshiptypes', 'isdirectional'):
|
||||
op.add_column(
|
||||
'relationshiptypes',
|
||||
sa.Column('isdirectional', sa.Boolean(), nullable=False,
|
||||
server_default='1'))
|
||||
|
||||
# flip the known symmetric types. re-running just re-sets the same values.
|
||||
placeholders = ', '.join([f':n{i}' for i in range(len(SYMMETRIC_TYPES))])
|
||||
params = {f'n{i}': name for i, name in enumerate(SYMMETRIC_TYPES)}
|
||||
bind.execute(
|
||||
sa.text(
|
||||
"UPDATE relationshiptypes SET isdirectional = 0 "
|
||||
f"WHERE relationshiptype IN ({placeholders})"),
|
||||
params)
|
||||
|
||||
|
||||
def downgrade():
|
||||
bind = op.get_bind()
|
||||
insp = sa.inspect(bind)
|
||||
if _has_column(insp, 'relationshiptypes', 'isdirectional'):
|
||||
op.drop_column('relationshiptypes', 'isdirectional')
|
||||
109
migrations/versions/7d20_relationshiptypepropagations.py
Normal file
109
migrations/versions/7d20_relationshiptypepropagations.py
Normal file
@@ -0,0 +1,109 @@
|
||||
"""Relationship-type propagation goes M:N (relationshiptypepropagations)
|
||||
|
||||
Replaces the single-valued relationshiptypes.propagatesthroughid with a join
|
||||
table so one type can propagate through several through-types at once. On this
|
||||
DB `controls` needs both `partof` (declared) and `Dualpath` (consumed: a
|
||||
dual-bay pair is one physical machine with a single controller, so both bays
|
||||
must carry the controls link to that PC).
|
||||
|
||||
Upgrade: create relationshiptypepropagations, migrate the existing
|
||||
propagatesthroughid value into a row, drop the column, then seed the
|
||||
controls -> Dualpath row (resolved by name, skipped if either type missing).
|
||||
Downgrade: recreate the column from the first propagation row per type (best
|
||||
effort) and drop the table.
|
||||
|
||||
Idempotent guards so it is safe on a partially-migrated box.
|
||||
|
||||
Revision ID: 7d20_relationshiptypepropagations
|
||||
Revises: 7d19_relationshiptype_directional
|
||||
Create Date: 2026-07-11
|
||||
|
||||
"""
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
|
||||
revision = '7d20_relationshiptypepropagations'
|
||||
down_revision = '7d19_relationshiptype_directional'
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def _has_column(insp, table, column):
|
||||
return column in [c['name'] for c in insp.get_columns(table)]
|
||||
|
||||
|
||||
def upgrade():
|
||||
bind = op.get_bind()
|
||||
insp = sa.inspect(bind)
|
||||
|
||||
if 'relationshiptypepropagations' not in insp.get_table_names():
|
||||
op.create_table(
|
||||
'relationshiptypepropagations',
|
||||
sa.Column('propagationid', sa.Integer(), primary_key=True),
|
||||
sa.Column('relationshiptypeid', sa.Integer(), nullable=False),
|
||||
sa.Column('throughtypeid', sa.Integer(), nullable=False),
|
||||
sa.ForeignKeyConstraint(['relationshiptypeid'],
|
||||
['relationshiptypes.relationshiptypeid']),
|
||||
sa.ForeignKeyConstraint(['throughtypeid'],
|
||||
['relationshiptypes.relationshiptypeid']),
|
||||
sa.UniqueConstraint('relationshiptypeid', 'throughtypeid',
|
||||
name='uq_reltype_propagation'),
|
||||
)
|
||||
|
||||
if _has_column(insp, 'relationshiptypes', 'propagatesthroughid'):
|
||||
# migrate the single-FK value into a row (dedup guard for re-runs)
|
||||
bind.execute(sa.text(
|
||||
"INSERT INTO relationshiptypepropagations "
|
||||
"(relationshiptypeid, throughtypeid) "
|
||||
"SELECT rt.relationshiptypeid, rt.propagatesthroughid "
|
||||
"FROM relationshiptypes rt "
|
||||
"WHERE rt.propagatesthroughid IS NOT NULL AND NOT EXISTS ("
|
||||
" SELECT 1 FROM relationshiptypepropagations p "
|
||||
" WHERE p.relationshiptypeid = rt.relationshiptypeid "
|
||||
" AND p.throughtypeid = rt.propagatesthroughid)"))
|
||||
# drop the self-FK (known name from 7a01, else reflect) then the column
|
||||
fk_names = [fk['name'] for fk in insp.get_foreign_keys('relationshiptypes')
|
||||
if fk.get('constrained_columns') == ['propagatesthroughid']
|
||||
and fk.get('name')]
|
||||
for name in fk_names:
|
||||
op.drop_constraint(name, 'relationshiptypes', type_='foreignkey')
|
||||
op.drop_column('relationshiptypes', 'propagatesthroughid')
|
||||
|
||||
# seed controls -> Dualpath by name; skip if either type is missing
|
||||
rows = bind.execute(sa.text(
|
||||
"SELECT relationshiptypeid, relationshiptype FROM relationshiptypes")).fetchall()
|
||||
byname = {r[1]: r[0] for r in rows}
|
||||
controls = byname.get('controls')
|
||||
dualpath = byname.get('Dualpath')
|
||||
if controls and dualpath:
|
||||
exists = bind.execute(sa.text(
|
||||
"SELECT 1 FROM relationshiptypepropagations "
|
||||
"WHERE relationshiptypeid = :c AND throughtypeid = :d"),
|
||||
{'c': controls, 'd': dualpath}).fetchone()
|
||||
if not exists:
|
||||
bind.execute(sa.text(
|
||||
"INSERT INTO relationshiptypepropagations "
|
||||
"(relationshiptypeid, throughtypeid) VALUES (:c, :d)"),
|
||||
{'c': controls, 'd': dualpath})
|
||||
|
||||
|
||||
def downgrade():
|
||||
bind = op.get_bind()
|
||||
insp = sa.inspect(bind)
|
||||
|
||||
if not _has_column(insp, 'relationshiptypes', 'propagatesthroughid'):
|
||||
op.add_column('relationshiptypes',
|
||||
sa.Column('propagatesthroughid', sa.Integer(), nullable=True))
|
||||
op.create_foreign_key('fk_relationshiptype_propagation',
|
||||
'relationshiptypes', 'relationshiptypes',
|
||||
['propagatesthroughid'], ['relationshiptypeid'])
|
||||
|
||||
if 'relationshiptypepropagations' in insp.get_table_names():
|
||||
# best effort: restore the first propagation row per type
|
||||
bind.execute(sa.text(
|
||||
"UPDATE relationshiptypes rt SET propagatesthroughid = ("
|
||||
" SELECT p.throughtypeid FROM relationshiptypepropagations p "
|
||||
" WHERE p.relationshiptypeid = rt.relationshiptypeid "
|
||||
" ORDER BY p.propagationid LIMIT 1)"))
|
||||
op.drop_table('relationshiptypepropagations')
|
||||
61
migrations/versions/7d21_apitokens.py
Normal file
61
migrations/versions/7d21_apitokens.py
Normal file
@@ -0,0 +1,61 @@
|
||||
"""Personal API tokens (apitokens)
|
||||
|
||||
Adds the apitokens table: personal access tokens that let scripts and
|
||||
integrations authenticate as a user without the hourly-expiring login JWT.
|
||||
Only the sha256 hash of each secret is stored.
|
||||
|
||||
Idempotent guard so it is safe on a partially-migrated box; real downgrade.
|
||||
|
||||
Revision ID: 7d21_apitokens
|
||||
Revises: 7d20_relationshiptypepropagations
|
||||
Create Date: 2026-07-12
|
||||
|
||||
"""
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
|
||||
revision = '7d21_apitokens'
|
||||
down_revision = '7d20_relationshiptypepropagations'
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade():
|
||||
bind = op.get_bind()
|
||||
insp = sa.inspect(bind)
|
||||
|
||||
if 'apitokens' in insp.get_table_names():
|
||||
return
|
||||
|
||||
op.create_table(
|
||||
'apitokens',
|
||||
sa.Column('tokenid', sa.Integer(), primary_key=True),
|
||||
sa.Column('userid', sa.Integer(), nullable=False),
|
||||
sa.Column('name', sa.String(length=100), nullable=False),
|
||||
sa.Column('tokenprefix', sa.String(length=16), nullable=True),
|
||||
sa.Column('tokenhash', sa.String(length=64), nullable=False),
|
||||
sa.Column('expiresat', sa.DateTime(), nullable=True),
|
||||
sa.Column('lastusedat', sa.DateTime(), 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'),
|
||||
sa.ForeignKeyConstraint(['userid'], ['users.userid']),
|
||||
sa.UniqueConstraint('tokenhash', name='uq_apitoken_tokenhash'),
|
||||
)
|
||||
op.create_index('ix_apitokens_userid', 'apitokens', ['userid'])
|
||||
op.create_index('ix_apitokens_tokenprefix', 'apitokens', ['tokenprefix'])
|
||||
op.create_index('ix_apitokens_tokenhash', 'apitokens', ['tokenhash'])
|
||||
|
||||
|
||||
def downgrade():
|
||||
bind = op.get_bind()
|
||||
insp = sa.inspect(bind)
|
||||
|
||||
if 'apitokens' not in insp.get_table_names():
|
||||
return
|
||||
|
||||
op.drop_index('ix_apitokens_tokenhash', table_name='apitokens')
|
||||
op.drop_index('ix_apitokens_tokenprefix', table_name='apitokens')
|
||||
op.drop_index('ix_apitokens_userid', table_name='apitokens')
|
||||
op.drop_table('apitokens')
|
||||
49
migrations/versions/7d22_apitokens_scopes.py
Normal file
49
migrations/versions/7d22_apitokens_scopes.py
Normal file
@@ -0,0 +1,49 @@
|
||||
"""API token permission scopes (apitokens.scopes)
|
||||
|
||||
Adds a nullable scopes column to apitokens: a JSON array of permission-name
|
||||
strings. NULL means the token is unscoped and acts with the full authority of
|
||||
its owner (the original behavior). A scoped token grants ONLY the listed
|
||||
permissions, intersected with what the owner actually holds, and suspends the
|
||||
admin-role bypass so a scoped token minted by an admin is genuinely limited.
|
||||
|
||||
Idempotent guard so it is safe on a partially-migrated box; real downgrade.
|
||||
|
||||
Revision ID: 7d22_apitokens_scopes
|
||||
Revises: 7d21_apitokens
|
||||
Create Date: 2026-07-12
|
||||
|
||||
"""
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
|
||||
revision = '7d22_apitokens_scopes'
|
||||
down_revision = '7d21_apitokens'
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade():
|
||||
bind = op.get_bind()
|
||||
insp = sa.inspect(bind)
|
||||
|
||||
if 'apitokens' not in insp.get_table_names():
|
||||
return
|
||||
columns = {c['name'] for c in insp.get_columns('apitokens')}
|
||||
if 'scopes' in columns:
|
||||
return
|
||||
|
||||
op.add_column('apitokens', sa.Column('scopes', sa.Text(), nullable=True))
|
||||
|
||||
|
||||
def downgrade():
|
||||
bind = op.get_bind()
|
||||
insp = sa.inspect(bind)
|
||||
|
||||
if 'apitokens' not in insp.get_table_names():
|
||||
return
|
||||
columns = {c['name'] for c in insp.get_columns('apitokens')}
|
||||
if 'scopes' not in columns:
|
||||
return
|
||||
|
||||
op.drop_column('apitokens', 'scopes')
|
||||
50
migrations/versions/7d23_user_mustchangepassword.py
Normal file
50
migrations/versions/7d23_user_mustchangepassword.py
Normal file
@@ -0,0 +1,50 @@
|
||||
"""User forced-password-change flag (users.mustchangepassword)
|
||||
|
||||
Adds a boolean users.mustchangepassword column (default false). An admin who
|
||||
creates a user sets it true so the user is forced through a password change on
|
||||
first login; changing the password clears it.
|
||||
|
||||
Idempotent guard so it is safe on a partially-migrated box; real downgrade.
|
||||
|
||||
Revision ID: 7d23_user_mustchangepassword
|
||||
Revises: 7d22_apitokens_scopes
|
||||
Create Date: 2026-07-12
|
||||
|
||||
"""
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
|
||||
revision = '7d23_user_mustchangepassword'
|
||||
down_revision = '7d22_apitokens_scopes'
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade():
|
||||
bind = op.get_bind()
|
||||
insp = sa.inspect(bind)
|
||||
|
||||
if 'users' not in insp.get_table_names():
|
||||
return
|
||||
columns = {c['name'] for c in insp.get_columns('users')}
|
||||
if 'mustchangepassword' in columns:
|
||||
return
|
||||
|
||||
op.add_column(
|
||||
'users',
|
||||
sa.Column('mustchangepassword', sa.Boolean(),
|
||||
nullable=False, server_default=sa.false()))
|
||||
|
||||
|
||||
def downgrade():
|
||||
bind = op.get_bind()
|
||||
insp = sa.inspect(bind)
|
||||
|
||||
if 'users' not in insp.get_table_names():
|
||||
return
|
||||
columns = {c['name'] for c in insp.get_columns('users')}
|
||||
if 'mustchangepassword' not in columns:
|
||||
return
|
||||
|
||||
op.drop_column('users', 'mustchangepassword')
|
||||
50
migrations/versions/7d24_customfield_searchable.py
Normal file
50
migrations/versions/7d24_customfield_searchable.py
Normal file
@@ -0,0 +1,50 @@
|
||||
"""Custom field searchable flag (customfields.searchable)
|
||||
|
||||
Adds a boolean customfields.searchable column (default false). When true, the
|
||||
field's stored values are matched by global search and a hit routes to the
|
||||
owning asset's detail page.
|
||||
|
||||
Idempotent guard so it is safe on a partially-migrated box; real downgrade.
|
||||
|
||||
Revision ID: 7d24_customfield_searchable
|
||||
Revises: 7d23_user_mustchangepassword
|
||||
Create Date: 2026-07-12
|
||||
|
||||
"""
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
|
||||
revision = '7d24_customfield_searchable'
|
||||
down_revision = '7d23_user_mustchangepassword'
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade():
|
||||
bind = op.get_bind()
|
||||
insp = sa.inspect(bind)
|
||||
|
||||
if 'customfields' not in insp.get_table_names():
|
||||
return
|
||||
columns = {c['name'] for c in insp.get_columns('customfields')}
|
||||
if 'searchable' in columns:
|
||||
return
|
||||
|
||||
op.add_column(
|
||||
'customfields',
|
||||
sa.Column('searchable', sa.Boolean(),
|
||||
nullable=False, server_default=sa.false()))
|
||||
|
||||
|
||||
def downgrade():
|
||||
bind = op.get_bind()
|
||||
insp = sa.inspect(bind)
|
||||
|
||||
if 'customfields' not in insp.get_table_names():
|
||||
return
|
||||
columns = {c['name'] for c in insp.get_columns('customfields')}
|
||||
if 'searchable' not in columns:
|
||||
return
|
||||
|
||||
op.drop_column('customfields', 'searchable')
|
||||
57
migrations/versions/7d25_drop_redundant_indexes.py
Normal file
57
migrations/versions/7d25_drop_redundant_indexes.py
Normal file
@@ -0,0 +1,57 @@
|
||||
"""Drop redundant secondary indexes duplicating a named idx_* / unique index.
|
||||
|
||||
The DB review found five non-unique secondary indexes that duplicate an existing
|
||||
index on the same column: the auto-named ix_* from a column index=True alongside
|
||||
a named idx_* in __table_args__ (communications.assetid, and the hostname on
|
||||
computers/networkdevices/printers), plus idx_usb_serial duplicating the
|
||||
serialnumber unique index on usbdevices. Each redundant pair costs insert/update
|
||||
maintenance with zero read benefit. The models had the redundant index source
|
||||
removed; this drops the live duplicates so the schema matches the models.
|
||||
|
||||
Idempotent (skips a missing table/index); real downgrade recreates them.
|
||||
|
||||
Revision ID: 7d25_drop_redundant_indexes
|
||||
Revises: 7d24_customfield_searchable
|
||||
Create Date: 2026-07-13
|
||||
|
||||
"""
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
|
||||
revision = '7d25_drop_redundant_indexes'
|
||||
down_revision = '7d24_customfield_searchable'
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
# (table, index name, columns, unique) - columns/unique used only to recreate on
|
||||
# downgrade. All non-unique; the surviving idx_* / unique index still covers each.
|
||||
_REDUNDANT = [
|
||||
('communications', 'ix_communications_assetid', ['assetid'], False),
|
||||
('computers', 'ix_computers_hostname', ['hostname'], False),
|
||||
('networkdevices', 'ix_networkdevices_hostname', ['hostname'], False),
|
||||
('printers', 'ix_printers_hostname', ['hostname'], False),
|
||||
('usbdevices', 'idx_usb_serial', ['serialnumber'], False),
|
||||
]
|
||||
|
||||
|
||||
def _index_names(insp, table):
|
||||
return {i['name'] for i in insp.get_indexes(table)}
|
||||
|
||||
|
||||
def upgrade():
|
||||
bind = op.get_bind()
|
||||
insp = sa.inspect(bind)
|
||||
tables = set(insp.get_table_names())
|
||||
for table, name, _cols, _unique in _REDUNDANT:
|
||||
if table in tables and name in _index_names(insp, table):
|
||||
op.drop_index(name, table_name=table)
|
||||
|
||||
|
||||
def downgrade():
|
||||
bind = op.get_bind()
|
||||
insp = sa.inspect(bind)
|
||||
tables = set(insp.get_table_names())
|
||||
for table, name, cols, unique in _REDUNDANT:
|
||||
if table in tables and name not in _index_names(insp, table):
|
||||
op.create_index(name, table, cols, unique=unique)
|
||||
Reference in New Issue
Block a user