From 9c1684bce3dcf6c7390cd809730cd5b751bee754 Mon Sep 17 00:00:00 2001 From: Daniel Egger Date: Wed, 31 May 2023 16:53:16 +0200 Subject: [PATCH] Add excel import code --- .gitignore | 8 +- server/requirements/requirements-dev.txt | 4 +- server/requirements/requirements.in | 1 + server/requirements/requirements.txt | 4 +- .../assignment/tests/test_assignment_api.py | 1 + .../assignment/tests/test_services.py | 1 + .../migrations/0002_alter_user_managers.py | 20 +++++ server/vbv_lernwelt/core/models.py | 2 - .../course/creators/test_course.py | 2 + .../commands/create_default_courses.py | 7 +- .../course/migrations/0004_import_fields.py | 39 +++++++++ server/vbv_lernwelt/course/models.py | 7 +- .../course/tests/test_completion_api.py | 1 + .../course/tests/test_course_session_api.py | 1 + .../course/tests/test_document_uploads.py | 1 + .../feedback/tests/test_feedback_api.py | 1 + server/vbv_lernwelt/importer/services.py | 32 ++++++++ .../Schulungen_Durchfuehrung_Trainer.xlsx | Bin 0 -> 15523 bytes .../tests/Schulungen_Teilnehmende.xlsx | Bin 0 -> 20072 bytes .../tests/test_import_course_sessions.py | 74 ++++++++++++++++++ .../vbv_lernwelt/learnpath/tests/test_api.py | 1 + 21 files changed, 194 insertions(+), 13 deletions(-) create mode 100644 server/vbv_lernwelt/core/migrations/0002_alter_user_managers.py create mode 100644 server/vbv_lernwelt/course/migrations/0004_import_fields.py create mode 100644 server/vbv_lernwelt/importer/tests/Schulungen_Durchfuehrung_Trainer.xlsx create mode 100644 server/vbv_lernwelt/importer/tests/Schulungen_Teilnehmende.xlsx create mode 100644 server/vbv_lernwelt/importer/tests/test_import_course_sessions.py diff --git a/.gitignore b/.gitignore index a511dff6..0daeaac9 100644 --- a/.gitignore +++ b/.gitignore @@ -61,8 +61,6 @@ target/ # pyenv .python-version - - # Environments .venv venv/ @@ -76,7 +74,6 @@ venv/ # mypy .mypy_cache/ - ### Node template # Logs logs @@ -159,10 +156,6 @@ typings/ # Local History for Visual Studio Code .history/ - - - - ### Windows template # Windows thumbnail cache files Thumbs.db @@ -272,6 +265,7 @@ tags ### Project template +.~lock.* .pytest_cache/ .ipython/ vendors.js diff --git a/server/requirements/requirements-dev.txt b/server/requirements/requirements-dev.txt index 62594c31..0753ad9e 100644 --- a/server/requirements/requirements-dev.txt +++ b/server/requirements/requirements-dev.txt @@ -319,7 +319,9 @@ mypy-extensions==0.4.3 nodeenv==1.6.0 # via pre-commit openpyxl==3.1.2 - # via wagtail + # via + # -r requirements.in + # wagtail packaging==21.3 # via # build diff --git a/server/requirements/requirements.in b/server/requirements/requirements.in index 7910d91e..dfaf5b87 100644 --- a/server/requirements/requirements.in +++ b/server/requirements/requirements.in @@ -47,3 +47,4 @@ azure-storage-blob azure-identity boto3 +openpyxl diff --git a/server/requirements/requirements.txt b/server/requirements/requirements.txt index b3015ddd..7c36d7b5 100644 --- a/server/requirements/requirements.txt +++ b/server/requirements/requirements.txt @@ -190,7 +190,9 @@ msal==1.22.0 msal-extensions==1.0.0 # via azure-identity openpyxl==3.1.2 - # via wagtail + # via + # -r requirements.in + # wagtail packaging==21.3 # via # marshmallow diff --git a/server/vbv_lernwelt/assignment/tests/test_assignment_api.py b/server/vbv_lernwelt/assignment/tests/test_assignment_api.py index 86fe4edf..704705b6 100644 --- a/server/vbv_lernwelt/assignment/tests/test_assignment_api.py +++ b/server/vbv_lernwelt/assignment/tests/test_assignment_api.py @@ -28,6 +28,7 @@ class AssignmentApiTestCase(APITestCase): self.cs = CourseSession.objects.create( course_id=COURSE_TEST_ID, title="Test Lehrgang Session", + import_id="Test Lehrgang Session", ) self.student = User.objects.get(username="student") self.student_csu = CourseSessionUser.objects.create( diff --git a/server/vbv_lernwelt/assignment/tests/test_services.py b/server/vbv_lernwelt/assignment/tests/test_services.py index 64e24d77..56bf9558 100644 --- a/server/vbv_lernwelt/assignment/tests/test_services.py +++ b/server/vbv_lernwelt/assignment/tests/test_services.py @@ -33,6 +33,7 @@ class UpdateAssignmentCompletionTestCase(TestCase): self.course_session = CourseSession.objects.create( course_id=COURSE_TEST_ID, title="Bern 2022 a", + import_id="Bern 2022 a", ) self.user = User.objects.get(username="student") self.trainer = User.objects.get(username="admin") diff --git a/server/vbv_lernwelt/core/migrations/0002_alter_user_managers.py b/server/vbv_lernwelt/core/migrations/0002_alter_user_managers.py new file mode 100644 index 00000000..f795d66e --- /dev/null +++ b/server/vbv_lernwelt/core/migrations/0002_alter_user_managers.py @@ -0,0 +1,20 @@ +# Generated by Django 3.2.13 on 2023-05-31 14:34 + +import django.contrib.auth.models +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('core', '0001_initial'), + ] + + operations = [ + migrations.AlterModelManagers( + name='user', + managers=[ + ('objects', django.contrib.auth.models.UserManager()), + ], + ), + ] diff --git a/server/vbv_lernwelt/core/models.py b/server/vbv_lernwelt/core/models.py index c6727611..bd687d08 100644 --- a/server/vbv_lernwelt/core/models.py +++ b/server/vbv_lernwelt/core/models.py @@ -2,8 +2,6 @@ from django.contrib.auth.models import AbstractUser from django.db import models from django.db.models import JSONField -from vbv_lernwelt.core.managers import UserManager - class User(AbstractUser): """ diff --git a/server/vbv_lernwelt/course/creators/test_course.py b/server/vbv_lernwelt/course/creators/test_course.py index e6681709..66b46e69 100644 --- a/server/vbv_lernwelt/course/creators/test_course.py +++ b/server/vbv_lernwelt/course/creators/test_course.py @@ -83,11 +83,13 @@ def create_test_course(include_uk=True, include_vv=True, with_sessions=False): cs_bern = CourseSession.objects.create( course_id=COURSE_TEST_ID, title="Test Bern 2022 a", + import_id="Test Bern 2022 a", id=TEST_COURSE_SESSION_BERN_ID, ) cs_zurich = CourseSession.objects.create( course_id=COURSE_TEST_ID, title="Test Zürich 2022 a", + import_id="Test Zürich 2022 a", id=TEST_COURSE_SESSION_ZURICH_ID, ) diff --git a/server/vbv_lernwelt/course/management/commands/create_default_courses.py b/server/vbv_lernwelt/course/management/commands/create_default_courses.py index f681bc5b..6eba6df4 100644 --- a/server/vbv_lernwelt/course/management/commands/create_default_courses.py +++ b/server/vbv_lernwelt/course/management/commands/create_default_courses.py @@ -117,6 +117,7 @@ def create_versicherungsvermittlerin_course(): cs = CourseSession.objects.create( course_id=COURSE_VERSICHERUNGSVERMITTLERIN_ID, title="Versicherungsvermittler/-in", + import_id="Versicherungsvermittler/-in", ) for user_data in default_users: CourseSessionUser.objects.create( @@ -185,6 +186,7 @@ def create_course_uk_de(): cs = CourseSession.objects.create( course_id=COURSE_UK, title="Bern 2023 a", + import_id="Bern 2023 a", attendance_courses=[ { "learningContentId": LearningContentAttendanceCourse.objects.get( @@ -280,6 +282,7 @@ def create_course_uk_de(): cs = CourseSession.objects.create( course_id=COURSE_UK, title="Zürich 2023 a", + import_id="Zürich 2023 a", ) # for user_data in default_users: # CourseSessionUser.objects.create( @@ -322,6 +325,7 @@ def create_course_uk_fr(): cs = CourseSession.objects.create( course_id=COURSE_UK_FR, title="Cours hors établissement année 1 - Région Fribourg", + import_id="Cours hors établissement année 1 - Région Fribourg", ) csu = CourseSessionUser.objects.create( @@ -441,7 +445,8 @@ def create_course_training_de(): cs = CourseSession.objects.create( course_id=COURSE_UK_TRAINING, - title="Demo-Tag", + title="myVBV Training", + import_id="myVBV Training", attendance_courses=[ { "learningContentId": LearningContentAttendanceCourse.objects.get( diff --git a/server/vbv_lernwelt/course/migrations/0004_import_fields.py b/server/vbv_lernwelt/course/migrations/0004_import_fields.py new file mode 100644 index 00000000..a9d05b81 --- /dev/null +++ b/server/vbv_lernwelt/course/migrations/0004_import_fields.py @@ -0,0 +1,39 @@ +# Generated by Django 3.2.13 on 2023-05-31 14:56 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('course', '0003_rename_attendance_days_coursesession_attendance_courses'), + ] + + operations = [ + migrations.AddField( + model_name='coursesession', + name='generation', + field=models.TextField(blank=True, default=''), + ), + migrations.AddField( + model_name='coursesession', + name='group', + field=models.TextField(blank=True, default=''), + ), + migrations.AddField( + model_name='coursesession', + name='import_id', + field=models.TextField(default='', unique=True), + preserve_default=False, + ), + migrations.AddField( + model_name='coursesession', + name='region', + field=models.TextField(blank=True, default=''), + ), + migrations.AlterField( + model_name='coursesession', + name='title', + field=models.TextField(unique=True), + ), + ] diff --git a/server/vbv_lernwelt/course/models.py b/server/vbv_lernwelt/course/models.py index f0bd86b2..4942e936 100644 --- a/server/vbv_lernwelt/course/models.py +++ b/server/vbv_lernwelt/course/models.py @@ -215,7 +215,12 @@ class CourseSession(models.Model): updated_at = models.DateTimeField(auto_now=True) course = models.ForeignKey("course.Course", on_delete=models.CASCADE) - title = models.TextField() + title = models.TextField(unique=True) + import_id = models.TextField(unique=True) + + generation = models.TextField(blank=True, default="") + region = models.TextField(blank=True, default="") + group = models.TextField(blank=True, default="") start_date = models.DateField(null=True, blank=True) end_date = models.DateField(null=True, blank=True) diff --git a/server/vbv_lernwelt/course/tests/test_completion_api.py b/server/vbv_lernwelt/course/tests/test_completion_api.py index 4c5db0af..1860e29a 100644 --- a/server/vbv_lernwelt/course/tests/test_completion_api.py +++ b/server/vbv_lernwelt/course/tests/test_completion_api.py @@ -22,6 +22,7 @@ class CourseCompletionApiTestCase(APITestCase): self.cs = CourseSession.objects.create( course_id=COURSE_TEST_ID, title="Test Lehrgang Session", + import_id="Test Lehrgang Session", ) csu = CourseSessionUser.objects.create( course_session=self.cs, diff --git a/server/vbv_lernwelt/course/tests/test_course_session_api.py b/server/vbv_lernwelt/course/tests/test_course_session_api.py index 17d3616c..423b8493 100644 --- a/server/vbv_lernwelt/course/tests/test_course_session_api.py +++ b/server/vbv_lernwelt/course/tests/test_course_session_api.py @@ -19,6 +19,7 @@ class CourseCompletionApiTestCase(APITestCase): self.course_session = CourseSession.objects.create( course_id=COURSE_TEST_ID, title="Test Lehrgang Session", + import_id="Test Lehrgang Session", ) self.client.login(username="student", password="test") diff --git a/server/vbv_lernwelt/course/tests/test_document_uploads.py b/server/vbv_lernwelt/course/tests/test_document_uploads.py index 4d08b6cf..2d69adb3 100644 --- a/server/vbv_lernwelt/course/tests/test_document_uploads.py +++ b/server/vbv_lernwelt/course/tests/test_document_uploads.py @@ -23,6 +23,7 @@ class DocumentUploadApiTestCase(APITestCase): self.course_session = CourseSession.objects.create( course_id=COURSE_TEST_ID, title="Test Lehrgang Session", + import_id="Test Lehrgang Session", ) csu = CourseSessionUser.objects.create( diff --git a/server/vbv_lernwelt/feedback/tests/test_feedback_api.py b/server/vbv_lernwelt/feedback/tests/test_feedback_api.py index 6ad2136c..22efad7a 100644 --- a/server/vbv_lernwelt/feedback/tests/test_feedback_api.py +++ b/server/vbv_lernwelt/feedback/tests/test_feedback_api.py @@ -23,6 +23,7 @@ class FeedbackApiBaseTestCase(APITestCase): self.course_session = CourseSession.objects.create( course_id=COURSE_TEST_ID, title="Test Lehrgang Session", + import_id="Test Lehrgang Session", ) csu = CourseSessionUser.objects.create( diff --git a/server/vbv_lernwelt/importer/services.py b/server/vbv_lernwelt/importer/services.py index 1b17bf6c..04c836ef 100644 --- a/server/vbv_lernwelt/importer/services.py +++ b/server/vbv_lernwelt/importer/services.py @@ -1,4 +1,7 @@ +from typing import Dict, Any + from vbv_lernwelt.core.models import User +from vbv_lernwelt.course.models import Course, CourseSession def create_or_update_user( @@ -28,3 +31,32 @@ def create_or_update_user( user.save() return user + + +def create_or_update_course_session(course: Course, data: Dict[str, Any]): + """ + :param data: the following keys are required to process the data: Generation, Region, Klasse + :return: + """ + + # TODO: validation + generation = str(data["Generation"]).strip() + region = data["Region"].strip() + group = data["Klasse"].strip() + import_id = data["ID"].strip() + + title = f"{region} {generation} {group}" + + cs, _created = CourseSession.objects.get_or_create( + import_id=import_id, course=course + ) + + cs.generation = generation + cs.region = region + cs.group = group + cs.import_id = import_id + + cs.additional_json_data["import_data"] = data + cs.save() + + return cs diff --git a/server/vbv_lernwelt/importer/tests/Schulungen_Durchfuehrung_Trainer.xlsx b/server/vbv_lernwelt/importer/tests/Schulungen_Durchfuehrung_Trainer.xlsx new file mode 100644 index 0000000000000000000000000000000000000000..9df30a5ff87fbaa9b5ed42b2b9103243cdf6fc93 GIT binary patch literal 15523 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 literal 0 HcmV?d00001 diff --git a/server/vbv_lernwelt/importer/tests/test_import_course_sessions.py b/server/vbv_lernwelt/importer/tests/test_import_course_sessions.py new file mode 100644 index 00000000..470928b1 --- /dev/null +++ b/server/vbv_lernwelt/importer/tests/test_import_course_sessions.py @@ -0,0 +1,74 @@ +import os + +from django.test import TestCase +from openpyxl.reader.excel import load_workbook + +from vbv_lernwelt.course.factories import CourseFactory +from vbv_lernwelt.importer.services import create_or_update_course_session + +test_dir = os.path.dirname(os.path.abspath(__file__)) + + +class ImportCourseSessionTestCase(TestCase): + def test_open_excel_file(self): + workbook = load_workbook( + filename=f"{test_dir}/Schulungen_Durchfuehrung_Trainer.xlsx" + ) + sheet = workbook["Schulungen Durchführung"] + + # Read the header row separately + header = [cell.value for cell in sheet[1]] + + # Loop through the remaining rows in the sheet + for row in sheet.iter_rows(min_row=2, values_only=True): + row_with_header = list(zip(header, row)) + print(row_with_header) + + def test_create_or_update_course_session(self): + row = [ + ("ID", "DE 2023 A"), + ("Generation", 2023), + ("Region", "Deutschschweiz"), + ("Klasse", "A"), + ("Fahrzeug Start", "06.06.2023, 13:30"), + ("Fahrzeug Ende", "06.06.2023, 15:00"), + ( + "Fahrzeug Raum", + "https://teams.microsoft.com/l/meetup-join/19%3ameeting_N2I5YzViZTQtYTM2Ny00OTYwLTgzNzAtYWI4OTQzODcxNTlj%40thread.v2/0?context=%7b%22Tid%22%3a%22fedd03c8-a756-4803-8f27-0db8f7c488f2%22%2c%22Oid%22%3a%22f92e6382-3884-4e71-a2fd-b305a75d9812%22%7d", + ), + ("Fahrzeug Standort", None), + ("Fahrzeug Adresse", None), + ] + + print(dict(row)) + + +class CreateOrUpdateCourseSessionTestCase(TestCase): + def setUp(self): + self.course = CourseFactory(title="myVBV Training") + + def test_create(self): + row = [ + ("ID", "DE 2023"), + ("Generation", 2023), + ("Region", "Deutschschweiz"), + ("Klasse", "A"), + ("Fahrzeug Start", "06.06.2023, 13:30"), + ("Fahrzeug Ende", "06.06.2023, 15:00"), + ( + "Fahrzeug Raum", + "https://teams.microsoft.com/l/meetup-join/19%3ameeting_N2I5YzViZTQtYTM2Ny00OTYwLTgzNzAtYWI4OTQzODcxNTlj%40thread.v2/0?context=%7b%22Tid%22%3a%22fedd03c8-a756-4803-8f27-0db8f7c488f2%22%2c%22Oid%22%3a%22f92e6382-3884-4e71-a2fd-b305a75d9812%22%7d", + ), + ("Fahrzeug Standort", None), + ("Fahrzeug Adresse", None), + ] + + data = dict(row) + + cs = create_or_update_course_session(self.course, data) + + self.assertEqual(cs.import_id, "DE 2023") + self.assertEqual(cs.title, "Deutschschweiz 2023 A") + self.assertEqual(cs.generation, "2023") + self.assertEqual(cs.region, "Deutschschweiz") + self.assertEqual(cs.group, "A") diff --git a/server/vbv_lernwelt/learnpath/tests/test_api.py b/server/vbv_lernwelt/learnpath/tests/test_api.py index da6c8cc3..46c45490 100644 --- a/server/vbv_lernwelt/learnpath/tests/test_api.py +++ b/server/vbv_lernwelt/learnpath/tests/test_api.py @@ -45,6 +45,7 @@ class TestRetrieveLearingPathContents(APITestCase): course_session = CourseSession.objects.create( course_id=COURSE_TEST_ID, title="Test Lehrgang Session", + import_id="Test Lehrgang Session", ) CourseSessionUser.objects.create( course_session=course_session,