diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 0012f7ae6f2e8f418c0a4b50af0fedd385db87b5..8b55fc8dd21619cd55817143df53fd08627e9fe0 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -9,11 +9,28 @@ and this project adheres to `Semantic Versioning`_. Unreleased ---------- +Added +~~~~~ + +* Add fuzzy matching mode for searching course groups: If no 100 % match is found, + the importer will search a match by a subset of parent groups. + Changed ~~~~~~~ * Let untis_import_mysql management command default to ``current`` instead of all to prevent accidental imports of old plans +* Use new change tracker from Chronos to trigger notifications + +Fixed +~~~~~ + +* Search course groups not only by parent groups and subject, but also take + the teachers (group owners) into account +* Don't recreate lesson periods if they change, but just update them. + +`2.1.3`_ - 2022-02-06 +--------------------- Fixed ~~~~~ @@ -182,3 +199,4 @@ Fixed .. _2.1: https://edugit.org/Teckids/AlekSIS/AlekSIS-App-Untis/-/tags/2.1 .. _2.1.1: https://edugit.org/Teckids/AlekSIS/AlekSIS-App-Untis/-/tags/2.1.1 .. _2.1.2: https://edugit.org/Teckids/AlekSIS/AlekSIS-App-Untis/-/tags/2.1.2 +.. _2.1.3: https://edugit.org/Teckids/AlekSIS/AlekSIS-App-Untis/-/tags/2.1.3 diff --git a/aleksis/apps/untis/preferences.py b/aleksis/apps/untis/preferences.py index 99d9eeae4f6c3fbbaf135fa8a4edc8aa2ab505a3..d95f5bc30cb306d3bca40c0db60f682a26a86143 100644 --- a/aleksis/apps/untis/preferences.py +++ b/aleksis/apps/untis/preferences.py @@ -106,6 +106,15 @@ class DataCheckNotFoundCourseGroups(BooleanPreference): verbose_name = _("Register a data problem if a course group has been not found.") +@site_preferences_registry.register +class CourseGroupsFuzzyMatching(BooleanPreference): + section = untis_mysql + name = "course_groups_fuzzy_matching" + default = False + verbose_name = _("Match course groups by a subset of parent groups if no 100% match is found") + help_text = _("Works only if 'Use course groups' is activated.") + + @site_preferences_registry.register class IgnoreIncompleteSubstitutions(BooleanPreference): section = untis_mysql diff --git a/aleksis/apps/untis/util/mysql/importers/lessons.py b/aleksis/apps/untis/util/mysql/importers/lessons.py index 4b34f95c80c221af1942e6de4fab9d0f46a146cd..006fc497f3b57920e12237da04196912b2cc2abb 100644 --- a/aleksis/apps/untis/util/mysql/importers/lessons.py +++ b/aleksis/apps/untis/util/mysql/importers/lessons.py @@ -126,21 +126,54 @@ def import_lessons( # Negative import_ref denotes a course group group_import_ref = -int("{}{}".format(lesson_id, i)) - # Search by parent groups and subject + # Search by parent groups, teachers/owners and subject qs = core_models.Group.objects.filter( parent_groups__in=[c.id for c in course_classes], subject_id=subject.id, + owners__in=[t.id for t in teachers], ).filter(Q(school_term__isnull=True) | Q(school_term=validity_range.school_term)) # Check if found groups match course_group = None for found_group in qs: - if compare_m2m(course_classes, found_group.parent_groups.all()): + if compare_m2m(course_classes, found_group.parent_groups.all()) and compare_m2m( + teachers, found_group.owners.all() + ): + match = True course_group = found_group logger.info( - " Course group found by searching by parent groups and subject" + " Course group found by searching by parent groups, " + "teachers (owners) and subject" ) + if ( + not match + and get_site_preferences()["untis_mysql__course_groups_fuzzy_matching"] + ): + if qs.count() != 1: + logger.warning( + " Course group not found by searching by parent groups, " + "teachers (owners) and subject (fuzzy matching mode)" + ) + else: + for found_group in qs: + if compare_m2m(teachers, found_group.owners.all()): + if match: + logger.warning( + " More than one course group found " + "by searching by parent groups, " + "teachers (owners) and subject (fuzzy matching mode)" + ) + match = False + course_group = None + else: + match = True + course_group = found_group + logger.info( + " Course group found by searching by parent groups, " + "teachers (owners) and subject (fuzzy matching mode)" + ) + changed = False register_data_check = get_site_preferences()[ "untis_mysql__data_check_for_not_found_course_groups" @@ -257,10 +290,7 @@ def import_lessons( # All times for this course old_lesson_periods_qs = chronos_models.LessonPeriod.objects.filter(lesson=lesson) - # If length has changed, delete all lesson periods - if old_lesson_periods_qs.count() != len(time_periods): - old_lesson_periods_qs.delete() - logger.info(" Lesson periods deleted") + existing_lesson_period_pks = [] # Sync time periods for j, time_period in enumerate(time_periods): @@ -274,24 +304,29 @@ def import_lessons( room = None # Check if an old lesson period is provided - old_lesson_period_qs = old_lesson_periods_qs.filter(element_id_untis=j) + old_lesson_period_qs = old_lesson_periods_qs.filter(period=time_period) if old_lesson_period_qs.exists(): # Update old lesson period - old_lesson_period = old_lesson_period_qs[0] - if old_lesson_period.period != time_period or old_lesson_period.room != room: - old_lesson_period.period = time_period - old_lesson_period.room = room - old_lesson_period.save() - logger.info(" Time period and room updated") + lesson_period = old_lesson_period_qs[0] + if lesson_period.room != room or lesson_period.element_id_untis != j: + lesson_period.element_id_untis = j + lesson_period.room = room + lesson_period.save() + logger.info(" Untis reference and room updated") else: # Create new lesson period - chronos_models.LessonPeriod.objects.create( + lesson_period = chronos_models.LessonPeriod.objects.create( lesson=lesson, period=time_period, room=room, element_id_untis=j ) - logger.info(" New time period added") + logger.info(" New lesson period added") + + existing_lesson_period_pks.append(lesson_period.pk) + # delete all no-longer existing lesson periods + old_lesson_periods_qs.exclude(pk__in=existing_lesson_period_pks).delete() + logger.info(" Old lesson periods deleted") current_teacher_id = teacher_id for lesson in chronos_models.Lesson.objects.filter(validity=validity_range): diff --git a/aleksis/apps/untis/util/mysql/main.py b/aleksis/apps/untis/util/mysql/main.py index b3e6ca510ced6f8cb0bb1c13803537422501fdaa..7ae5113f890f93a2314ea6cf84924c5d479c0958 100644 --- a/aleksis/apps/untis/util/mysql/main.py +++ b/aleksis/apps/untis/util/mysql/main.py @@ -5,6 +5,7 @@ from django.db.models import QuerySet import reversion from tqdm import tqdm +from aleksis.apps.chronos.util.change_tracker import TimetableDataChangeTracker from aleksis.apps.untis.util.mysql.importers.terms import import_terms from aleksis.apps.untis.util.mysql.util import TQDM_DEFAULTS @@ -35,6 +36,8 @@ def untis_import_mysql(terms: Optional[QuerySet] = None, version: Optional[int] with reversion.create_revision(atomic=True): reversion.set_comment(f"Untis import for validity range {validity_range}") + tracker = TimetableDataChangeTracker() + # Common data for Chronos subjects_ref = import_subjects(validity_range) rooms_ref = import_rooms(validity_range) diff --git a/docs/conf.py b/docs/conf.py index c6d2a9fa5181e38139d767e7e472553a3c1d434b..ab89cfeaf2822a867e3757347d97986de72f6b14 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -31,7 +31,7 @@ author = "The AlekSIS Team" # The short X.Y version version = "2.1" # The full version, including alpha/beta/rc tags -release = "2.1.3.dev0" +release = "2.1.4.dev0" # -- General configuration --------------------------------------------------- diff --git a/pyproject.toml b/pyproject.toml index 468e28dba206227db2678ef421ba90049bc401e3..f26eef75d7acee3f5d82adfaaef9b5e79237ead1 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "AlekSIS-App-Untis" -version = "2.1.3.dev0" +version = "2.1.4.dev0" packages = [ { include = "aleksis" } ] @@ -41,11 +41,11 @@ python = "^3.9" mysqlclient = "^2.0.0" tqdm = "^4.44.1" defusedxml = "^0.7.0" -aleksis-core = "^2.0" -aleksis-app-chronos = "^2.0" +aleksis-core = "^2.8" +aleksis-app-chronos = "^2.3" [tool.poetry.dev-dependencies] -aleksis-builddeps = "^6" +aleksis-builddeps = "*" [tool.poetry.plugins."aleksis.app"] untis = "aleksis.apps.untis.apps:UntisConfig"