From 977e0cb7c487fc155ec9b73090b73d3281101fdc Mon Sep 17 00:00:00 2001 From: Christian Cueni Date: Mon, 7 Aug 2023 16:29:18 +0200 Subject: [PATCH] WIP: Add import test --- .../edoniq_test/tests/test_import.py | 107 +++++++++ server/vbv_lernwelt/importer/services.py | 206 ++++++++++++++++-- 2 files changed, 296 insertions(+), 17 deletions(-) create mode 100644 server/vbv_lernwelt/edoniq_test/tests/test_import.py diff --git a/server/vbv_lernwelt/edoniq_test/tests/test_import.py b/server/vbv_lernwelt/edoniq_test/tests/test_import.py new file mode 100644 index 00000000..f6ffa336 --- /dev/null +++ b/server/vbv_lernwelt/edoniq_test/tests/test_import.py @@ -0,0 +1,107 @@ +from django.test import TestCase +from django.utils import timezone + +from vbv_lernwelt.assignment.models import AssignmentType +from vbv_lernwelt.core.create_default_users import create_default_users +from vbv_lernwelt.course.creators.test_course import create_test_course +from vbv_lernwelt.course.models import CourseSession +from vbv_lernwelt.course_session.models import CourseSessionAttendanceCourse, CourseSessionAssignment +from vbv_lernwelt.importer.services import create_or_update_course_session +from vbv_lernwelt.learnpath.models import LearningContentAttendanceCourse, LearningContentAssignment + +TEST_SESSION = { + "ID": "AG 2023 A", + "Generation": "2023", + "Region": "Aargau", + "Sprache": "de", + "Klasse": "A", + "Kickoff Start": "30.08.2023, 09:15", + "Kickoff Ende": "30.08.2023, 17:00", + "Kickoff Raum": "E34", + "Kickoff Standort": "HKV Aarau", + "Kickoff Adresse": "Bahnhofstrasse 46, 5000, Aarau", + "Basis Start": "06.09.2023, 09:15", + "Basis Ende": "06.09.2023, 17:00", + "Basis Raum": "E34", + "Basis Standort": "HKV Aarau", + "Basis Adresse": "Bahnhofstrasse 46, 5000, Aarau", + "Fahrzeug Start": "08.11.2023, 09:15", + "Fahrzeug Ende": "08.11.2023, 17:00", + "Fahrzeug Raum": "E34", + "Fahrzeug Standort": "HKV Aarau", + "Fahrzeug Adresse": "Bahnhofstrasse 46, 5000, Aarau", + "Haushalt Teil 1 Start": "17.01.2024, 09:15", + "Hauhalt Teil 1 Ende": "17.01.2024, 17:00", + "Haushalt Teil 1 Raum": "E34", + "Haushalt Teil 1 Standort": "HKV Aarau", + "Haushalt Teil 1 Adresse": "Bahnhofstrasse 46, 5000, Aarau", + "Haushalt Teil 2 Start": "20.03.2024, 09:15", + "Haushalt Teil 2 Ende": "20.03.2024, 17:00", + "Haushalt Teil 2 Raum": "E34", + "Haushalt Teil 2 Standort": "HKV Aarau", + "Haushalt Teil 2 Adresse": "Bahnhofstrasse 46, 5000, Aarau", +} + +TEST_CIRCLES = ["Fahrzeug"] + +LP_DATA = { + "Fahrzeug": + { + "de": { + "title": "Fahrzeug", + "slug": "fahrzeug", + "presence_course": "fahrzeug-lc-präsenzkurs-fahrzeug", + "assignments": [ + "fahrzeug-lc-überprüfen-einer-motorfahrzeug-versicherungspolice", + "fahrzeug-lc-fahrzeug-mein-erstes-auto", + ] + }, + }, +} + + +class EdoniqSessionTrainerImportTestCase(TestCase): + def setUp(self) -> None: + create_default_users() + self.course = create_test_course(with_sessions=False) + + def test_session_import(self): + create_or_update_course_session(self.course, TEST_SESSION, "de", circle_keys=TEST_CIRCLES) + cs = CourseSession.objects.get(import_id="AG 2023 A") + + self.assertEqual(cs.course, self.course) + + for circle_name in TEST_CIRCLES: + self._check_attendance(circle_name, cs) + self._check_assignments(circle_name, cs) + + def _check_attendance(self, circle_name: str, cs: CourseSession): + attendance = LearningContentAttendanceCourse.objects.filter( + slug=f"{self.course.slug}-lp-circle-{LP_DATA[circle_name]['de']['presence_course']}", + ) + self.assertEqual(attendance.count(), 1) # only one attendance per circle + + csac = CourseSessionAttendanceCourse.objects.get( + course_session=cs, learning_content=attendance.first() + ) + + self.assertEqual( + f"{TEST_SESSION[f'{circle_name} Raum']}, {TEST_SESSION[f'{circle_name} Standort']}, {TEST_SESSION[f'{circle_name} Adresse']}", + csac.location) + self.assertEqual(f"{TEST_SESSION[f'{circle_name} Start']}", + timezone.localtime(csac.due_date.start).strftime("%d.%m.%Y, %H:%M")) + self.assertEqual(f"{TEST_SESSION[f'{circle_name} Ende']}", + timezone.localtime(csac.due_date.end).strftime("%d.%m.%Y, %H:%M")) + self.assertEqual("", csac.trainer) + + def _check_assignments(self, circle_name: str, cs: CourseSession): + for assignment_slug in LP_DATA[circle_name]["de"]["assignments"]: + csa = CourseSessionAssignment.objects.get( + course_session=cs, + learning_content=LearningContentAssignment.objects.get( + slug=f"{self.course.slug}-lp-circle-{assignment_slug}" + ), + ) + if csa.learning_content.assignment_type == AssignmentType.PREP_ASSIGNMENT.value: + self.assertEqual(f"{TEST_SESSION[f'{circle_name} Start']}", + timezone.localtime(csa.submission_deadline.end).strftime("%d.%m.%Y, %H:%M")) diff --git a/server/vbv_lernwelt/importer/services.py b/server/vbv_lernwelt/importer/services.py index b773b221..5270f18a 100644 --- a/server/vbv_lernwelt/importer/services.py +++ b/server/vbv_lernwelt/importer/services.py @@ -2,27 +2,171 @@ from datetime import date, datetime, time from typing import Any, Dict, List import structlog +from django.utils import timezone from openpyxl.reader.excel import load_workbook +from vbv_lernwelt.assignment.models import AssignmentType from vbv_lernwelt.core.models import User from vbv_lernwelt.course.consts import COURSE_UK, COURSE_UK_FR, COURSE_UK_IT from vbv_lernwelt.course.models import Course, CourseSession, CourseSessionUser -from vbv_lernwelt.course_session.models import CourseSessionAttendanceCourse +from vbv_lernwelt.course_session.models import CourseSessionAttendanceCourse, CourseSessionAssignment from vbv_lernwelt.importer.utils import ( calc_header_tuple_list_from_pyxl_sheet, parse_circle_group_string, try_parse_datetime, ) -from vbv_lernwelt.learnpath.models import Circle, LearningContentAttendanceCourse +from vbv_lernwelt.learnpath.models import Circle, LearningContentAttendanceCourse, LearningContentAssignment logger = structlog.get_logger(__name__) -CIRCLE_NAMES = { - "Kickoff": {"de": "Kickoff", "fr": "Lancement", "it": "Introduzione"}, - "Basis": {"de": "Basis", "fr": "Base", "it": "Base"}, - "Fahrzeug": {"de": "Fahrzeug", "fr": "Véhicule", "it": "Veicolo"}, - "Haushalt Teil 1": {"de": "Haushalt Teil 1", "fr": "Habitat partie 1", "it": ""}, - "Haushalt Teil 2": {"de": "Haushalt Teil 2", "fr": "Haushalt Teil 2", "it": ""}, +# it's easier to define the data here, constructing slugs is error-prone and there are some exceptions +LP_DATA = { + "Kickoff": + { + "de": { + "title": "Kickoff", + "slug": "kickoff", + "presence_course": "kickoff-lc-präsenzkurs-kickoff", + "assignments": [ + "kickoff-lc-versicherungswirtschaft", + "kickoff-lc-redlichkeitserklärung", + "kickoff-lc-reflexion", + ] + }, + "fr": { + "title": "Lancement", + "slug": "lancement", + "presence_course": "lancement-lc-cours-de-présence-lancement", + "assignments": [ + "lancement-lc-secteur-de-lassurance", + "lancement-lc-redlichkeitserklärung", + "lancement-lc-réflexion", + ] + }, + "it": { + "title": "Introduzione", + "slug": "introduzione", + "presence_course": "introduzione-lc-corso-di-presenza-introduzione", + "assignments": [ + "introduzione-lc-settore-assicurativo", + "introduzione-lc-redlichkeitserklärung", + "introduzione-lc-riflessione", + ] + }, + }, + "Basis": + { + "de": { + "title": "Basis", + "slug": "basis", + "presence_course": "basis-lc-präsenzkurs-basis", + "assignments": [ + "basis-lc-vorbereitungsauftrag-circle-basis", + ] + }, + "fr": { + "title": "Base", + "slug": "base", + "presence_course": "base-lc-cours-de-présence-base", + "assignments": [ + "base-lc-mandats-préparatoires-circle-base", + ] + }, + "it": { + "title": "Base", + "slug": "base", + "presence_course": "base-lc-corso-di-presenza-base", + "assignments": [ + "base-lc-vorbereitungsauftrag-circle-basis", + ] + }, + }, + "Fahrzeug": + { + "de": { + "title": "Fahrzeug", + "slug": "fahrzeug", + "presence_course": "fahrzeug-lc-präsenzkurs-fahrzeug", + "assignments": [ + "fahrzeug-lc-überprüfen-einer-motorfahrzeug-versicherungspolice", + "fahrzeug-lc-fahrzeug-mein-erstes-auto", + ] + }, + "fr": { + "title": "Véhicule", + "slug": "véhicule", + "presence_course": "véhicule-lc-cours-de-présence-véhicule-à-moteur", + "assignments": [ + "véhicule-lc-vérification-dune-police-dassurance-de-véhicule-à-moteur", + "véhicule-lc-véhicule-à-moteur-ma-première-voiture", + ] + }, + "it": { + "title": "Veicolo", + "slug": "veicolo", + "presence_course": "veicolo-lc-corso-di-presenza-veicolo", + "assignments": [ + "veicolo-lc-verifica-di-una-polizza-di-assicurazione-veicoli-a-motore", + "veicolo-lc-veicolo-la-mia-prima-auto", + ] + }, + }, + "Haushalt Teil 1": + { + "de": { + "title": "Haushalt Teil 1", + "slug": "haushalt-teil-1", + "presence_course": "", + "assignments": [] + }, + "fr": { + "title": "Habitat partie 1", + "slug": "habitat-partie-1", + "presence_course": "", + "assignments": [] + }, + "it": {} + }, + "Haushalt Teil 2": { + "de": { + "title": "Haushalt Teil 2", + "slug": "haushalt-teil-2", + "presence_course": "", + "assignments": [] + }, + "fr": { + "title": "Habitat partie 2", + "slug": "habitat-partie-2", + "presence_course": "", + "assignments": [] + }, + "it": {} + }, +} + +# the request is always, so we cannot rely on the language in the request +TRANSLATIONS = { + "de": { + "raum": "Raum", + "standort": "Standort", + "adresse": "Adresse", + "start": "Start", + "ende": "Ende", + }, + "fr": { + "raum": "Raum", + "standort": "Standort", + "adresse": "Adresse", + "start": "Start", + "ende": "Ende", + }, + "it": { + "raum": "Raum", + "standort": "Standort", + "adresse": "Adresse", + "start": "Start", + "ende": "Ende", + } } T2L_IGNORE_FIELDS = ["Vorname", "Name", "Email", "Sprache", "Durchführungen"] @@ -85,6 +229,7 @@ def import_course_sessions_from_excel( ): workbook = load_workbook(filename=filename) sheet = workbook["Schulungen Durchführung"] + no_course = course is None tuple_list = calc_header_tuple_list_from_pyxl_sheet(sheet) @@ -97,7 +242,7 @@ def import_course_sessions_from_excel( if restrict_language and language != restrict_language: continue - if not course: + if no_course: course = get_uk_course(language) create_or_update_course_session( @@ -110,6 +255,7 @@ def create_or_update_course_session( data: Dict[str, Any], language: str, circle_keys=None, + lp_data=LP_DATA, ): """ :param data: the following keys are required to process the data: Generation, Region, Klasse @@ -148,25 +294,51 @@ def create_or_update_course_session( cs.save() for circle in circle_keys: - circle_name = CIRCLE_NAMES[circle][language] + circle_data = lp_data[circle][language] attendance_course_lc = LearningContentAttendanceCourse.objects.filter( - slug=f"{course.slug}-lp-circle-{circle_name.lower()}-lc-präsenzkurs-{circle_name.lower()}" + slug=f"{course.slug}-lp-circle-{circle_data['presence_course']}" ).first() + room = data[f"{circle} {TRANSLATIONS[language]['raum']}"] + place = data[f"{circle} {TRANSLATIONS[language]['standort']}"] + address = data[f"{circle} {TRANSLATIONS[language]['adresse']}"] + location = f"{room}, {place}, {address}" + start = try_parse_datetime(data[f"{circle} {TRANSLATIONS[language]['start']}"])[1] + end = try_parse_datetime(data[f"{circle} {TRANSLATIONS[language]['ende']}"])[1] + if attendance_course_lc: - # update existing data csa, _created = CourseSessionAttendanceCourse.objects.get_or_create( course_session=cs, learning_content=attendance_course_lc ) - location = f"{data[f'{circle} Raum']}, {data[f'{circle} Standort']}, {data[f'{circle} Adresse']}" csa.location = location + expert = CourseSessionUser.objects.filter(course_session_id=cs.id, + role=CourseSessionUser.Role.EXPERT).first() - csa.trainer = "" - csa.due_date.start = try_parse_datetime(data[f"{circle} Start"])[1] - csa.due_date.end = try_parse_datetime(data[f"{circle} Ende"])[1] + if expert: + csa.trainer = f"{expert.user.first_name} {expert.user.last_name}" + csa.due_date.start = timezone.make_aware(start) + csa.due_date.end = timezone.make_aware(end) csa.due_date.save() + csa.save() + + for assignment_slug in circle_data["assignments"]: + logger.debug( + "import", + slug=f"{course.slug}-lp-circle-{assignment_slug}" + ) + + csa, _created = CourseSessionAssignment.objects.get_or_create( + course_session=cs, + learning_content=LearningContentAssignment.objects.get( + slug=f"{course.slug}-lp-circle-{assignment_slug}" + ), + ) + + if csa.learning_content.assignment_type == AssignmentType.PREP_ASSIGNMENT.value: + csa.submission_deadline.end = timezone.make_aware(start) + csa.submission_deadline.save() return cs @@ -264,7 +436,7 @@ def create_or_update_trainer(course: Course, data: Dict[str, Any], language="de" # circle expert handling circle_data = parse_circle_group_string(data["Circles"]) for circle_key in circle_data: - circle_name = CIRCLE_NAMES[circle_key][language] + circle_name = CIRCLE_DATA[circle_key][language]["title"] # print(circle_name, groups) import_id = f"{data['Generation'].strip()} {group}"