New Plugins: - USB plugin: Device checkout/checkin with employee lookup, checkout history - Notifications plugin: Announcements with types, scheduling, shopfloor display - Network plugin: Network device management with subnets and VLANs - Equipment and Computers plugins: Asset type separation Frontend: - EmployeeSearch component: Reusable employee lookup with autocomplete - USB views: List, detail, checkout/checkin modals - Notifications views: List, form with recognition mode - Network views: Device list, detail, form - Calendar view with FullCalendar integration - Shopfloor and TV dashboard views - Reports index page - Map editor for asset positioning - Light/dark mode fixes for map tooltips Backend: - Employee search API with external lookup service - Collector API for PowerShell data collection - Reports API endpoints - Slides API for TV dashboard - Fixed AppVersion model (removed BaseModel inheritance) - Added checkout_name column to usbcheckouts table Styling: - Unified detail page styles - Improved pagination (page numbers instead of prev/next) - Dark/light mode theme improvements Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
140 lines
5.0 KiB
Python
140 lines
5.0 KiB
Python
"""
|
|
Migrate notifications from legacy database.
|
|
|
|
This script migrates notification data from the VBScript database
|
|
to the new notifications plugin schema.
|
|
|
|
Usage:
|
|
python -m scripts.migration.migrate_notifications --source <connection_string>
|
|
"""
|
|
|
|
import argparse
|
|
import logging
|
|
from datetime import datetime
|
|
from sqlalchemy import create_engine, text
|
|
from sqlalchemy.orm import sessionmaker
|
|
|
|
logging.basicConfig(level=logging.INFO)
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
def get_notification_type_mapping(target_session):
|
|
"""Get mapping of type names to IDs in target database."""
|
|
result = target_session.execute(text(
|
|
"SELECT notificationtypeid, typename FROM notificationtypes"
|
|
))
|
|
return {row.typename.lower(): row.notificationtypeid for row in result}
|
|
|
|
|
|
def run_migration(source_conn_str, target_conn_str, dry_run=False):
|
|
"""
|
|
Run notification migration.
|
|
|
|
Args:
|
|
source_conn_str: Source database connection string
|
|
target_conn_str: Target database connection string
|
|
dry_run: If True, don't commit changes
|
|
"""
|
|
source_engine = create_engine(source_conn_str)
|
|
target_engine = create_engine(target_conn_str)
|
|
|
|
SourceSession = sessionmaker(bind=source_engine)
|
|
TargetSession = sessionmaker(bind=target_engine)
|
|
|
|
source_session = SourceSession()
|
|
target_session = TargetSession()
|
|
|
|
try:
|
|
# Get type mappings
|
|
type_mapping = get_notification_type_mapping(target_session)
|
|
|
|
# Default type if not found
|
|
default_type_id = type_mapping.get('general', 1)
|
|
|
|
# Fetch notifications from source
|
|
# Adjust column names based on actual legacy schema
|
|
notifications = source_session.execute(text("""
|
|
SELECT n.*, nt.typename
|
|
FROM notifications n
|
|
LEFT JOIN notificationtypes nt ON n.notificationtypeid = nt.notificationtypeid
|
|
"""))
|
|
|
|
migrated = 0
|
|
errors = 0
|
|
|
|
for notif in notifications:
|
|
notif_dict = dict(notif._mapping)
|
|
|
|
try:
|
|
# Map notification type
|
|
type_name = (notif_dict.get('typename') or 'general').lower()
|
|
type_id = type_mapping.get(type_name, default_type_id)
|
|
|
|
# Insert into target
|
|
target_session.execute(text("""
|
|
INSERT INTO notifications (
|
|
title, message, notificationtypeid,
|
|
startdate, enddate, ispinned, showbanner, allday,
|
|
linkurl, affectedsystems, isactive, createddate
|
|
) VALUES (
|
|
:title, :message, :notificationtypeid,
|
|
:startdate, :enddate, :ispinned, :showbanner, :allday,
|
|
:linkurl, :affectedsystems, :isactive, :createddate
|
|
)
|
|
"""), {
|
|
'title': notif_dict.get('title', 'Untitled'),
|
|
'message': notif_dict.get('message', ''),
|
|
'notificationtypeid': type_id,
|
|
'startdate': notif_dict.get('startdate', datetime.utcnow()),
|
|
'enddate': notif_dict.get('enddate'),
|
|
'ispinned': notif_dict.get('ispinned', False),
|
|
'showbanner': notif_dict.get('showbanner', True),
|
|
'allday': notif_dict.get('allday', True),
|
|
'linkurl': notif_dict.get('linkurl'),
|
|
'affectedsystems': notif_dict.get('affectedsystems'),
|
|
'isactive': notif_dict.get('isactive', True),
|
|
'createddate': notif_dict.get('createddate', datetime.utcnow()),
|
|
})
|
|
|
|
migrated += 1
|
|
|
|
except Exception as e:
|
|
logger.error(f"Error migrating notification: {e}")
|
|
errors += 1
|
|
|
|
if dry_run:
|
|
logger.info("Dry run - rolling back changes")
|
|
target_session.rollback()
|
|
else:
|
|
target_session.commit()
|
|
|
|
logger.info(f"Migration complete: {migrated} migrated, {errors} errors")
|
|
|
|
finally:
|
|
source_session.close()
|
|
target_session.close()
|
|
|
|
|
|
def main():
|
|
parser = argparse.ArgumentParser(description='Migrate notifications')
|
|
parser.add_argument('--source', required=True, help='Source database connection string')
|
|
parser.add_argument('--target', help='Target database connection string')
|
|
parser.add_argument('--dry-run', action='store_true', help='Dry run without committing')
|
|
|
|
args = parser.parse_args()
|
|
|
|
target = args.target
|
|
if not target:
|
|
import os
|
|
import sys
|
|
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.dirname(__file__))))
|
|
from shopdb import create_app
|
|
app = create_app()
|
|
target = app.config['SQLALCHEMY_DATABASE_URI']
|
|
|
|
run_migration(args.source, target, args.dry_run)
|
|
|
|
|
|
if __name__ == '__main__':
|
|
main()
|