501 lines
21 KiB
Python
501 lines
21 KiB
Python
#!/usr/bin/env python3
|
|
"""
|
|
Migration script to convert old application system to dynamic application system
|
|
|
|
This script:
|
|
1. Creates default QSM and VSM application types
|
|
2. Migrates existing applications to the new dynamic format
|
|
3. Preserves all data and relationships
|
|
"""
|
|
|
|
import os
|
|
import sys
|
|
import json
|
|
import logging
|
|
from datetime import datetime
|
|
from typing import Dict, Any, List, Optional
|
|
|
|
# Add parent directory to path for imports
|
|
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
|
|
|
from sqlalchemy import create_engine, text
|
|
from sqlalchemy.orm import sessionmaker
|
|
from src.models.application_type import (
|
|
ApplicationType, ApplicationField, ApplicationTypeStatus,
|
|
StatusTransition, DynamicApplication, ApplicationHistory,
|
|
FieldType, TransitionTriggerType
|
|
)
|
|
from src.models.base import Base
|
|
from src.config.database import get_database_url
|
|
|
|
# Configure logging
|
|
logging.basicConfig(
|
|
level=logging.INFO,
|
|
format='%(asctime)s - %(levelname)s - %(message)s'
|
|
)
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
def create_qsm_application_type(session) -> ApplicationType:
|
|
"""Create QSM application type with all fields"""
|
|
logger.info("Creating QSM application type...")
|
|
|
|
qsm_type = ApplicationType(
|
|
type_id="qsm",
|
|
name="QSM - Qualitätssicherungsmittel",
|
|
description="Antrag für Qualitätssicherungsmittel zur Verbesserung der Lehre",
|
|
is_active=True,
|
|
is_public=True,
|
|
max_cost_positions=100,
|
|
max_comparison_offers=100
|
|
)
|
|
session.add(qsm_type)
|
|
session.flush()
|
|
|
|
# Define QSM fields
|
|
qsm_fields = [
|
|
# Institution fields
|
|
{"field_id": "institution_type", "type": FieldType.SELECT, "name": "Art der Institution",
|
|
"options": ["Fachschaft", "STUPA-Referat", "Studentische Hochschulgruppe", "Fakultät", "Hochschuleinrichtung"],
|
|
"required": True, "order": 10},
|
|
{"field_id": "institution_name", "type": FieldType.TEXT_SHORT, "name": "Name der Institution",
|
|
"required": True, "order": 11},
|
|
|
|
# Applicant fields
|
|
{"field_id": "applicant_type", "type": FieldType.SELECT, "name": "Antragsteller",
|
|
"options": ["Person", "Institution"], "required": True, "order": 20},
|
|
{"field_id": "course", "type": FieldType.SELECT, "name": "Studiengang",
|
|
"options": ["INF", "ESB", "LS", "TEC", "TEX", "NXT"], "order": 21},
|
|
{"field_id": "role", "type": FieldType.SELECT, "name": "Rolle",
|
|
"options": ["Student", "Professor", "Mitarbeiter", "AStA", "Referatsleitung", "Fachschaftsvorstand"],
|
|
"order": 22},
|
|
{"field_id": "phone", "type": FieldType.PHONE, "name": "Telefonnummer", "order": 23},
|
|
|
|
# Project fields
|
|
{"field_id": "project_description", "type": FieldType.TEXT_LONG, "name": "Projektbeschreibung",
|
|
"required": True, "order": 30},
|
|
{"field_id": "project_start", "type": FieldType.DATE, "name": "Projektbeginn",
|
|
"required": True, "order": 31},
|
|
{"field_id": "project_end", "type": FieldType.DATE, "name": "Projektende", "order": 32},
|
|
{"field_id": "participants", "type": FieldType.NUMBER, "name": "Anzahl Teilnehmer", "order": 33},
|
|
|
|
# Participation
|
|
{"field_id": "faculty_inf", "type": FieldType.CHECKBOX, "name": "Fakultät INF", "order": 40},
|
|
{"field_id": "faculty_esb", "type": FieldType.CHECKBOX, "name": "Fakultät ESB", "order": 41},
|
|
{"field_id": "faculty_ls", "type": FieldType.CHECKBOX, "name": "Fakultät LS", "order": 42},
|
|
{"field_id": "faculty_tec", "type": FieldType.CHECKBOX, "name": "Fakultät TEC", "order": 43},
|
|
{"field_id": "faculty_tex", "type": FieldType.CHECKBOX, "name": "Fakultät TEX", "order": 44},
|
|
{"field_id": "faculty_nxt", "type": FieldType.CHECKBOX, "name": "Fakultät NxT", "order": 45},
|
|
{"field_id": "faculty_open", "type": FieldType.CHECKBOX, "name": "Fakultätsübergreifend", "order": 46},
|
|
|
|
# Financing
|
|
{"field_id": "qsm_code", "type": FieldType.SELECT, "name": "QSM-Code",
|
|
"options": [
|
|
"vwv-3-2-1-1: Finanzierung zusätzlicher Lehr- und Seminarangebote",
|
|
"vwv-3-2-1-2: Fachspezifische Studienprojekte",
|
|
"vwv-3-2-1-3: Hochschuldidaktische Fort- und Weiterbildungsmaßnahmen",
|
|
"vwv-3-2-2-1: Verbesserung/Ausbau von Serviceeinrichtungen",
|
|
"vwv-3-2-2-2: Lehr- und Lernmaterialien",
|
|
"vwv-3-2-2-3: Durchführung von Exkursionen",
|
|
"vwv-3-2-2-4: Infrastrukturelle Begleit- und Anpassungsmaßnahmen",
|
|
"vwv-3-2-3-1: Verbesserung der Beratungsangebote",
|
|
"vwv-3-2-3-2: Studium Generale und fachübergreifende Lehrangebote",
|
|
"vwv-3-2-3-3: Sonstige Maßnahmen im Interesse der Studierendenschaft"
|
|
],
|
|
"required": True, "order": 50},
|
|
{"field_id": "qsm_stellenfinanzierungen", "type": FieldType.CHECKBOX,
|
|
"name": "Stellenfinanzierungen", "order": 51},
|
|
{"field_id": "qsm_studierende", "type": FieldType.CHECKBOX,
|
|
"name": "Für Studierende", "order": 52},
|
|
{"field_id": "qsm_individuell", "type": FieldType.CHECKBOX,
|
|
"name": "Individuelle Maßnahme", "order": 53},
|
|
{"field_id": "qsm_exkursion_genehmigt", "type": FieldType.CHECKBOX,
|
|
"name": "Exkursion genehmigt", "order": 54},
|
|
{"field_id": "qsm_exkursion_bezuschusst", "type": FieldType.CHECKBOX,
|
|
"name": "Exkursion bezuschusst", "order": 55},
|
|
|
|
# Attachments
|
|
{"field_id": "comparative_offers", "type": FieldType.CHECKBOX,
|
|
"name": "Vergleichsangebote vorhanden", "order": 60},
|
|
{"field_id": "fakultaet_attachment", "type": FieldType.CHECKBOX,
|
|
"name": "Fakultätsbeschluss angehängt", "order": 61},
|
|
]
|
|
|
|
for field_def in qsm_fields:
|
|
field = ApplicationField(
|
|
application_type_id=qsm_type.id,
|
|
field_id=field_def["field_id"],
|
|
field_type=field_def["type"],
|
|
name=field_def["name"],
|
|
field_order=field_def.get("order", 0),
|
|
is_required=field_def.get("required", False),
|
|
options=field_def.get("options"),
|
|
validation_rules=field_def.get("validation", {})
|
|
)
|
|
session.add(field)
|
|
|
|
return qsm_type
|
|
|
|
|
|
def create_vsm_application_type(session) -> ApplicationType:
|
|
"""Create VSM application type with all fields"""
|
|
logger.info("Creating VSM application type...")
|
|
|
|
vsm_type = ApplicationType(
|
|
type_id="vsm",
|
|
name="VSM - Verfasste Studierendenschaft",
|
|
description="Antrag für Mittel der Verfassten Studierendenschaft",
|
|
is_active=True,
|
|
is_public=True,
|
|
max_cost_positions=100,
|
|
max_comparison_offers=100
|
|
)
|
|
session.add(vsm_type)
|
|
session.flush()
|
|
|
|
# Define VSM fields (similar to QSM but with VSM-specific financing)
|
|
vsm_fields = [
|
|
# Institution fields
|
|
{"field_id": "institution_type", "type": FieldType.SELECT, "name": "Art der Institution",
|
|
"options": ["Fachschaft", "STUPA-Referat", "Studentische Hochschulgruppe"],
|
|
"required": True, "order": 10},
|
|
{"field_id": "institution_name", "type": FieldType.TEXT_SHORT, "name": "Name der Institution",
|
|
"required": True, "order": 11},
|
|
|
|
# Applicant fields (same as QSM)
|
|
{"field_id": "applicant_type", "type": FieldType.SELECT, "name": "Antragsteller",
|
|
"options": ["Person", "Institution"], "required": True, "order": 20},
|
|
{"field_id": "course", "type": FieldType.SELECT, "name": "Studiengang",
|
|
"options": ["INF", "ESB", "LS", "TEC", "TEX", "NXT"], "order": 21},
|
|
{"field_id": "role", "type": FieldType.SELECT, "name": "Rolle",
|
|
"options": ["Student", "AStA", "Referatsleitung", "Fachschaftsvorstand"],
|
|
"order": 22},
|
|
{"field_id": "phone", "type": FieldType.PHONE, "name": "Telefonnummer", "order": 23},
|
|
|
|
# Project fields (same as QSM)
|
|
{"field_id": "project_description", "type": FieldType.TEXT_LONG, "name": "Projektbeschreibung",
|
|
"required": True, "order": 30},
|
|
{"field_id": "project_start", "type": FieldType.DATE, "name": "Projektbeginn",
|
|
"required": True, "order": 31},
|
|
{"field_id": "project_end", "type": FieldType.DATE, "name": "Projektende", "order": 32},
|
|
{"field_id": "participants", "type": FieldType.NUMBER, "name": "Anzahl Teilnehmer", "order": 33},
|
|
|
|
# VSM-specific financing
|
|
{"field_id": "vsm_code", "type": FieldType.SELECT, "name": "VSM-Code",
|
|
"options": [
|
|
"lhg-01: Hochschulpolitische, fachliche, soziale, wirtschaftliche und kulturelle Belange",
|
|
"lhg-02: Mitwirkung an den Aufgaben der Hochschulen",
|
|
"lhg-03: Politische Bildung",
|
|
"lhg-04: Förderung der Chancengleichheit",
|
|
"lhg-05: Förderung der Integration ausländischer Studierender",
|
|
"lhg-06: Förderung der sportlichen Aktivitäten",
|
|
"lhg-07: Pflege der überregionalen Studierendenbeziehungen"
|
|
],
|
|
"required": True, "order": 50},
|
|
{"field_id": "vsm_aufgaben", "type": FieldType.CHECKBOX,
|
|
"name": "Aufgaben der Studierendenschaft", "order": 51},
|
|
{"field_id": "vsm_individuell", "type": FieldType.CHECKBOX,
|
|
"name": "Individuelle Maßnahme", "order": 52},
|
|
|
|
# Attachments
|
|
{"field_id": "comparative_offers", "type": FieldType.CHECKBOX,
|
|
"name": "Vergleichsangebote vorhanden", "order": 60},
|
|
]
|
|
|
|
for field_def in vsm_fields:
|
|
field = ApplicationField(
|
|
application_type_id=vsm_type.id,
|
|
field_id=field_def["field_id"],
|
|
field_type=field_def["type"],
|
|
name=field_def["name"],
|
|
field_order=field_def.get("order", 0),
|
|
is_required=field_def.get("required", False),
|
|
options=field_def.get("options"),
|
|
validation_rules=field_def.get("validation", {})
|
|
)
|
|
session.add(field)
|
|
|
|
return vsm_type
|
|
|
|
|
|
def create_statuses_and_transitions(session, app_type: ApplicationType):
|
|
"""Create standard statuses and transitions for an application type"""
|
|
logger.info(f"Creating statuses and transitions for {app_type.name}...")
|
|
|
|
# Define standard statuses
|
|
statuses = [
|
|
{"id": "draft", "name": "Entwurf", "editable": True, "color": "#6B7280",
|
|
"initial": True, "final": False},
|
|
{"id": "submitted", "name": "Beantragt", "editable": False, "color": "#3B82F6",
|
|
"initial": False, "final": False, "notification": True},
|
|
{"id": "processing_locked", "name": "Bearbeitung gesperrt", "editable": False,
|
|
"color": "#F59E0B", "initial": False, "final": False},
|
|
{"id": "under_review", "name": "Zu prüfen", "editable": False, "color": "#8B5CF6",
|
|
"initial": False, "final": False},
|
|
{"id": "voting", "name": "Zur Abstimmung", "editable": False, "color": "#EC4899",
|
|
"initial": False, "final": False},
|
|
{"id": "approved", "name": "Genehmigt", "editable": False, "color": "#10B981",
|
|
"initial": False, "final": True, "notification": True},
|
|
{"id": "rejected", "name": "Abgelehnt", "editable": False, "color": "#EF4444",
|
|
"initial": False, "final": True, "notification": True},
|
|
{"id": "cancelled", "name": "Zurückgezogen", "editable": False, "color": "#9CA3AF",
|
|
"initial": False, "final": True, "cancelled": True},
|
|
]
|
|
|
|
status_objects = {}
|
|
for i, status_def in enumerate(statuses):
|
|
status = ApplicationTypeStatus(
|
|
application_type_id=app_type.id,
|
|
status_id=status_def["id"],
|
|
name=status_def["name"],
|
|
is_editable=status_def["editable"],
|
|
color=status_def["color"],
|
|
display_order=i * 10,
|
|
is_initial=status_def.get("initial", False),
|
|
is_final=status_def.get("final", False),
|
|
is_cancelled=status_def.get("cancelled", False),
|
|
send_notification=status_def.get("notification", False)
|
|
)
|
|
session.add(status)
|
|
session.flush()
|
|
status_objects[status_def["id"]] = status
|
|
|
|
# Define transitions
|
|
transitions = [
|
|
# From Draft
|
|
{"from": "draft", "to": "submitted", "name": "Antrag einreichen",
|
|
"trigger": TransitionTriggerType.APPLICANT_ACTION},
|
|
|
|
# From Submitted
|
|
{"from": "submitted", "to": "processing_locked", "name": "Bearbeitung sperren",
|
|
"trigger": TransitionTriggerType.USER_APPROVAL, "role": "admin"},
|
|
{"from": "submitted", "to": "under_review", "name": "Zur Prüfung freigeben",
|
|
"trigger": TransitionTriggerType.USER_APPROVAL, "role": "admin"},
|
|
{"from": "submitted", "to": "cancelled", "name": "Zurückziehen",
|
|
"trigger": TransitionTriggerType.APPLICANT_ACTION},
|
|
|
|
# From Processing Locked
|
|
{"from": "processing_locked", "to": "under_review", "name": "Bearbeitung entsperren",
|
|
"trigger": TransitionTriggerType.USER_APPROVAL, "role": "admin"},
|
|
|
|
# From Under Review
|
|
{"from": "under_review", "to": "voting", "name": "Zur Abstimmung freigeben",
|
|
"trigger": TransitionTriggerType.USER_APPROVAL, "role": "budget_reviewer"},
|
|
{"from": "under_review", "to": "rejected", "name": "Ablehnen",
|
|
"trigger": TransitionTriggerType.USER_APPROVAL, "role": "budget_reviewer"},
|
|
|
|
# From Voting
|
|
{"from": "voting", "to": "approved", "name": "Genehmigen",
|
|
"trigger": TransitionTriggerType.USER_APPROVAL, "role": "asta_member",
|
|
"required": 3}, # Requires 3 AStA members to approve
|
|
{"from": "voting", "to": "rejected", "name": "Ablehnen",
|
|
"trigger": TransitionTriggerType.USER_APPROVAL, "role": "asta_member",
|
|
"required": 3}, # Requires 3 AStA members to reject
|
|
]
|
|
|
|
for trans_def in transitions:
|
|
config = {"role": trans_def.get("role", "admin")}
|
|
if "required" in trans_def:
|
|
config["required_approvals"] = trans_def["required"]
|
|
|
|
transition = StatusTransition(
|
|
from_status_id=status_objects[trans_def["from"]].id,
|
|
to_status_id=status_objects[trans_def["to"]].id,
|
|
name=trans_def["name"],
|
|
trigger_type=trans_def["trigger"],
|
|
trigger_config=config,
|
|
is_active=True
|
|
)
|
|
session.add(transition)
|
|
|
|
|
|
def migrate_old_application(session, old_app: Dict[str, Any], app_type: ApplicationType) -> DynamicApplication:
|
|
"""Migrate an old application to the new dynamic format"""
|
|
|
|
# Extract data from old format
|
|
payload = old_app.get("payload", {})
|
|
pa = payload.get("pa", {})
|
|
applicant = pa.get("applicant", {})
|
|
project = pa.get("project", {})
|
|
|
|
# Map old status to new status
|
|
status_map = {
|
|
"DRAFT": "draft",
|
|
"BEANTRAGT": "submitted",
|
|
"BEARBEITUNG_GESPERRT": "processing_locked",
|
|
"ZU_PRUEFEN": "under_review",
|
|
"ZUR_ABSTIMMUNG": "voting",
|
|
"GENEHMIGT": "approved",
|
|
"ABGELEHNT": "rejected",
|
|
"CANCELLED": "cancelled"
|
|
}
|
|
|
|
# Build field data
|
|
field_data = {}
|
|
|
|
# Institution fields
|
|
institution = applicant.get("institution", {})
|
|
field_data["institution_type"] = institution.get("type", "")
|
|
field_data["institution_name"] = institution.get("name", "")
|
|
|
|
# Applicant fields
|
|
field_data["applicant_type"] = applicant.get("type", "person")
|
|
name = applicant.get("name", {})
|
|
contact = applicant.get("contact", {})
|
|
field_data["course"] = applicant.get("course", "")
|
|
field_data["role"] = applicant.get("role", "")
|
|
field_data["phone"] = contact.get("phone", "")
|
|
|
|
# Project fields
|
|
field_data["project_description"] = project.get("description", "")
|
|
dates = project.get("dates", {})
|
|
field_data["project_start"] = dates.get("start", "")
|
|
field_data["project_end"] = dates.get("end", "")
|
|
field_data["participants"] = project.get("participants", 0)
|
|
|
|
# Participation
|
|
participation = project.get("participation", {})
|
|
faculties = participation.get("faculties", {})
|
|
field_data["faculty_inf"] = faculties.get("inf", False)
|
|
field_data["faculty_esb"] = faculties.get("esb", False)
|
|
field_data["faculty_ls"] = faculties.get("ls", False)
|
|
field_data["faculty_tec"] = faculties.get("tec", False)
|
|
field_data["faculty_tex"] = faculties.get("tex", False)
|
|
field_data["faculty_nxt"] = faculties.get("nxt", False)
|
|
field_data["faculty_open"] = faculties.get("open", False)
|
|
|
|
# Financing
|
|
financing = project.get("financing", {})
|
|
if app_type.type_id == "qsm":
|
|
qsm = financing.get("qsm", {})
|
|
field_data["qsm_code"] = qsm.get("code", "")
|
|
flags = qsm.get("flags", {})
|
|
field_data["qsm_stellenfinanzierungen"] = flags.get("stellenfinanzierungen", False)
|
|
field_data["qsm_studierende"] = flags.get("studierende", False)
|
|
field_data["qsm_individuell"] = flags.get("individuell", False)
|
|
field_data["qsm_exkursion_genehmigt"] = flags.get("exkursionGenehmigt", False)
|
|
field_data["qsm_exkursion_bezuschusst"] = flags.get("exkursionBezuschusst", False)
|
|
else: # VSM
|
|
vsm = financing.get("vsm", {})
|
|
field_data["vsm_code"] = vsm.get("code", "")
|
|
flags = vsm.get("flags", {})
|
|
field_data["vsm_aufgaben"] = flags.get("aufgaben", False)
|
|
field_data["vsm_individuell"] = flags.get("individuell", False)
|
|
|
|
# Attachments
|
|
attachments = pa.get("attachments", {})
|
|
field_data["comparative_offers"] = attachments.get("comparativeOffers", False)
|
|
if app_type.type_id == "qsm":
|
|
field_data["fakultaet_attachment"] = attachments.get("fakultaet", False)
|
|
|
|
# Cost positions
|
|
costs = project.get("costs", [])
|
|
cost_positions = []
|
|
for cost in costs:
|
|
cost_positions.append({
|
|
"description": cost.get("name", ""),
|
|
"amount": cost.get("amountEur", 0),
|
|
"category": "",
|
|
"notes": ""
|
|
})
|
|
|
|
# Create new dynamic application
|
|
new_app = DynamicApplication(
|
|
application_id=old_app["pa_id"],
|
|
application_key=old_app["pa_key"],
|
|
application_type_id=app_type.id,
|
|
user_id=old_app.get("user_id"),
|
|
email=contact.get("email", ""),
|
|
status_id=status_map.get(old_app.get("status", "DRAFT"), "draft"),
|
|
title=project.get("name", ""),
|
|
first_name=name.get("first", ""),
|
|
last_name=name.get("last", ""),
|
|
field_data=field_data,
|
|
cost_positions=cost_positions,
|
|
total_amount=project.get("totals", {}).get("requestedAmountEur", 0),
|
|
submitted_at=old_app.get("submitted_at"),
|
|
created_at=old_app.get("created_at", datetime.utcnow()),
|
|
updated_at=old_app.get("updated_at", datetime.utcnow())
|
|
)
|
|
|
|
return new_app
|
|
|
|
|
|
def main():
|
|
"""Main migration function"""
|
|
logger.info("Starting migration to dynamic application system...")
|
|
|
|
# Create database connection
|
|
engine = create_engine(get_database_url())
|
|
Session = sessionmaker(bind=engine)
|
|
session = Session()
|
|
|
|
try:
|
|
# Step 1: Create application types
|
|
qsm_type = create_qsm_application_type(session)
|
|
vsm_type = create_vsm_application_type(session)
|
|
session.commit()
|
|
logger.info("Application types created successfully")
|
|
|
|
# Step 2: Create statuses and transitions
|
|
create_statuses_and_transitions(session, qsm_type)
|
|
create_statuses_and_transitions(session, vsm_type)
|
|
session.commit()
|
|
logger.info("Statuses and transitions created successfully")
|
|
|
|
# Step 3: Migrate existing applications
|
|
logger.info("Migrating existing applications...")
|
|
|
|
# Query old applications (if table exists)
|
|
try:
|
|
result = session.execute(text("SELECT * FROM applications"))
|
|
old_applications = result.fetchall()
|
|
|
|
migrated_count = 0
|
|
for old_app_row in old_applications:
|
|
old_app = dict(old_app_row._mapping)
|
|
|
|
# Determine type based on variant
|
|
variant = old_app.get("variant", "QSM")
|
|
app_type = qsm_type if variant == "QSM" else vsm_type
|
|
|
|
# Migrate application
|
|
new_app = migrate_old_application(session, old_app, app_type)
|
|
session.add(new_app)
|
|
|
|
# Create history entry
|
|
history = ApplicationHistory(
|
|
application_id=new_app.id,
|
|
action="migrated",
|
|
comment=f"Migrated from old {variant} application",
|
|
created_at=datetime.utcnow()
|
|
)
|
|
session.add(history)
|
|
|
|
migrated_count += 1
|
|
|
|
if migrated_count % 100 == 0:
|
|
session.commit()
|
|
logger.info(f"Migrated {migrated_count} applications...")
|
|
|
|
session.commit()
|
|
logger.info(f"Successfully migrated {migrated_count} applications")
|
|
|
|
except Exception as e:
|
|
logger.warning(f"Could not migrate old applications: {e}")
|
|
logger.info("This is normal if running on a fresh database")
|
|
|
|
logger.info("Migration completed successfully!")
|
|
|
|
except Exception as e:
|
|
logger.error(f"Migration failed: {e}")
|
|
session.rollback()
|
|
raise
|
|
|
|
finally:
|
|
session.close()
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|