From 2f32277c0ebdc947dfb0a68deff21ec22e5f2ba2 Mon Sep 17 00:00:00 2001 From: Dominik George <dominik.george@teckids.org> Date: Sat, 18 Apr 2020 15:17:16 +0200 Subject: [PATCH] Use atomic transaction for whole import; use Enums instead of magic values Also, throws exceptions on unrecoverable errors to terminate and rollback the transaction cleanly --- .../untis/util/mysql/importers/absences.py | 12 ++++++--- .../untis/util/mysql/importers/common_data.py | 25 +++++++++--------- .../untis/util/mysql/importers/holidays.py | 3 +-- .../untis/util/mysql/importers/lessons.py | 3 ++- .../util/mysql/importers/substitutions.py | 26 +++++++++---------- aleksis/apps/untis/util/mysql/main.py | 3 +++ 6 files changed, 38 insertions(+), 34 deletions(-) diff --git a/aleksis/apps/untis/util/mysql/importers/absences.py b/aleksis/apps/untis/util/mysql/importers/absences.py index d6b7d45..4191f79 100644 --- a/aleksis/apps/untis/util/mysql/importers/absences.py +++ b/aleksis/apps/untis/util/mysql/importers/absences.py @@ -1,3 +1,4 @@ +from enum import Enum import logging from tqdm import tqdm @@ -14,6 +15,10 @@ from ..util import ( logger = logging.getLogger(__name__) unknown_reason, _ = chronos_models.AbsenceReason.objects.get_or_create(short_name="?") +class AbsenceType(Enum): + GROUP = 100 + TEACHER = 101 + ROOM = 102 def import_absences( absence_reasons_ref, time_periods_ref, teachers_ref, classes_ref, rooms_ref @@ -73,12 +78,11 @@ def import_absences( teacher = None room = None - # 100, 101 and 102 are UNTIS constants - if type_ == 100: + if type_ == AbsenceType.GROUP: group = classes_ref[absence.ida] - elif type_ == 101: + elif type_ == AbsenceType.TEACHER: teacher = teachers_ref[absence.ida] - elif type == 102: + elif type == AbsenceType.ROOM: room = rooms_ref[absence.ida] new_absence, created = chronos_models.Absence.objects.get_or_create( diff --git a/aleksis/apps/untis/util/mysql/importers/common_data.py b/aleksis/apps/untis/util/mysql/importers/common_data.py index 001f1d2..95f7444 100644 --- a/aleksis/apps/untis/util/mysql/importers/common_data.py +++ b/aleksis/apps/untis/util/mysql/importers/common_data.py @@ -1,3 +1,4 @@ +from enum import Enum import logging from datetime import time from typing import List, Dict @@ -15,6 +16,10 @@ from ..util import run_default_filter, untis_colour_to_hex, untis_split_first, c logger = logging.getLogger(__name__) +class CommonDataId(Enum): + PERIOD = 40 + + def import_subjects() -> Dict[int, chronos_models.Subject]: """ Import subjects """ @@ -26,12 +31,11 @@ def import_subjects() -> Dict[int, chronos_models.Subject]: for subject in tqdm(subjects, desc="Import subjects", **TQDM_DEFAULTS): # Check if needed data are provided if not subject.name: - logger.error( + raise RuntimeException( "Subject ID {}: Cannot import subject without short name.".format( subject.subject_id ) ) - continue # Build values short_name = subject.name[:10] @@ -95,12 +99,11 @@ def import_teachers() -> Dict[int, core_models.Person]: for teacher in tqdm(teachers, desc="Import teachers", **TQDM_DEFAULTS): # Check if needed data are provided if not teacher.name: - logger.error( + raise RuntimeException( "Teacher ID {}: Cannot import teacher without short name.".format( teacher.teacher_id ) ) - continue # Build values short_name = teacher.name @@ -165,12 +168,11 @@ def import_classes( for class_ in tqdm(course_classes, desc="Import classes", **TQDM_DEFAULTS): # Check if needed data are provided if not class_.name: - logger.error( + raise RuntimeException( "Class ID {}: Cannot import class without short name.".format( class_.teacher_id ) ) - continue # Build values short_name = class_.name[:16] @@ -234,12 +236,11 @@ def import_rooms() -> Dict[int, chronos_models.Room]: for room in tqdm(rooms, desc="Import rooms", **TQDM_DEFAULTS): if not room.name: - logger.error( + raise RuntimeException( "Room ID {}: Cannot import room without short name.".format( room.room_id ) ) - continue # Build values short_name = room.name[:10] @@ -288,12 +289,11 @@ def import_supervision_areas( for area in tqdm(areas, desc="Import supervision areas", **TQDM_DEFAULTS): if not area.name: - logger.error( + raise RuntimeException( "Supervision area ID {}: Cannot import supervision area without short name.".format( area.corridor_id ) ) - continue short_name = area.name[:10] name = area.longname[:50] if area.longname else short_name @@ -417,7 +417,7 @@ def import_time_periods() -> Dict[int, Dict[int, chronos_models.TimePeriod]]: periods = ( run_default_filter(mysql_models.Commondata.objects, filter_term=False) - .filter(id=40) # Fixed UNTIS constant + .filter(id=CommonDataId.PERIOD) .order_by("number", "number1") ) @@ -510,12 +510,11 @@ def import_absence_reasons() -> Dict[int, chronos_models.AbsenceReason]: for reason in tqdm(reasons, desc="Import absence reasons", **TQDM_DEFAULTS): if not reason.name: - logger.error( + raise RuntimeException( "Absence reason ID {}: Cannot import absence reason without short name.".format( reason.absence_reason_id ) ) - continue # Build values short_name = reason.name diff --git a/aleksis/apps/untis/util/mysql/importers/holidays.py b/aleksis/apps/untis/util/mysql/importers/holidays.py index 004acc2..c3d20ed 100644 --- a/aleksis/apps/untis/util/mysql/importers/holidays.py +++ b/aleksis/apps/untis/util/mysql/importers/holidays.py @@ -23,12 +23,11 @@ def import_holidays() -> Dict[int, chronos_models.Holiday]: # Check if needed data are provided if not holiday.name: - logger.error( + raise RuntimeException( "Holiday ID {}: Cannot import holiday without short name.".format( import_ref ) ) - continue title = holiday.name[:50] comments = holiday.longname diff --git a/aleksis/apps/untis/util/mysql/importers/lessons.py b/aleksis/apps/untis/util/mysql/importers/lessons.py index 9f95dcc..d9de93c 100644 --- a/aleksis/apps/untis/util/mysql/importers/lessons.py +++ b/aleksis/apps/untis/util/mysql/importers/lessons.py @@ -116,6 +116,7 @@ def import_lessons( course_classes.append(c) if config.UNTIS_IMPORT_MYSQL_USE_COURSE_GROUPS: + # Negative import_ref denotes a course group group_import_ref = -int("{}{}".format(lesson_id, i)) subject_ref = subject.abbrev @@ -223,7 +224,7 @@ def import_lessons( logger.info(" New lesson created") # Sync groups - lesson.groupsset(groups) + lesson.groups.set(groups) # Sync teachers lesson.teachers.set(teachers) diff --git a/aleksis/apps/untis/util/mysql/importers/substitutions.py b/aleksis/apps/untis/util/mysql/importers/substitutions.py index eaaf47a..0d2692c 100644 --- a/aleksis/apps/untis/util/mysql/importers/substitutions.py +++ b/aleksis/apps/untis/util/mysql/importers/substitutions.py @@ -1,3 +1,4 @@ +from enum import Enum import logging from calendarweek import CalendarWeek @@ -15,6 +16,9 @@ from .... import models as mysql_models logger = logging.getLogger(__name__) +class SubstitutionFlag(Enum): + CANCELLED = "E" + CANCELLED_FOR_TEACHERS = "F" def import_substitutions( teachers_ref, subjects_ref, rooms_ref, classes_ref, supervision_areas_ref @@ -58,9 +62,9 @@ def import_substitutions( # Cancellation? cancelled, cancelled_for_teachers = False, False - if "E" in sub.flags: + if SubstitutionFlag.CANCELLED in sub.flags: cancelled = True - elif "F" in sub.flags: + elif SubstitutionFlag.CANCELLED_FOR_TEACHERS in sub.flags: cancelled_for_teachers = True # Comment @@ -88,6 +92,7 @@ def import_substitutions( period__period=period, period__weekday=weekday, ).on_day(date) + if lesson_periods.exists(): lesson_period = lesson_periods[0] logger.info(" Matching lesson period found ({})".format(lesson_period)) @@ -106,7 +111,7 @@ def import_substitutions( if cancelled: subject_new = None - # # Room + # Room room_old = lesson_period.room if lesson_period else None room_new = None if sub.room_idsubst != 0: @@ -115,7 +120,7 @@ def import_substitutions( if room_old is not None and room_old.id == room_new.id: room_new = None - # # Classes + # Classes classes = [] class_ids = untis_split_first(sub.classids, conv=int) @@ -194,13 +199,6 @@ def import_substitutions( logger.info(" Supervision substitution updated") # Delete all no longer existing substitutions - for s in chronos_models.LessonSubstitution.objects.within_dates(date_start, date_end): - if s.import_ref_untis and s.import_ref_untis not in existing_subs: - logger.info("Substitution {} deleted".format(s.id)) - s.delete() - - # Delete all no longer existing supervision substitutions - for s in chronos_models.SupervisionSubstitution.objects.filter(date__gte=date_start, date__lte=date_end): - if s.import_ref_untis and s.import_ref_untis not in existing_subs: - logger.info("Supervision substitution {} deleted".format(s.id)) - s.delete() + chronos_models.LessonSubstitution.objects.within_dates(date_start, date_end).exclude(import_ref_untis__in=existing_subs).delete() + chronos_models.SupervisionSubstitution.objects.filter(date__gte=date_start, date__lte=date_end).exclude(existing_subs__in=existing_subs).delete() + logger.info("Left-over substitutions deleted") diff --git a/aleksis/apps/untis/util/mysql/main.py b/aleksis/apps/untis/util/mysql/main.py index ac4e73e..be8aa46 100644 --- a/aleksis/apps/untis/util/mysql/main.py +++ b/aleksis/apps/untis/util/mysql/main.py @@ -1,3 +1,5 @@ +from django.db import transaction + from .importers.absences import import_absences from .importers.common_data import ( import_subjects, @@ -15,6 +17,7 @@ from .importers.lessons import import_lessons from .importers.substitutions import import_substitutions +@transaction.atomic def untis_import_mysql(): # Coomon data for Chronos subjects_ref = import_subjects() -- GitLab