diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 94d5cb6e7f63888570cb3db31bfcf57467c8e650..4e4236f19cb708b07d3faa2c6a15e7238331907c 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -14,6 +14,7 @@ Added * Support for configuring the Untis school ID * Add Ukrainian locale (contributed by Sergiy Gorichenko from Fre(i)e Software GmbH). +* Import exams from Untis. Fixed ~~~~~ diff --git a/README.rst b/README.rst index d1f6c454c66b982c298bb41668b42f11b308aff0..10164b7f4f2f85c31d268c9e71ca535e9f97528e 100644 --- a/README.rst +++ b/README.rst @@ -14,6 +14,7 @@ Features * Import breaks * Import classes * Import events +* Import exams * Import exported Untis database via MySQL import * Import exported Untis XML files * Import holidays diff --git a/aleksis/apps/untis/model_extensions.py b/aleksis/apps/untis/model_extensions.py index 0baf5a886796a34fb989cc67065b15a10f1dbef2..ca4366eb7d25d234612b41ca2224e5ddbb09559f 100644 --- a/aleksis/apps/untis/model_extensions.py +++ b/aleksis/apps/untis/model_extensions.py @@ -74,3 +74,6 @@ chronos_models.Holiday.field( chronos_models.ExtraLesson.field( import_ref_untis=IntegerField(verbose_name=_("UNTIS import reference"), null=True, blank=True) ) +chronos_models.Exam.field( + import_ref_untis=IntegerField(verbose_name=_("UNTIS import reference"), null=True, blank=True) +) diff --git a/aleksis/apps/untis/util/mysql/importers/exams.py b/aleksis/apps/untis/util/mysql/importers/exams.py new file mode 100644 index 0000000000000000000000000000000000000000..dd9d788cb761383a7ab6b9f52af3b2f7b04cee89 --- /dev/null +++ b/aleksis/apps/untis/util/mysql/importers/exams.py @@ -0,0 +1,166 @@ +import logging + +from django.utils.translation import gettext as _ + +from calendarweek import CalendarWeek +from tqdm import tqdm + +from aleksis.apps.chronos import models as chronos_models +from aleksis.apps.chronos.models import ExtraLesson, Lesson, ValidityRange + +from .... import models as mysql_models +from ..util import ( + TQDM_DEFAULTS, + connect_untis_fields, + date_to_untis_date, + get_first_period, + get_last_period, + move_weekday_to_range, + run_default_filter, + untis_date_to_date, + untis_split_second, +) + +logger = logging.getLogger(__name__) + + +def import_exams( + validity_range: ValidityRange, + time_periods_ref, + subjects_ref, + teachers_ref, + rooms_ref, +): + ref = {} + + # Get absences + exams = ( + run_default_filter(validity_range, mysql_models.Exam.objects, filter_term=False) + .filter( + date__lte=date_to_untis_date(validity_range.date_end), + date__gte=date_to_untis_date(validity_range.date_start), + ) + .order_by("exam_id") + ) + + existing_exams = [] + for exam in tqdm(exams, desc="Import exams", **TQDM_DEFAULTS): + import_ref = exam.exam_id + + logger.info("Import exam {}".format(import_ref)) + + # Build values + title = exam.name + comment = exam.text + + day = untis_date_to_date(exam.date) + period_from = exam.lessonfrom + period_to = exam.lessonto + + weekday = day.weekday() + week = CalendarWeek.from_date(day) + + # Check min/max weekdays + weekday = move_weekday_to_range(time_periods_ref, weekday) + + # Check min/max periods + first_period = get_first_period(time_periods_ref, weekday) + last_period = get_last_period(time_periods_ref, weekday) + + if period_from == 0: + period_from = first_period + if period_to == 0: + period_to = last_period + + time_period_from = time_periods_ref[weekday][period_from] + time_period_to = time_periods_ref[weekday][period_to] + + # Get groups, teachers and rooms + raw_exams = connect_untis_fields(exam, "examelement", 10) + first = True + lesson = None + subject = None + exams = [] + for raw_exam in raw_exams: + el = untis_split_second(raw_exam, remove_empty=False) + if first: + lesson_id = int(el[0]) + subject_id = int(el[1]) + lesson = Lesson.objects.get(validity=validity_range, lesson_id_untis=lesson_id) + subject = subjects_ref[subject_id] + first = False + period = int(el[4]) + period = time_periods_ref[weekday][period] + teacher_id = int(el[5]) + room_id = int(el[6]) + teacher = teachers_ref[teacher_id] + room = rooms_ref[room_id] + exams.append((period, teacher, room)) + + if not lesson or not subject: + logger.warning(f"Skip exam {import_ref} due to missing data.") + continue + + new_exam, created = chronos_models.Exam.objects.update_or_create( + import_ref_untis=import_ref, + defaults={ + "date": day, + "lesson": lesson, + "period_from": time_period_from, + "period_to": time_period_to, + "title": title, + "comment": comment, + "school_term": validity_range.school_term, + }, + ) + + if created: + logger.info(" New exam created") + + extra_lesson_pks = [] + for exam in exams: + period, teacher, room = exam + comment = new_exam.title or _("Exam") + extra_lesson, __ = ExtraLesson.objects.get_or_create( + exam=new_exam, + period=period, + defaults={ + "room": room, + "week": week.week, + "year": week.year, + "comment": comment, + "subject": subject, + }, + ) + if ( + extra_lesson.room != room + or extra_lesson.week != week.week + or extra_lesson.year != week.year + or extra_lesson.comment != comment + or extra_lesson.subject != subject + ): + extra_lesson.room = room + extra_lesson.week = week.week + extra_lesson.year = week.year + extra_lesson.comment = comment + extra_lesson.subject = subject + extra_lesson.save() + + extra_lesson.groups.set(lesson.groups.all()) + extra_lesson.teachers.set([teacher]) + + extra_lesson_pks.append(extra_lesson.pk) + + # Delete no-longer necessary extra lessons + ExtraLesson.objects.filter(exam=new_exam).exclude(pk__in=extra_lesson_pks).delete() + + existing_exams.append(import_ref) + ref[import_ref] = new_exam + + # Delete all no longer existing exams + for e in chronos_models.Exam.objects.within_dates( + validity_range.date_start, validity_range.date_end + ): + if e.import_ref_untis and e.import_ref_untis not in existing_exams: + logger.info("exam {} deleted".format(e.id)) + e.delete() diff --git a/aleksis/apps/untis/util/mysql/main.py b/aleksis/apps/untis/util/mysql/main.py index 8b59bb6482d008e6a48baa66aecc375943b8a0c9..3e66968866a33611f1b89bbb7b439e71a285880a 100644 --- a/aleksis/apps/untis/util/mysql/main.py +++ b/aleksis/apps/untis/util/mysql/main.py @@ -21,6 +21,7 @@ from .importers.common_data import ( import_time_periods, ) from .importers.events import import_events +from .importers.exams import import_exams from .importers.holidays import import_holidays from .importers.lessons import import_lessons from .importers.substitutions import import_substitutions @@ -93,3 +94,6 @@ def untis_import_mysql( # Events import_events(validity_range, time_periods_ref, teachers_ref, classes_ref, rooms_ref) + + # Exams + import_exams(validity_range, time_periods_ref, subjects_ref, teachers_ref, rooms_ref) diff --git a/docs/admin/10_features.rst b/docs/admin/10_features.rst index 8d6e0c2a73d58c43ff5960cc3674b2ef12149650..fac7e04750a8aaa46934e37785483a59eadf9f8d 100644 --- a/docs/admin/10_features.rst +++ b/docs/admin/10_features.rst @@ -25,6 +25,7 @@ information from Untis can be imported into AlekSIS: * Absences, absence reasons * Substitutions, extra lessons, cancellations * Events +* Exams The Untis integration supports the versioning features of Untis. By default, the most recent version of each object is imported. @@ -32,10 +33,10 @@ the most recent version of each object is imported. Currently, the following features are known not to be supported: * Students, student groups, student choices -* Exams -* Calendars * Prebookings * Statistical data * Special rooms (subject and group rooms) +AlekSIS does not support so-called "day texts" from Untis. These are incompatible with AlekSIS' announcement feature, which can be used as a replacement. + .. _Untis MultiUser: https://www.untis.at/produkte/untis-das-grundpaket/multiuser