From 61ce0897cf84cd76a8e02ebe2049582908a17d89 Mon Sep 17 00:00:00 2001 From: Daniel Egger Date: Wed, 31 May 2023 15:19:04 +0200 Subject: [PATCH 01/18] Refactor user creation code for sso and import --- server/vbv_lernwelt/core/managers.py | 25 ----- server/vbv_lernwelt/core/models.py | 2 - server/vbv_lernwelt/importer/__init__.py | 0 server/vbv_lernwelt/importer/admin.py | 3 + server/vbv_lernwelt/importer/apps.py | 6 ++ .../importer/migrations/__init__.py | 0 server/vbv_lernwelt/importer/models.py | 0 server/vbv_lernwelt/importer/services.py | 30 ++++++ .../importer/tests/test_services.py | 96 +++++++++++++++++++ server/vbv_lernwelt/importer/urls.py | 0 server/vbv_lernwelt/importer/views.py | 0 server/vbv_lernwelt/sso/tests.py | 3 - server/vbv_lernwelt/sso/tests/sso_token.json | 17 ++++ server/vbv_lernwelt/sso/views.py | 14 +-- 14 files changed, 160 insertions(+), 36 deletions(-) delete mode 100644 server/vbv_lernwelt/core/managers.py create mode 100644 server/vbv_lernwelt/importer/__init__.py create mode 100644 server/vbv_lernwelt/importer/admin.py create mode 100644 server/vbv_lernwelt/importer/apps.py create mode 100644 server/vbv_lernwelt/importer/migrations/__init__.py create mode 100644 server/vbv_lernwelt/importer/models.py create mode 100644 server/vbv_lernwelt/importer/services.py create mode 100644 server/vbv_lernwelt/importer/tests/test_services.py create mode 100644 server/vbv_lernwelt/importer/urls.py create mode 100644 server/vbv_lernwelt/importer/views.py delete mode 100644 server/vbv_lernwelt/sso/tests.py create mode 100644 server/vbv_lernwelt/sso/tests/sso_token.json diff --git a/server/vbv_lernwelt/core/managers.py b/server/vbv_lernwelt/core/managers.py deleted file mode 100644 index eead0bf9..00000000 --- a/server/vbv_lernwelt/core/managers.py +++ /dev/null @@ -1,25 +0,0 @@ -from django.contrib.auth.base_user import BaseUserManager -from django.contrib.auth.models import AbstractUser - - -class UserManager(BaseUserManager): - def create_or_update_by_email(self, user_dict: dict) -> tuple[AbstractUser, bool]: - # create or sync user with OpenID Data - user, created = self.model.objects.get_or_create( - sso_id=user_dict["oid"], - defaults={ - "email": user_dict["email"], - "username": user_dict["email"], - "first_name": user_dict["first_name"], - "last_name": user_dict["last_name"], - }, - ) - - if not created: - user.email = user_dict["email"] - user.username = user_dict["email"] - user.first_name = user_dict["first_name"] - user.last_name = user_dict["last_name"] - user.save() - - return user, created diff --git a/server/vbv_lernwelt/core/models.py b/server/vbv_lernwelt/core/models.py index 73e498f1..c6727611 100644 --- a/server/vbv_lernwelt/core/models.py +++ b/server/vbv_lernwelt/core/models.py @@ -27,8 +27,6 @@ class User(AbstractUser): additional_json_data = JSONField(default=dict, blank=True) language = models.CharField(max_length=2, choices=LANGUAGE_CHOICES, default="de") - objects = UserManager() - class SecurityRequestResponseLog(models.Model): label = models.CharField(max_length=255, blank=True, default="") diff --git a/server/vbv_lernwelt/importer/__init__.py b/server/vbv_lernwelt/importer/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/server/vbv_lernwelt/importer/admin.py b/server/vbv_lernwelt/importer/admin.py new file mode 100644 index 00000000..8c38f3f3 --- /dev/null +++ b/server/vbv_lernwelt/importer/admin.py @@ -0,0 +1,3 @@ +from django.contrib import admin + +# Register your models here. diff --git a/server/vbv_lernwelt/importer/apps.py b/server/vbv_lernwelt/importer/apps.py new file mode 100644 index 00000000..a0f916a8 --- /dev/null +++ b/server/vbv_lernwelt/importer/apps.py @@ -0,0 +1,6 @@ +from django.apps import AppConfig + + +class SsoConfig(AppConfig): + default_auto_field = "django.db.models.BigAutoField" + name = "vbv_lernwelt.importer" diff --git a/server/vbv_lernwelt/importer/migrations/__init__.py b/server/vbv_lernwelt/importer/migrations/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/server/vbv_lernwelt/importer/models.py b/server/vbv_lernwelt/importer/models.py new file mode 100644 index 00000000..e69de29b diff --git a/server/vbv_lernwelt/importer/services.py b/server/vbv_lernwelt/importer/services.py new file mode 100644 index 00000000..1b17bf6c --- /dev/null +++ b/server/vbv_lernwelt/importer/services.py @@ -0,0 +1,30 @@ +from vbv_lernwelt.core.models import User + + +def create_or_update_user( + email: str, first_name: str = "", last_name: str = "", sso_id: str = None +): + user = None + if sso_id: + user_qs = User.objects.filter(sso_id=sso_id) + if user_qs.exists(): + user = user_qs.first() + + if not user: + user_qs = User.objects.filter(email=email) + if user_qs.exists(): + user = user_qs.first() + + if not user: + # create user + user = User(sso_id=sso_id, email=email, username=email) + + user.email = email + user.sso_id = sso_id + user.username = email + user.first_name = first_name + user.last_name = last_name + user.set_unusable_password() + user.save() + + return user diff --git a/server/vbv_lernwelt/importer/tests/test_services.py b/server/vbv_lernwelt/importer/tests/test_services.py new file mode 100644 index 00000000..b7d76813 --- /dev/null +++ b/server/vbv_lernwelt/importer/tests/test_services.py @@ -0,0 +1,96 @@ +from django.test import TestCase + +from vbv_lernwelt.core.models import User +from vbv_lernwelt.importer.services import create_or_update_user + + +class CreateOrUpdateUserTestCase(TestCase): + def test_create_user(self): + u = create_or_update_user( + email="daniel@example.com", + first_name="Daniel", + last_name="Egger", + sso_id="12229620-81ea-483d-8d96-6ba8be5f9eb7", + ) + + saved_user = User.objects.get(id=u.id) + + self.assertEqual(saved_user.email, "daniel@example.com") + self.assertEqual(saved_user.username, "daniel@example.com") + self.assertEqual(saved_user.first_name, "Daniel") + self.assertEqual(saved_user.last_name, "Egger") + self.assertEqual(str(saved_user.sso_id), "12229620-81ea-483d-8d96-6ba8be5f9eb7") + + def test_update_existing_user_with_oid(self): + User.objects.create( + email="daniel@example.com", + username="daniel@example.com", + first_name="Daniel", + last_name="Egger", + ) + + create_or_update_user( + email="daniel@example.com", + first_name="Daniel", + last_name="Egger", + sso_id="12229620-81ea-483d-8d96-6ba8be5f9eb7", + ) + + self.assertEqual(1, User.objects.count()) + user = User.objects.first() + + self.assertEqual(user.email, "daniel@example.com") + self.assertEqual(user.username, "daniel@example.com") + self.assertEqual(user.first_name, "Daniel") + self.assertEqual(user.last_name, "Egger") + self.assertEqual(str(user.sso_id), "12229620-81ea-483d-8d96-6ba8be5f9eb7") + + def test_update_existing_user_with_new_last_name(self): + User.objects.create( + email="daniel@example.com", + username="daniel@example.com", + first_name="Daniel", + last_name="Egger", + sso_id="12229620-81ea-483d-8d96-6ba8be5f9eb7", + ) + + create_or_update_user( + email="daniel@example.com", + first_name="Daniel", + last_name="Marro", + sso_id="12229620-81ea-483d-8d96-6ba8be5f9eb7", + ) + + self.assertEqual(1, User.objects.count()) + user = User.objects.first() + + self.assertEqual(user.email, "daniel@example.com") + self.assertEqual(user.username, "daniel@example.com") + self.assertEqual(user.first_name, "Daniel") + self.assertEqual(user.last_name, "Marro") + self.assertEqual(str(user.sso_id), "12229620-81ea-483d-8d96-6ba8be5f9eb7") + + def test_update_existing_user_with_new_email(self): + User.objects.create( + email="daniel@example.com", + username="daniel@example.com", + first_name="Daniel", + last_name="Egger", + sso_id="12229620-81ea-483d-8d96-6ba8be5f9eb7", + ) + + create_or_update_user( + email="danu@example.com", + first_name="Daniel", + last_name="Egger", + sso_id="12229620-81ea-483d-8d96-6ba8be5f9eb7", + ) + + self.assertEqual(1, User.objects.count()) + user = User.objects.first() + + self.assertEqual(user.email, "danu@example.com") + self.assertEqual(user.username, "danu@example.com") + self.assertEqual(user.first_name, "Daniel") + self.assertEqual(user.last_name, "Egger") + self.assertEqual(str(user.sso_id), "12229620-81ea-483d-8d96-6ba8be5f9eb7") diff --git a/server/vbv_lernwelt/importer/urls.py b/server/vbv_lernwelt/importer/urls.py new file mode 100644 index 00000000..e69de29b diff --git a/server/vbv_lernwelt/importer/views.py b/server/vbv_lernwelt/importer/views.py new file mode 100644 index 00000000..e69de29b diff --git a/server/vbv_lernwelt/sso/tests.py b/server/vbv_lernwelt/sso/tests.py deleted file mode 100644 index 7ce503c2..00000000 --- a/server/vbv_lernwelt/sso/tests.py +++ /dev/null @@ -1,3 +0,0 @@ -from django.test import TestCase - -# Create your tests here. diff --git a/server/vbv_lernwelt/sso/tests/sso_token.json b/server/vbv_lernwelt/sso/tests/sso_token.json new file mode 100644 index 00000000..e9995eee --- /dev/null +++ b/server/vbv_lernwelt/sso/tests/sso_token.json @@ -0,0 +1,17 @@ +{ + "ver": "1.0", + "iss": "https://vbvtst.b2clogin.com/6967b19e-ec5c-4a46-bb16-01b0983da41b/v2.0/", + "sub": "f8c8e526-9fb1-4983-a5b7-4c069a83e317", + "aud": "8d32c131-0d60-4588-b01a-ae3435d44c23", + "exp": 1685538794, + "nonce": "mABq9hjYOMF34fCEi3VL", + "iat": 1685535194, + "auth_time": 1685535194, + "oid": "f8c8e526-9fb1-4983-a5b7-4c069a83e317", + "given_name": "Daniel", + "family_name": "Egger", + "name": "unknown", + "emails": ["daniel.egger+vbv-stage@gmail.com"], + "tfp": "B2C_1_SignUpAndSignIn_v3", + "nbf": 1685535194 +} diff --git a/server/vbv_lernwelt/sso/views.py b/server/vbv_lernwelt/sso/views.py index d5a4698d..83407746 100644 --- a/server/vbv_lernwelt/sso/views.py +++ b/server/vbv_lernwelt/sso/views.py @@ -1,10 +1,11 @@ import structlog as structlog from authlib.integrations.base_client import OAuthError from django.conf import settings -from django.contrib.auth import get_user_model, login as dj_login +from django.contrib.auth import login as dj_login from django.shortcuts import redirect from sentry_sdk import capture_exception +from vbv_lernwelt.importer.services import create_or_update_user from vbv_lernwelt.sso.client import oauth from vbv_lernwelt.sso.jwt import decode_jwt @@ -22,19 +23,20 @@ def login(request): def authorize(request): try: - logger.debug(request) + logger.debug(request, label="sso") token = getattr(oauth, settings.OAUTH["client_name"]).authorize_access_token( request ) - deocded_token = decode_jwt(token["id_token"]) + decoded_token = decode_jwt(token["id_token"]) + # logger.debug(label="sso", decoded_token=decoded_token) except OAuthError as e: - logger.error(f"OAuth error: {e}") + logger.error(e, exc_info=True, label="sso") if not settings.DEBUG: capture_exception(e) return redirect(f"/{OAUTH_FAIL_REDIRECT}?state=someerror") # to be defined - user_data = _user_data_from_token_data(deocded_token) - user, created = get_user_model().objects.create_or_update_by_email(user_data) + user_data = _user_data_from_token_data(decoded_token) + user = create_or_update_user(**user_data) dj_login(request, user) return redirect(f"/") From 9c1684bce3dcf6c7390cd809730cd5b751bee754 Mon Sep 17 00:00:00 2001 From: Daniel Egger Date: Wed, 31 May 2023 16:53:16 +0200 Subject: [PATCH 02/18] 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, From 73d44478db8a250ea57f18185e911d9c9f3456cd Mon Sep 17 00:00:00 2001 From: Daniel Egger Date: Wed, 31 May 2023 17:17:04 +0200 Subject: [PATCH 03/18] Add datetime parsing function from MyService --- server/requirements/requirements-dev.txt | 1 + server/requirements/requirements.in | 1 + server/requirements/requirements.txt | 1 + server/vbv_lernwelt/importer/services.py | 12 ++ .../tests/test_import_course_sessions.py | 2 +- .../vbv_lernwelt/importer/tests/test_utils.py | 182 ++++++++++++++++++ server/vbv_lernwelt/importer/utils.py | 90 +++++++++ 7 files changed, 288 insertions(+), 1 deletion(-) create mode 100644 server/vbv_lernwelt/importer/tests/test_utils.py create mode 100644 server/vbv_lernwelt/importer/utils.py diff --git a/server/requirements/requirements-dev.txt b/server/requirements/requirements-dev.txt index 0753ad9e..79b0692c 100644 --- a/server/requirements/requirements-dev.txt +++ b/server/requirements/requirements-dev.txt @@ -407,6 +407,7 @@ pytest-sugar==0.9.4 # via -r requirements-dev.in python-dateutil==2.8.2 # via + # -r requirements.in # botocore # faker python-dotenv==0.20.0 diff --git a/server/requirements/requirements.in b/server/requirements/requirements.in index dfaf5b87..6364d87b 100644 --- a/server/requirements/requirements.in +++ b/server/requirements/requirements.in @@ -37,6 +37,7 @@ sendgrid structlog python-json-logger concurrent-log-handler +python-dateutil wagtail>=4 wagtail-factories>=4 diff --git a/server/requirements/requirements.txt b/server/requirements/requirements.txt index 7c36d7b5..8e3e7d35 100644 --- a/server/requirements/requirements.txt +++ b/server/requirements/requirements.txt @@ -221,6 +221,7 @@ pyrsistent==0.18.1 # via jsonschema python-dateutil==2.8.2 # via + # -r requirements.in # botocore # faker python-dotenv==0.20.0 diff --git a/server/vbv_lernwelt/importer/services.py b/server/vbv_lernwelt/importer/services.py index 04c836ef..2109fed3 100644 --- a/server/vbv_lernwelt/importer/services.py +++ b/server/vbv_lernwelt/importer/services.py @@ -51,6 +51,7 @@ def create_or_update_course_session(course: Course, data: Dict[str, Any]): import_id=import_id, course=course ) + cs.title = title cs.generation = generation cs.region = region cs.group = group @@ -59,4 +60,15 @@ def create_or_update_course_session(course: Course, data: Dict[str, Any]): cs.additional_json_data["import_data"] = data cs.save() + """ + ("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), + """ + return cs diff --git a/server/vbv_lernwelt/importer/tests/test_import_course_sessions.py b/server/vbv_lernwelt/importer/tests/test_import_course_sessions.py index 470928b1..88133986 100644 --- a/server/vbv_lernwelt/importer/tests/test_import_course_sessions.py +++ b/server/vbv_lernwelt/importer/tests/test_import_course_sessions.py @@ -47,7 +47,7 @@ class CreateOrUpdateCourseSessionTestCase(TestCase): def setUp(self): self.course = CourseFactory(title="myVBV Training") - def test_create(self): + def test_create_course_session(self): row = [ ("ID", "DE 2023"), ("Generation", 2023), diff --git a/server/vbv_lernwelt/importer/tests/test_utils.py b/server/vbv_lernwelt/importer/tests/test_utils.py new file mode 100644 index 00000000..210d9ae0 --- /dev/null +++ b/server/vbv_lernwelt/importer/tests/test_utils.py @@ -0,0 +1,182 @@ +from datetime import date, datetime +from unittest import TestCase + +from vbv_lernwelt.importer.utils import ( + try_parse_date, + try_parse_int, + try_parse_datetime, +) + + +class TryParseDateTestCase(TestCase): + def test_wrongData_returnsFalseAndValue(self): + flag, value = try_parse_date("nonsense") + + self.assertFalse(flag) + self.assertEqual("nonsense", value) + + def test_isoDate_returnsCorrectDate(self): + flag, value = try_parse_date("2015-01-20") + + self.assertTrue(flag) + self.assertEqual(date(2015, 1, 20), value) + + def test_isoDateWithTime_returnsCorrectDate(self): + flag, value = try_parse_date("2015-01-20T15:21") + + self.assertTrue(flag) + self.assertEqual(date(2015, 1, 20), value) + + def test_isoDateWithTime2_returnsCorrectDate(self): + flag, value = try_parse_date("2018-05-03T00:00:00") + + self.assertTrue(flag) + self.assertEqual(date(2018, 5, 3), value) + + def test_swissDate_returnsCorrectDate(self): + flag, value = try_parse_date("01.05.2018") + + self.assertTrue(flag) + self.assertEqual(date(2018, 5, 1), value) + + def test_wrongIsoDate_returnsFalseAndValue(self): + flag, value = try_parse_date("2015-14-40") + + self.assertFalse(flag) + self.assertEqual("2015-14-40", value) + + def test_inputIsDate_returnsDate(self): + flag, value = try_parse_date(date(2016, 5, 1)) + + self.assertTrue(flag) + self.assertEqual(date(2016, 5, 1), value) + + def test_inputIsNumber_returnsFalseAndNumber(self): + flag, value = try_parse_date(123) + + self.assertFalse(flag) + self.assertEqual(123, value) + + def test_inputIsNumberString_returnsFalseAndString(self): + flag, value = try_parse_date("56") + + self.assertFalse(flag) + self.assertEqual("56", value) + + def test_inputIsFloatString_returnsFalseAndString(self): + flag, value = try_parse_date("3.14") + + self.assertFalse(flag) + self.assertEqual("3.14", value) + + def test_inputIsShortDateWithoutYear_returnsFalseAndString(self): + flag, value = try_parse_date("11-01") + + self.assertFalse(flag) + self.assertEqual("11-01", value) + + +class TryParseInt(TestCase): + def test_int_works(self): + flag, value = try_parse_int(123) + + self.assertTrue(flag) + self.assertEqual(123, value) + + def test_valid_string_works(self): + flag, value = try_parse_int("123") + + self.assertTrue(flag) + self.assertEqual(123, value) + + def test_invalid_string_breaks(self): + flag, value = try_parse_int("123qwer") + + self.assertFalse(flag) + self.assertEqual("123qwer", value) + + def test_invalid_string_returns_default(self): + flag, value = try_parse_int("123qwer", 0) + + self.assertFalse(flag) + self.assertEqual(0, value) + + +class TryParseDateTimeTestCase(TestCase): + def test_isoDateTime_returnsCorrectDateTime(self): + flag, value = try_parse_datetime("2016-05-31T10:00:00.000000") + + self.assertTrue(flag) + self.assertEqual(datetime(2016, 5, 31, 10, 0, 0), value) + + def test_isoDateTimeWithoutSeconds_returnsCorrectDateTime(self): + flag, value = try_parse_datetime("2016-05-02T10:00") + + self.assertTrue(flag) + self.assertEqual(datetime(2016, 5, 2, 10, 0, 0), value) + + def test_isoDateWithoutTime_returnTrueAndDatetimeWithZeroHour(self): + flag, value = try_parse_datetime("2016-05-31") + + self.assertTrue(flag) + self.assertEqual(datetime(2016, 5, 31), value) + + def test_isoDateWithSpaceBeforeTime_returnTrueAndDatetimeWithZeroHour(self): + flag, value = try_parse_datetime("2016-05-03 14:12") + + self.assertTrue(flag) + self.assertEqual(datetime(2016, 5, 3, 14, 12), value) + + def test_swissDateWithTime_returnTrueAndDatetime(self): + flag, value = try_parse_datetime("01.05.2018 15:20") + + self.assertTrue(flag) + self.assertEqual(datetime(2018, 5, 1, 15, 20), value) + + def test_swissDateWithTimeWithMultipleSpaces_returnTrueAndDatetime(self): + flag, value = try_parse_datetime("01 .05. 2018 15:20") + + self.assertTrue(flag) + self.assertEqual(datetime(2018, 5, 1, 15, 20), value) + + def test_withDateTimeInput_returnsTrueAndDateTime(self): + flag, value = try_parse_datetime(datetime(2016, 5, 31, 10, 0)) + + self.assertTrue(flag) + self.assertEqual(datetime(2016, 5, 31, 10, 0, 0), value) + + def test_inputIsNumber_returnsFalseAndNumber(self): + flag, value = try_parse_datetime(123) + + self.assertFalse(flag) + self.assertEqual(123, value) + + def test_inputIsNumberString_returnsFalseAndString(self): + flag, value = try_parse_datetime("56") + + self.assertFalse(flag) + self.assertEqual("56", value) + + def test_inputIsFloatString_returnsFalseAndString(self): + flag, value = try_parse_datetime("3.14") + + self.assertFalse(flag) + self.assertEqual("3.14", value) + + def test_inputIsShortDateWithoutYear_returnsFalseAndString(self): + flag, value = try_parse_date("11-01") + + self.assertFalse(flag) + self.assertEqual("11-01", value) + + def test_inputIsTimeString_returnsCurrentDateWithGivenTime(self): + flag, value = try_parse_datetime(" 15:00") + + self.assertFalse(flag) + self.assertEqual(" 15:00", value) + + def test_inputFromVbvExcel_returnsCurrentDateWithGivenTime(self): + flag, value = try_parse_datetime("09.06.2023, 13:30") + + self.assertTrue(flag) + self.assertEqual(datetime(2023, 6, 9, 13, 30, 0), value) diff --git a/server/vbv_lernwelt/importer/utils.py b/server/vbv_lernwelt/importer/utils.py new file mode 100644 index 00000000..b60d659f --- /dev/null +++ b/server/vbv_lernwelt/importer/utils.py @@ -0,0 +1,90 @@ +import datetime +import re +from typing import Any, Tuple, Union, Optional + +from dateutil.parser import parse +from six import string_types + + +def parse_formats(dt_str, fmt_strs, **parser_kwargs): + for fmt in fmt_strs: + try: + return datetime.datetime.strptime(dt_str, fmt) + except ValueError: + pass + + return parse(dt_str, **parser_kwargs) + + +def try_parse_int(x: Any, default: Optional[Any] = None) -> Tuple[bool, Any]: + try: + return True, int(x) + # pylint: disable=broad-except + except Exception: + if default is None: + return False, x + return False, default + + +def try_parse_date( + value: Union[str, datetime.date] +) -> Tuple[bool, Union[str, datetime.date]]: + if isinstance(value, datetime.date): + return True, value + elif isinstance(value, datetime.datetime): + return True, value.date() + elif isinstance(value, string_types): + if value.strip().replace(".", "", 1).isdigit(): + return False, value + + # date needs at least 3 parts + if len(re.split(r"[.-]", value)) < 3: + return False, value + + try: + date_with_time = parse_formats( + value, + [ + "%Y-%m-%dT%H:%M:%S", + "%Y-%m-%d", + "%d.%m.%Y", + ], + dayfirst=True, + ) + return True, date_with_time.date() + except ValueError: + return False, value + else: + return False, value + + +def try_parse_datetime( + value: Union[str, datetime.datetime] +) -> Tuple[bool, Union[str, datetime.datetime]]: + if isinstance(value, datetime.datetime): + return True, value + elif isinstance(value, string_types): + if value.strip().replace(".", "", 1).isdigit(): + return False, value + + # date needs at least 3 parts + if len(re.split(r"[.-]", value)) < 3: + return False, value + + try: + date_with_time = parse_formats( + value, + [ + "%Y-%m-%dT%H:%M:%S", + "%Y-%m-%dT%H:%M", + "%Y-%m-%d %H:%M:%S", + "%Y-%m-%d %H:%M", + "%d.%m.%Y, %H:%M", + ], + dayfirst=True, + ) + return True, date_with_time + except ValueError: + return False, value + else: + return False, value From e6c782fe5bfba3a6f81de56089caa8d6d7ae505e Mon Sep 17 00:00:00 2001 From: Daniel Egger Date: Wed, 31 May 2023 17:35:37 +0200 Subject: [PATCH 04/18] Parse attendance course data --- server/vbv_lernwelt/importer/services.py | 26 +++++++++++++++++-- .../tests/test_import_course_sessions.py | 22 +++++++++++++--- 2 files changed, 43 insertions(+), 5 deletions(-) diff --git a/server/vbv_lernwelt/importer/services.py b/server/vbv_lernwelt/importer/services.py index 2109fed3..d1fa0bab 100644 --- a/server/vbv_lernwelt/importer/services.py +++ b/server/vbv_lernwelt/importer/services.py @@ -2,6 +2,8 @@ from typing import Dict, Any from vbv_lernwelt.core.models import User from vbv_lernwelt.course.models import Course, CourseSession +from vbv_lernwelt.importer.utils import try_parse_datetime +from vbv_lernwelt.learnpath.models import LearningContentAttendanceCourse def create_or_update_user( @@ -33,12 +35,15 @@ def create_or_update_user( return user -def create_or_update_course_session(course: Course, data: Dict[str, Any]): +def create_or_update_course_session(course: Course, data: Dict[str, Any], circles=None): """ :param data: the following keys are required to process the data: Generation, Region, Klasse :return: """ + if circles is None: + circles = [] + # TODO: validation generation = str(data["Generation"]).strip() region = data["Region"].strip() @@ -51,13 +56,15 @@ def create_or_update_course_session(course: Course, data: Dict[str, Any]): import_id=import_id, course=course ) + cs.additional_json_data["import_data"] = data + cs.save() + cs.title = title cs.generation = generation cs.region = region cs.group = group cs.import_id = import_id - cs.additional_json_data["import_data"] = data cs.save() """ @@ -70,5 +77,20 @@ def create_or_update_course_session(course: Course, data: Dict[str, Any]): ("Fahrzeug Standort", None), ("Fahrzeug Adresse", None), """ + for circle in circles: + attendance_course_lp_qs = LearningContentAttendanceCourse.objects.filter( + slug=f"{course.slug}-lp-circle-{circle.lower()}-lc-präsenzkurs-{circle.lower()}" + ) + if attendance_course_lp_qs.exists(): + cs.attendance_courses.append( + { + "learningContentId": attendance_course_lp_qs.first().id, + "start": try_parse_datetime(data[f"{circle} Start"])[1].isoformat(), + "end": try_parse_datetime(data[f"{circle} Ende"])[1].isoformat(), + "location": data[f"{circle} Raum"], + "trainer": "", + } + ) + cs.save() return cs diff --git a/server/vbv_lernwelt/importer/tests/test_import_course_sessions.py b/server/vbv_lernwelt/importer/tests/test_import_course_sessions.py index 88133986..68a5223b 100644 --- a/server/vbv_lernwelt/importer/tests/test_import_course_sessions.py +++ b/server/vbv_lernwelt/importer/tests/test_import_course_sessions.py @@ -3,7 +3,7 @@ import os from django.test import TestCase from openpyxl.reader.excel import load_workbook -from vbv_lernwelt.course.factories import CourseFactory +from vbv_lernwelt.course.creators.test_course import create_test_course from vbv_lernwelt.importer.services import create_or_update_course_session test_dir = os.path.dirname(os.path.abspath(__file__)) @@ -45,7 +45,7 @@ class ImportCourseSessionTestCase(TestCase): class CreateOrUpdateCourseSessionTestCase(TestCase): def setUp(self): - self.course = CourseFactory(title="myVBV Training") + self.course = create_test_course(include_vv=False) def test_create_course_session(self): row = [ @@ -65,10 +65,26 @@ class CreateOrUpdateCourseSessionTestCase(TestCase): data = dict(row) - cs = create_or_update_course_session(self.course, data) + cs = create_or_update_course_session(self.course, data, circles=["Fahrzeug"]) 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") + + attendance_course = cs.attendance_courses[0] + attendance_course = { + k: v + for k, v in attendance_course.items() + if k not in ["learningContentId", "location"] + } + + self.assertDictEqual( + attendance_course, + { + "start": "2023-06-06T13:30:00", + "end": "2023-06-06T15:00:00", + "trainer": "", + }, + ) From 281521a8d0e602a2965d48ab5908c8034a70526b Mon Sep 17 00:00:00 2001 From: Daniel Egger Date: Wed, 31 May 2023 18:06:38 +0200 Subject: [PATCH 05/18] Import course sesssions from excel file --- .../assignment/tests/test_assignment_api.py | 1 - .../assignment/tests/test_services.py | 1 - .../course/creators/test_course.py | 2 - .../commands/create_default_courses.py | 43 +++++----- .../course/migrations/0004_import_fields.py | 5 +- server/vbv_lernwelt/course/models.py | 3 +- .../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 +++++--- .../tests/test_import_course_sessions.py | 82 ++++++++++++++----- .../vbv_lernwelt/learnpath/tests/test_api.py | 1 - 13 files changed, 104 insertions(+), 70 deletions(-) diff --git a/server/vbv_lernwelt/assignment/tests/test_assignment_api.py b/server/vbv_lernwelt/assignment/tests/test_assignment_api.py index 704705b6..86fe4edf 100644 --- a/server/vbv_lernwelt/assignment/tests/test_assignment_api.py +++ b/server/vbv_lernwelt/assignment/tests/test_assignment_api.py @@ -28,7 +28,6 @@ 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 56bf9558..64e24d77 100644 --- a/server/vbv_lernwelt/assignment/tests/test_services.py +++ b/server/vbv_lernwelt/assignment/tests/test_services.py @@ -33,7 +33,6 @@ 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/course/creators/test_course.py b/server/vbv_lernwelt/course/creators/test_course.py index 66b46e69..e6681709 100644 --- a/server/vbv_lernwelt/course/creators/test_course.py +++ b/server/vbv_lernwelt/course/creators/test_course.py @@ -83,13 +83,11 @@ 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 6eba6df4..12363a7e 100644 --- a/server/vbv_lernwelt/course/management/commands/create_default_courses.py +++ b/server/vbv_lernwelt/course/management/commands/create_default_courses.py @@ -1,3 +1,4 @@ +import os import random import djclick as click @@ -43,9 +44,15 @@ from vbv_lernwelt.course.creators.uk_training_course import ( from vbv_lernwelt.course.creators.versicherungsvermittlerin import ( create_versicherungsvermittlerin_with_categories, ) -from vbv_lernwelt.course.models import CoursePage, CourseSession, CourseSessionUser +from vbv_lernwelt.course.models import ( + CoursePage, + CourseSession, + CourseSessionUser, + Course, +) from vbv_lernwelt.course.services import mark_course_completion from vbv_lernwelt.feedback.creators.create_demo_feedback import create_feedback +from vbv_lernwelt.importer.services import import_course_sessions_from_excel from vbv_lernwelt.learnpath.create_vv_new_learning_path import ( create_vv_new_learning_path, ) @@ -117,7 +124,6 @@ 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( @@ -186,7 +192,6 @@ 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( @@ -282,7 +287,6 @@ 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( @@ -325,7 +329,6 @@ 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( @@ -443,22 +446,16 @@ def create_course_training_de(): create_uk_training_competence_profile(course_id=COURSE_UK_TRAINING) create_default_media_library(course_id=COURSE_UK_TRAINING) - cs = CourseSession.objects.create( - course_id=COURSE_UK_TRAINING, - title="myVBV Training", - import_id="myVBV Training", - attendance_courses=[ - { - "learningContentId": LearningContentAttendanceCourse.objects.get( - slug=f"{course.slug}-lp-circle-fahrzeug-lc-präsenzkurs-fahrzeug" - ).id, - "start": "2023-05-23T08:30:00+0200", - "end": "2023-05-23T17:00:00+0200", - "location": "Handelsschule KV Bern, Zimmer 123, Eigerstrasse 16, 3012 Bern", - "trainer": "Roland Grossenbacher, roland.grossenbacher@helvetia.ch", - } - ], - assignment_details_list=[ + current_dir = os.path.dirname(os.path.realpath(__file__)) + print(current_dir) + course = Course.objects.get(id=COURSE_UK_TRAINING) + import_course_sessions_from_excel( + course, + f"{current_dir}/../../../importer/tests/Schulungen_Durchfuehrung_Trainer.xlsx", + ) + + for cs in CourseSession.objects.filter(course_id=COURSE_UK_TRAINING): + cs.assignment_details_list = [ { "learningContentId": LearningContentAssignment.objects.get( slug=f"{course.slug}-lp-circle-fahrzeug-lc-überprüfen-einer-motorfahrzeug-versicherungspolice" @@ -473,5 +470,5 @@ def create_course_training_de(): "submissionDeadlineDateTimeUtc": "2023-06-13T19:00:00Z", "evaluationDeadlineDateTimeUtc": "2023-06-27T19:00:00Z", }, - ], - ) + ] + cs.save() diff --git a/server/vbv_lernwelt/course/migrations/0004_import_fields.py b/server/vbv_lernwelt/course/migrations/0004_import_fields.py index a9d05b81..15296edb 100644 --- a/server/vbv_lernwelt/course/migrations/0004_import_fields.py +++ b/server/vbv_lernwelt/course/migrations/0004_import_fields.py @@ -1,4 +1,4 @@ -# Generated by Django 3.2.13 on 2023-05-31 14:56 +# Generated by Django 3.2.13 on 2023-05-31 15:59 from django.db import migrations, models @@ -23,8 +23,7 @@ class Migration(migrations.Migration): migrations.AddField( model_name='coursesession', name='import_id', - field=models.TextField(default='', unique=True), - preserve_default=False, + field=models.TextField(blank=True, default=''), ), migrations.AddField( model_name='coursesession', diff --git a/server/vbv_lernwelt/course/models.py b/server/vbv_lernwelt/course/models.py index 4942e936..2a49e8d9 100644 --- a/server/vbv_lernwelt/course/models.py +++ b/server/vbv_lernwelt/course/models.py @@ -216,7 +216,8 @@ class CourseSession(models.Model): course = models.ForeignKey("course.Course", on_delete=models.CASCADE) title = models.TextField(unique=True) - import_id = models.TextField(unique=True) + + import_id = models.TextField(blank=True, default="") generation = models.TextField(blank=True, default="") region = models.TextField(blank=True, default="") diff --git a/server/vbv_lernwelt/course/tests/test_completion_api.py b/server/vbv_lernwelt/course/tests/test_completion_api.py index 1860e29a..4c5db0af 100644 --- a/server/vbv_lernwelt/course/tests/test_completion_api.py +++ b/server/vbv_lernwelt/course/tests/test_completion_api.py @@ -22,7 +22,6 @@ 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 423b8493..17d3616c 100644 --- a/server/vbv_lernwelt/course/tests/test_course_session_api.py +++ b/server/vbv_lernwelt/course/tests/test_course_session_api.py @@ -19,7 +19,6 @@ 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 2d69adb3..4d08b6cf 100644 --- a/server/vbv_lernwelt/course/tests/test_document_uploads.py +++ b/server/vbv_lernwelt/course/tests/test_document_uploads.py @@ -23,7 +23,6 @@ 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 22efad7a..6ad2136c 100644 --- a/server/vbv_lernwelt/feedback/tests/test_feedback_api.py +++ b/server/vbv_lernwelt/feedback/tests/test_feedback_api.py @@ -23,7 +23,6 @@ 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 d1fa0bab..a7ac207f 100644 --- a/server/vbv_lernwelt/importer/services.py +++ b/server/vbv_lernwelt/importer/services.py @@ -1,5 +1,7 @@ from typing import Dict, Any +from openpyxl.reader.excel import load_workbook + from vbv_lernwelt.core.models import User from vbv_lernwelt.course.models import Course, CourseSession from vbv_lernwelt.importer.utils import try_parse_datetime @@ -35,6 +37,19 @@ def create_or_update_user( return user +def import_course_sessions_from_excel(course: Course, filename: str): + workbook = load_workbook(filename=filename) + sheet = workbook["Schulungen Durchführung"] + + header = [cell.value for cell in sheet[1]] + + for row in sheet.iter_rows(min_row=2, values_only=True): + row_with_header = list(zip(header, row)) + cs = create_or_update_course_session( + course, dict(row_with_header), circles=["Fahrzeug"] + ) + + def create_or_update_course_session(course: Course, data: Dict[str, Any], circles=None): """ :param data: the following keys are required to process the data: Generation, Region, Klasse @@ -45,15 +60,16 @@ def create_or_update_course_session(course: Course, data: Dict[str, Any], circle circles = [] # TODO: validation - generation = str(data["Generation"]).strip() - region = data["Region"].strip() group = data["Klasse"].strip() import_id = data["ID"].strip() + generation = str(data["Generation"]).strip() + region = data["Region"].strip() + title = f"{region} {generation} {group}" cs, _created = CourseSession.objects.get_or_create( - import_id=import_id, course=course + import_id=import_id, group=group, course=course ) cs.additional_json_data["import_data"] = data @@ -67,16 +83,6 @@ def create_or_update_course_session(course: Course, data: Dict[str, Any], circle cs.save() - """ - ("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), - """ for circle in circles: attendance_course_lp_qs = LearningContentAttendanceCourse.objects.filter( slug=f"{course.slug}-lp-circle-{circle.lower()}-lc-präsenzkurs-{circle.lower()}" diff --git a/server/vbv_lernwelt/importer/tests/test_import_course_sessions.py b/server/vbv_lernwelt/importer/tests/test_import_course_sessions.py index 68a5223b..fb9c6dc2 100644 --- a/server/vbv_lernwelt/importer/tests/test_import_course_sessions.py +++ b/server/vbv_lernwelt/importer/tests/test_import_course_sessions.py @@ -4,43 +4,32 @@ from django.test import TestCase from openpyxl.reader.excel import load_workbook from vbv_lernwelt.course.creators.test_course import create_test_course +from vbv_lernwelt.course.models import CourseSession 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): + def setUp(self): + self.course = create_test_course(include_vv=False) + + def test_import_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) + cs = create_or_update_course_session( + self.course, dict(row_with_header), circles=["Fahrzeug"] + ) + print(cs.title) - 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)) + self.assertEqual(CourseSession.objects.count(), 6) class CreateOrUpdateCourseSessionTestCase(TestCase): @@ -88,3 +77,54 @@ class CreateOrUpdateCourseSessionTestCase(TestCase): "trainer": "", }, ) + + def test_update_course_session(self): + cs = CourseSession.objects.create( + course_id=self.course.id, + title="Deutschschweiz 2023 A", + import_id="DE 2023", + group="A", + ) + + 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, circles=["Fahrzeug"]) + + self.assertEqual(1, CourseSession.objects.count()) + + 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") + + attendance_course = cs.attendance_courses[0] + attendance_course = { + k: v + for k, v in attendance_course.items() + if k not in ["learningContentId", "location"] + } + + self.assertDictEqual( + attendance_course, + { + "start": "2023-06-06T13:30:00", + "end": "2023-06-06T15:00:00", + "trainer": "", + }, + ) diff --git a/server/vbv_lernwelt/learnpath/tests/test_api.py b/server/vbv_lernwelt/learnpath/tests/test_api.py index 46c45490..da6c8cc3 100644 --- a/server/vbv_lernwelt/learnpath/tests/test_api.py +++ b/server/vbv_lernwelt/learnpath/tests/test_api.py @@ -45,7 +45,6 @@ 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, From 2e7a069d0ab37c3fab6e2c759f2748985ffd7ca2 Mon Sep 17 00:00:00 2001 From: Daniel Egger Date: Wed, 31 May 2023 18:52:01 +0200 Subject: [PATCH 06/18] Add trainer import code --- server/vbv_lernwelt/importer/services.py | 57 ++++++++++++- .../Schulungen_Durchfuehrung_Trainer.xlsx | Bin 15523 -> 15535 bytes .../importer/tests/test_import_trainers.py | 76 ++++++++++++++++++ .../importer/tests/test_services.py | 1 - .../vbv_lernwelt/importer/tests/test_utils.py | 10 +++ server/vbv_lernwelt/importer/utils.py | 10 ++- 6 files changed, 148 insertions(+), 6 deletions(-) create mode 100644 server/vbv_lernwelt/importer/tests/test_import_trainers.py diff --git a/server/vbv_lernwelt/importer/services.py b/server/vbv_lernwelt/importer/services.py index a7ac207f..e9033e56 100644 --- a/server/vbv_lernwelt/importer/services.py +++ b/server/vbv_lernwelt/importer/services.py @@ -3,9 +3,9 @@ from typing import Dict, Any from openpyxl.reader.excel import load_workbook from vbv_lernwelt.core.models import User -from vbv_lernwelt.course.models import Course, CourseSession -from vbv_lernwelt.importer.utils import try_parse_datetime -from vbv_lernwelt.learnpath.models import LearningContentAttendanceCourse +from vbv_lernwelt.course.models import Course, CourseSession, CourseSessionUser +from vbv_lernwelt.importer.utils import try_parse_datetime, parse_circle_group_string +from vbv_lernwelt.learnpath.models import LearningContentAttendanceCourse, Circle def create_or_update_user( @@ -27,7 +27,7 @@ def create_or_update_user( user = User(sso_id=sso_id, email=email, username=email) user.email = email - user.sso_id = sso_id + user.sso_id = user.sso_id or sso_id user.username = email user.first_name = first_name user.last_name = last_name @@ -100,3 +100,52 @@ def create_or_update_course_session(course: Course, data: Dict[str, Any], circle cs.save() return cs + + +def create_or_update_trainer(course: Course, data: Dict[str, Any]): + user = create_or_update_user( + email=data["Email"], + first_name=data["Vorname"], + last_name=data["Name"], + ) + + # TODO: handle language + + import_id = data["Generation"].strip() + groups = [g.strip() for g in data["Klasse"].strip().split(",")] + + # general expert handling + for group in groups: + course_session = CourseSession.objects.filter( + import_id=import_id, group=group, course=course + ).first() + if course_session: + csu, _created = CourseSessionUser.objects.get_or_create( + course_session_id=course_session.id, user_id=user.id + ) + csu.role = CourseSessionUser.Role.EXPERT + csu.save() + + # circle expert handling + circle_data = parse_circle_group_string(data["Circles"]) + for circle_string in circle_data: + parts = circle_string.split("(", 1) + circle_name = parts[0].strip() + groups = [g.strip() for g in parts[1].rstrip(")").strip().split(",")] + + # print(circle_name, groups) + for group in groups: + course_session = CourseSession.objects.filter( + import_id=import_id, group=group, course=course + ).first() + circle = Circle.objects.filter( + slug=f"{course.slug}-lp-circle-{circle_name.lower()}" + ).first() + + if course_session and circle: + csu = CourseSessionUser.objects.filter( + course_session_id=course_session.id, user_id=user.id + ).first() + if csu: + csu.expert.add(circle) + csu.save() diff --git a/server/vbv_lernwelt/importer/tests/Schulungen_Durchfuehrung_Trainer.xlsx b/server/vbv_lernwelt/importer/tests/Schulungen_Durchfuehrung_Trainer.xlsx index 9df30a5ff87fbaa9b5ed42b2b9103243cdf6fc93..d40126c789472b43c0a2114142433fe0ba92dac9 100644 GIT binary patch delta 6118 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 List[str]: + # This regex pattern matches any comma that is not inside parentheses + pattern = r",(?![^()]*\))" + + # re.split() splits the string based on the pattern + return [x.strip() for x in re.split(pattern, value)] From f42aae19ee2cf55e4903e631ff1985bbaafb37d4 Mon Sep 17 00:00:00 2001 From: Daniel Egger Date: Wed, 31 May 2023 21:19:28 +0200 Subject: [PATCH 07/18] Import trainer from excel file --- .../commands/create_default_courses.py | 9 ++++- server/vbv_lernwelt/importer/services.py | 36 +++++++++++++++++++ .../importer/tests/test_import_trainers.py | 1 + server/vbv_lernwelt/notify/service.py | 12 ++++--- server/vbv_lernwelt/sso/jwt.py | 9 +++-- 5 files changed, 59 insertions(+), 8 deletions(-) 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 12363a7e..8b16eaf8 100644 --- a/server/vbv_lernwelt/course/management/commands/create_default_courses.py +++ b/server/vbv_lernwelt/course/management/commands/create_default_courses.py @@ -52,7 +52,10 @@ from vbv_lernwelt.course.models import ( ) from vbv_lernwelt.course.services import mark_course_completion from vbv_lernwelt.feedback.creators.create_demo_feedback import create_feedback -from vbv_lernwelt.importer.services import import_course_sessions_from_excel +from vbv_lernwelt.importer.services import ( + import_course_sessions_from_excel, + import_trainers_from_excel, +) from vbv_lernwelt.learnpath.create_vv_new_learning_path import ( create_vv_new_learning_path, ) @@ -453,6 +456,10 @@ def create_course_training_de(): course, f"{current_dir}/../../../importer/tests/Schulungen_Durchfuehrung_Trainer.xlsx", ) + import_trainers_from_excel( + course, + f"{current_dir}/../../../importer/tests/Schulungen_Durchfuehrung_Trainer.xlsx", + ) for cs in CourseSession.objects.filter(course_id=COURSE_UK_TRAINING): cs.assignment_details_list = [ diff --git a/server/vbv_lernwelt/importer/services.py b/server/vbv_lernwelt/importer/services.py index e9033e56..9c66fe85 100644 --- a/server/vbv_lernwelt/importer/services.py +++ b/server/vbv_lernwelt/importer/services.py @@ -1,5 +1,6 @@ from typing import Dict, Any +import structlog from openpyxl.reader.excel import load_workbook from vbv_lernwelt.core.models import User @@ -7,10 +8,20 @@ from vbv_lernwelt.course.models import Course, CourseSession, CourseSessionUser from vbv_lernwelt.importer.utils import try_parse_datetime, parse_circle_group_string from vbv_lernwelt.learnpath.models import LearningContentAttendanceCourse, Circle +logger = structlog.get_logger(__name__) + def create_or_update_user( email: str, first_name: str = "", last_name: str = "", sso_id: str = None ): + logger.debug( + "create_or_update_user", + email=email, + first_name=first_name, + last_name=last_name, + sso_id=sso_id, + label="import", + ) user = None if sso_id: user_qs = User.objects.filter(sso_id=sso_id) @@ -56,6 +67,13 @@ def create_or_update_course_session(course: Course, data: Dict[str, Any], circle :return: """ + logger.debug( + "create_or_update_course_session", + course=course.title, + data=data, + label="import", + ) + if circles is None: circles = [] @@ -102,7 +120,25 @@ def create_or_update_course_session(course: Course, data: Dict[str, Any], circle return cs +def import_trainers_from_excel(course: Course, filename: str): + workbook = load_workbook(filename=filename) + sheet = workbook["Schulungen Trainer"] + + header = [cell.value for cell in sheet[1]] + + for row in sheet.iter_rows(min_row=2, values_only=True): + row_with_header = list(zip(header, row)) + create_or_update_trainer(course, dict(row_with_header)) + + def create_or_update_trainer(course: Course, data: Dict[str, Any]): + logger.debug( + "create_or_update_trainer", + course=course.title, + data=data, + label="import", + ) + user = create_or_update_user( email=data["Email"], first_name=data["Vorname"], diff --git a/server/vbv_lernwelt/importer/tests/test_import_trainers.py b/server/vbv_lernwelt/importer/tests/test_import_trainers.py index f02c8b15..9f47838c 100644 --- a/server/vbv_lernwelt/importer/tests/test_import_trainers.py +++ b/server/vbv_lernwelt/importer/tests/test_import_trainers.py @@ -26,6 +26,7 @@ class ImportTrainerTestCase(TestCase): for row in sheet.iter_rows(min_row=2, values_only=True): row_with_header = list(zip(header, row)) print(row_with_header) + create_or_update_trainer(self.course, dict(row_with_header)) class CreateOrUpdateCourseSessionTestCase(TestCase): diff --git a/server/vbv_lernwelt/notify/service.py b/server/vbv_lernwelt/notify/service.py index e14decd9..ec384814 100644 --- a/server/vbv_lernwelt/notify/service.py +++ b/server/vbv_lernwelt/notify/service.py @@ -1,6 +1,6 @@ -import logging from typing import Optional +import structlog from notifications.signals import notify from sendgrid import Mail, SendGridAPIClient from storages.utils import setting @@ -8,7 +8,7 @@ from storages.utils import setting from vbv_lernwelt.core.models import User from vbv_lernwelt.notify.models import Notification, NotificationType -logger = logging.getLogger(__name__) +logger = structlog.get_logger(__name__) class EmailService: @@ -25,10 +25,14 @@ class EmailService: ) try: cls._sendgrid_client.send(message) - logger.info(f"Successfully sent email to {recipient}") + logger.info(f"Successfully sent email to {recipient}", label="email") return True except Exception as e: - logger.error(f"Failed to send email to {recipient}: {e}") + logger.error( + f"Failed to send email to {recipient}: {e}", + exc_info=True, + label="email", + ) return False diff --git a/server/vbv_lernwelt/sso/jwt.py b/server/vbv_lernwelt/sso/jwt.py index c31462a6..dee3c02a 100644 --- a/server/vbv_lernwelt/sso/jwt.py +++ b/server/vbv_lernwelt/sso/jwt.py @@ -1,8 +1,9 @@ import base64 import json -import logging -logger = logging.getLogger(__name__) +import structlog + +logger = structlog.get_logger(__name__) def decode_jwt(jwt: str): @@ -11,7 +12,9 @@ def decode_jwt(jwt: str): payload_bytes = base64.urlsafe_b64decode(_correct_padding(jwt_parts[1])) payload = json.loads(payload_bytes.decode("UTF-8")) except Exception as e: - logger.warning(f"OAuthToken error: Could not decode jwt: {e}") + logger.warning( + f"OAuthToken error: Could not decode jwt: {e}", exc_info=True, label="sso" + ) return None return payload From ab2e4c5df25d4ae4b8aae35f82d8983ddc3f8d73 Mon Sep 17 00:00:00 2001 From: Daniel Egger Date: Wed, 31 May 2023 21:27:34 +0200 Subject: [PATCH 08/18] Refactor admin --- server/vbv_lernwelt/course/admin.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/server/vbv_lernwelt/course/admin.py b/server/vbv_lernwelt/course/admin.py index 450d187b..d8c8d941 100644 --- a/server/vbv_lernwelt/course/admin.py +++ b/server/vbv_lernwelt/course/admin.py @@ -20,6 +20,7 @@ class CourseSessionAdmin(admin.ModelAdmin): list_display = [ "title", "course", + "import_id", "start_date", "end_date", "created_at", @@ -31,8 +32,9 @@ class CourseSessionAdmin(admin.ModelAdmin): class CourseSessionUserAdmin(admin.ModelAdmin): date_hierarchy = "created_at" list_display = [ - "course_session", "user", + "course_session", + "role", "created_at", "updated_at", ] @@ -43,12 +45,11 @@ class CourseSessionUserAdmin(admin.ModelAdmin): "course_session__title", ] list_filter = [ - "course_session__course", "course_session", ] fieldsets = [ - (None, {"fields": ("user", "course_session")}), + (None, {"fields": ("user", "course_session", "role")}), ( "Expert/Trainer", { From 5f534dee9f68874909193a13d0d704c428464712 Mon Sep 17 00:00:00 2001 From: Daniel Egger Date: Wed, 31 May 2023 21:45:51 +0200 Subject: [PATCH 09/18] Import without changing file --- server/vbv_lernwelt/importer/services.py | 4 +- .../Schulungen_Durchfuehrung_Trainer.xlsx | Bin 15535 -> 15566 bytes .../tests/test_import_course_sessions.py | 5 +- .../importer/tests/test_import_students.py | 76 ++++++++++++++++++ .../importer/tests/test_import_trainers.py | 8 +- 5 files changed, 86 insertions(+), 7 deletions(-) create mode 100644 server/vbv_lernwelt/importer/tests/test_import_students.py diff --git a/server/vbv_lernwelt/importer/services.py b/server/vbv_lernwelt/importer/services.py index 9c66fe85..6d4f9174 100644 --- a/server/vbv_lernwelt/importer/services.py +++ b/server/vbv_lernwelt/importer/services.py @@ -127,6 +127,8 @@ def import_trainers_from_excel(course: Course, filename: str): header = [cell.value for cell in sheet[1]] for row in sheet.iter_rows(min_row=2, values_only=True): + if all(cell_value is None for cell_value in row): + continue row_with_header = list(zip(header, row)) create_or_update_trainer(course, dict(row_with_header)) @@ -147,11 +149,11 @@ def create_or_update_trainer(course: Course, data: Dict[str, Any]): # TODO: handle language - import_id = data["Generation"].strip() groups = [g.strip() for g in data["Klasse"].strip().split(",")] # general expert handling for group in groups: + import_id = f"{data['Generation'].strip()} {group}" course_session = CourseSession.objects.filter( import_id=import_id, group=group, course=course ).first() diff --git a/server/vbv_lernwelt/importer/tests/Schulungen_Durchfuehrung_Trainer.xlsx b/server/vbv_lernwelt/importer/tests/Schulungen_Durchfuehrung_Trainer.xlsx index d40126c789472b43c0a2114142433fe0ba92dac9..05311f3ea7f10552709b049d95cdb96cbed67b9c 100644 GIT binary patch delta 6817 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 delta 6774 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 Date: Wed, 31 May 2023 21:54:51 +0200 Subject: [PATCH 10/18] Refactor import data loading --- server/vbv_lernwelt/importer/services.py | 26 ++++++++----------- .../tests/test_import_course_sessions.py | 14 +++++----- .../importer/tests/test_import_students.py | 11 +++----- .../importer/tests/test_import_trainers.py | 13 ++++------ .../vbv_lernwelt/importer/tests/test_utils.py | 2 +- server/vbv_lernwelt/importer/utils.py | 12 +++++++++ 6 files changed, 39 insertions(+), 39 deletions(-) diff --git a/server/vbv_lernwelt/importer/services.py b/server/vbv_lernwelt/importer/services.py index 6d4f9174..f7aa9987 100644 --- a/server/vbv_lernwelt/importer/services.py +++ b/server/vbv_lernwelt/importer/services.py @@ -5,7 +5,11 @@ from openpyxl.reader.excel import load_workbook from vbv_lernwelt.core.models import User from vbv_lernwelt.course.models import Course, CourseSession, CourseSessionUser -from vbv_lernwelt.importer.utils import try_parse_datetime, parse_circle_group_string +from vbv_lernwelt.importer.utils import ( + try_parse_datetime, + parse_circle_group_string, + calc_header_tuple_list_from_pyxl_sheet, +) from vbv_lernwelt.learnpath.models import LearningContentAttendanceCourse, Circle logger = structlog.get_logger(__name__) @@ -52,13 +56,9 @@ def import_course_sessions_from_excel(course: Course, filename: str): workbook = load_workbook(filename=filename) sheet = workbook["Schulungen Durchführung"] - header = [cell.value for cell in sheet[1]] - - for row in sheet.iter_rows(min_row=2, values_only=True): - row_with_header = list(zip(header, row)) - cs = create_or_update_course_session( - course, dict(row_with_header), circles=["Fahrzeug"] - ) + tuple_list = calc_header_tuple_list_from_pyxl_sheet(sheet) + for row in tuple_list: + create_or_update_course_session(course, dict(row), circles=["Fahrzeug"]) def create_or_update_course_session(course: Course, data: Dict[str, Any], circles=None): @@ -124,13 +124,9 @@ def import_trainers_from_excel(course: Course, filename: str): workbook = load_workbook(filename=filename) sheet = workbook["Schulungen Trainer"] - header = [cell.value for cell in sheet[1]] - - for row in sheet.iter_rows(min_row=2, values_only=True): - if all(cell_value is None for cell_value in row): - continue - row_with_header = list(zip(header, row)) - create_or_update_trainer(course, dict(row_with_header)) + tuple_list = calc_header_tuple_list_from_pyxl_sheet(sheet) + for row in tuple_list: + create_or_update_trainer(course, dict(row)) def create_or_update_trainer(course: Course, data: Dict[str, Any]): diff --git a/server/vbv_lernwelt/importer/tests/test_import_course_sessions.py b/server/vbv_lernwelt/importer/tests/test_import_course_sessions.py index 59ced640..adf1c715 100644 --- a/server/vbv_lernwelt/importer/tests/test_import_course_sessions.py +++ b/server/vbv_lernwelt/importer/tests/test_import_course_sessions.py @@ -6,6 +6,7 @@ from openpyxl.reader.excel import load_workbook from vbv_lernwelt.course.creators.test_course import create_test_course from vbv_lernwelt.course.models import CourseSession from vbv_lernwelt.importer.services import create_or_update_course_session +from vbv_lernwelt.importer.utils import calc_header_tuple_list_from_pyxl_sheet test_dir = os.path.dirname(os.path.abspath(__file__)) @@ -20,15 +21,12 @@ class ImportCourseSessionTestCase(TestCase): ) sheet = workbook["Schulungen Durchführung"] - header = [cell.value for cell in sheet[1]] - - for row in sheet.iter_rows(min_row=2, values_only=True): - row_with_header = list(zip(header, row)) - print(row_with_header) - cs = create_or_update_course_session( - self.course, dict(row_with_header), circles=["Fahrzeug"] + tuple_list = calc_header_tuple_list_from_pyxl_sheet(sheet) + for row in tuple_list: + print(row) + create_or_update_course_session( + self.course, dict(row), circles=["Fahrzeug"] ) - print(cs.title) self.assertEqual(CourseSession.objects.count(), 6) diff --git a/server/vbv_lernwelt/importer/tests/test_import_students.py b/server/vbv_lernwelt/importer/tests/test_import_students.py index 8e026808..cc30ef20 100644 --- a/server/vbv_lernwelt/importer/tests/test_import_students.py +++ b/server/vbv_lernwelt/importer/tests/test_import_students.py @@ -7,6 +7,7 @@ from openpyxl.reader.excel import load_workbook from vbv_lernwelt.course.creators.test_course import create_test_course from vbv_lernwelt.course.models import CourseSession, CourseSessionUser from vbv_lernwelt.importer.services import create_or_update_trainer +from vbv_lernwelt.importer.utils import calc_header_tuple_list_from_pyxl_sheet test_dir = os.path.dirname(os.path.abspath(__file__)) @@ -19,13 +20,9 @@ class ImportStudentsTestCase(TestCase): workbook = load_workbook(filename=f"{test_dir}/Schulungen_Teilnehmende.xlsx") sheet = workbook.active - header = [cell.value for cell in sheet[1]] - - for row in sheet.iter_rows(min_row=2, values_only=True): - if all(cell_value is None for cell_value in row): - continue - row_with_header = list(zip(header, row)) - print(row_with_header) + tuple_list = calc_header_tuple_list_from_pyxl_sheet(sheet) + for row in tuple_list: + print(row) class CreateOrUpdateCourseSessionTestCase(TestCase): diff --git a/server/vbv_lernwelt/importer/tests/test_import_trainers.py b/server/vbv_lernwelt/importer/tests/test_import_trainers.py index aec5a2f7..2d7478be 100644 --- a/server/vbv_lernwelt/importer/tests/test_import_trainers.py +++ b/server/vbv_lernwelt/importer/tests/test_import_trainers.py @@ -6,6 +6,7 @@ from openpyxl.reader.excel import load_workbook from vbv_lernwelt.course.creators.test_course import create_test_course from vbv_lernwelt.course.models import CourseSession, CourseSessionUser from vbv_lernwelt.importer.services import create_or_update_trainer +from vbv_lernwelt.importer.utils import calc_header_tuple_list_from_pyxl_sheet test_dir = os.path.dirname(os.path.abspath(__file__)) @@ -20,14 +21,10 @@ class ImportTrainerTestCase(TestCase): ) sheet = workbook["Schulungen Trainer"] - header = [cell.value for cell in sheet[1]] - - for row in sheet.iter_rows(min_row=2, values_only=True): - row_with_header = list(zip(header, row)) - if all(cell_value is None for cell_value in row): - continue - print(row_with_header) - create_or_update_trainer(self.course, dict(row_with_header)) + tuple_list = calc_header_tuple_list_from_pyxl_sheet(sheet) + for row in tuple_list: + print(row) + create_or_update_trainer(self.course, dict(row)) class CreateOrUpdateCourseSessionTestCase(TestCase): diff --git a/server/vbv_lernwelt/importer/tests/test_utils.py b/server/vbv_lernwelt/importer/tests/test_utils.py index 6f5259e1..69b9bdc6 100644 --- a/server/vbv_lernwelt/importer/tests/test_utils.py +++ b/server/vbv_lernwelt/importer/tests/test_utils.py @@ -183,7 +183,7 @@ class TryParseDateTimeTestCase(TestCase): self.assertEqual(datetime(2023, 6, 9, 13, 30, 0), value) -class ParceCircleGroupStringTestCase(TestCase): +class ParseCircleGroupStringTestCase(TestCase): def test_withMultipleCircles(self): value = "Fahrzeug (A, B), Reisen (A), KMU (B)" self.assertEqual( diff --git a/server/vbv_lernwelt/importer/utils.py b/server/vbv_lernwelt/importer/utils.py index 6700209e..a64841b0 100644 --- a/server/vbv_lernwelt/importer/utils.py +++ b/server/vbv_lernwelt/importer/utils.py @@ -96,3 +96,15 @@ def parse_circle_group_string(value: str) -> List[str]: # re.split() splits the string based on the pattern return [x.strip() for x in re.split(pattern, value)] + + +def calc_header_tuple_list_from_pyxl_sheet(sheet): + header = [cell.value for cell in sheet[1]] + + result = [] + for row in sheet.iter_rows(min_row=2, values_only=True): + if all(cell_value is None for cell_value in row): + continue + result.append(list(zip(header, row))) + + return result From 32233ec38e36f087af7fcbff400e77fb0e60c9c0 Mon Sep 17 00:00:00 2001 From: Daniel Egger Date: Wed, 31 May 2023 22:09:36 +0200 Subject: [PATCH 11/18] Import students from excel --- .../migrations/0002_alter_user_managers.py | 6 +- server/vbv_lernwelt/course/admin.py | 1 + .../commands/create_default_courses.py | 7 ++- .../course/migrations/0004_import_fields.py | 30 +++++----- server/vbv_lernwelt/importer/services.py | 46 +++++++++++++-- .../tests/Schulungen_Teilnehmende.xlsx | Bin 20072 -> 57299 bytes .../importer/tests/test_import_students.py | 53 +++++++++--------- .../importer/tests/test_import_trainers.py | 18 +++++- .../vbv_lernwelt/importer/tests/test_utils.py | 6 +- server/vbv_lernwelt/importer/utils.py | 2 +- 10 files changed, 115 insertions(+), 54 deletions(-) diff --git a/server/vbv_lernwelt/core/migrations/0002_alter_user_managers.py b/server/vbv_lernwelt/core/migrations/0002_alter_user_managers.py index f795d66e..3f6b16fe 100644 --- a/server/vbv_lernwelt/core/migrations/0002_alter_user_managers.py +++ b/server/vbv_lernwelt/core/migrations/0002_alter_user_managers.py @@ -7,14 +7,14 @@ from django.db import migrations class Migration(migrations.Migration): dependencies = [ - ('core', '0001_initial'), + ("core", "0001_initial"), ] operations = [ migrations.AlterModelManagers( - name='user', + name="user", managers=[ - ('objects', django.contrib.auth.models.UserManager()), + ("objects", django.contrib.auth.models.UserManager()), ], ), ] diff --git a/server/vbv_lernwelt/course/admin.py b/server/vbv_lernwelt/course/admin.py index d8c8d941..ab5d2bbe 100644 --- a/server/vbv_lernwelt/course/admin.py +++ b/server/vbv_lernwelt/course/admin.py @@ -46,6 +46,7 @@ class CourseSessionUserAdmin(admin.ModelAdmin): ] list_filter = [ "course_session", + "role", ] fieldsets = [ 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 8b16eaf8..755db50c 100644 --- a/server/vbv_lernwelt/course/management/commands/create_default_courses.py +++ b/server/vbv_lernwelt/course/management/commands/create_default_courses.py @@ -45,15 +45,16 @@ from vbv_lernwelt.course.creators.versicherungsvermittlerin import ( create_versicherungsvermittlerin_with_categories, ) from vbv_lernwelt.course.models import ( + Course, CoursePage, CourseSession, CourseSessionUser, - Course, ) from vbv_lernwelt.course.services import mark_course_completion from vbv_lernwelt.feedback.creators.create_demo_feedback import create_feedback from vbv_lernwelt.importer.services import ( import_course_sessions_from_excel, + import_students_from_excel, import_trainers_from_excel, ) from vbv_lernwelt.learnpath.create_vv_new_learning_path import ( @@ -460,6 +461,10 @@ def create_course_training_de(): course, f"{current_dir}/../../../importer/tests/Schulungen_Durchfuehrung_Trainer.xlsx", ) + import_students_from_excel( + course, + f"{current_dir}/../../../importer/tests/Schulungen_Teilnehmende.xlsx", + ) for cs in CourseSession.objects.filter(course_id=COURSE_UK_TRAINING): cs.assignment_details_list = [ diff --git a/server/vbv_lernwelt/course/migrations/0004_import_fields.py b/server/vbv_lernwelt/course/migrations/0004_import_fields.py index 15296edb..cc7f16c7 100644 --- a/server/vbv_lernwelt/course/migrations/0004_import_fields.py +++ b/server/vbv_lernwelt/course/migrations/0004_import_fields.py @@ -6,33 +6,33 @@ from django.db import migrations, models class Migration(migrations.Migration): dependencies = [ - ('course', '0003_rename_attendance_days_coursesession_attendance_courses'), + ("course", "0003_rename_attendance_days_coursesession_attendance_courses"), ] operations = [ migrations.AddField( - model_name='coursesession', - name='generation', - field=models.TextField(blank=True, default=''), + model_name="coursesession", + name="generation", + field=models.TextField(blank=True, default=""), ), migrations.AddField( - model_name='coursesession', - name='group', - field=models.TextField(blank=True, default=''), + model_name="coursesession", + name="group", + field=models.TextField(blank=True, default=""), ), migrations.AddField( - model_name='coursesession', - name='import_id', - field=models.TextField(blank=True, default=''), + model_name="coursesession", + name="import_id", + field=models.TextField(blank=True, default=""), ), migrations.AddField( - model_name='coursesession', - name='region', - field=models.TextField(blank=True, default=''), + model_name="coursesession", + name="region", + field=models.TextField(blank=True, default=""), ), migrations.AlterField( - model_name='coursesession', - name='title', + model_name="coursesession", + name="title", field=models.TextField(unique=True), ), ] diff --git a/server/vbv_lernwelt/importer/services.py b/server/vbv_lernwelt/importer/services.py index f7aa9987..0588e84c 100644 --- a/server/vbv_lernwelt/importer/services.py +++ b/server/vbv_lernwelt/importer/services.py @@ -1,4 +1,4 @@ -from typing import Dict, Any +from typing import Any, Dict import structlog from openpyxl.reader.excel import load_workbook @@ -6,11 +6,11 @@ from openpyxl.reader.excel import load_workbook from vbv_lernwelt.core.models import User from vbv_lernwelt.course.models import Course, CourseSession, CourseSessionUser from vbv_lernwelt.importer.utils import ( - try_parse_datetime, - parse_circle_group_string, calc_header_tuple_list_from_pyxl_sheet, + parse_circle_group_string, + try_parse_datetime, ) -from vbv_lernwelt.learnpath.models import LearningContentAttendanceCourse, Circle +from vbv_lernwelt.learnpath.models import Circle, LearningContentAttendanceCourse logger = structlog.get_logger(__name__) @@ -183,3 +183,41 @@ def create_or_update_trainer(course: Course, data: Dict[str, Any]): if csu: csu.expert.add(circle) csu.save() + + +def import_students_from_excel(course: Course, filename: str): + workbook = load_workbook(filename=filename) + sheet = workbook.active + + tuple_list = calc_header_tuple_list_from_pyxl_sheet(sheet) + for row in tuple_list: + create_or_update_student(course, dict(row)) + + +def create_or_update_student(course: Course, data: Dict[str, Any]): + logger.debug( + "create_or_update_student", + course=course.title, + data=data, + label="import", + ) + + user = create_or_update_user( + email=data["Email"], + first_name=data["Vorname"], + last_name=data["Name"], + ) + + # TODO: handle language + + # general expert handling + import_ids = [i.strip() for i in data["Durchführungen"].split(",")] + for import_id in import_ids: + course_session = CourseSession.objects.filter( + import_id=import_id, course=course + ).first() + if course_session: + csu, _created = CourseSessionUser.objects.get_or_create( + course_session_id=course_session.id, user_id=user.id + ) + csu.save() diff --git a/server/vbv_lernwelt/importer/tests/Schulungen_Teilnehmende.xlsx b/server/vbv_lernwelt/importer/tests/Schulungen_Teilnehmende.xlsx index 9efe313508c17bb1f63c7d394fc46ecca1c523d6..4674281cf4b468e9d56125fe291d0fbf78cc1611 100644 GIT binary patch literal 57299 zcmeIY2UL^Wx-}dk0S(e5h)RnfLQtAisiG9=9Ym^yCcSqsBA^?jN>>OifCxzML_|Qk z(n1jly%`9-hJ4X;#yxxQyU)IN-~S)uyZ`=^G2V=qne{wbYp(gswbmQ7)lQH@04GnL z1QceX3<1A-Q4#Mg+^ihj1^9owHx9~ZwFuHtU>Z>z8{cYFGNCVXsBSKYX~*7q5xB2? zKf}u6;*tMbu+A^PEYW#Z1nz!&K+8Xz%@!x_(sWC&?5f#2@<@fD6y(Fc@$h{<6QlC! zQxx)-jRocY>ywgLycD>#*-lm`mUwXb0;W1_|dl-_aO6%H{_PU%oe z=yud^i^GP8x|fv}OP$)~@xE~S^||#zj!RT%K!O9yo>bj2W2x3Ho|!zp{ryDLT8C(i z_7n1?MN22MMXy1)oC~D0{T+6fQRuT<}U)1p+HBHSv(IVBjIXWn2nL%5P@??gKU=$4*0N_Io0BHYP z8fl3%`Zx%9JG)ycLY?e%bjPa;f)&xU+S7qWHpHzdp;ipLwmP1TER}O_^?DAM9W+vUVri z#1=kLD}q`xz58yx?d01(A7dAYTWl&5Dx^ybT^nzBk~*I0bVDa+5IKJvIeJ(n^q$Iu z%#sh!a}pWU6MCQZ!-pfF?ktW_fGF-ymqK;Yowuvue)sR zOgzuKERd#hx$9azsI)}Ty4;0Lh}PF@l7`nh{&8`_hdUmMEZ{uNbog@RS49}|#&=>>rX9{c0nQgcW3i@HpkAmTgIn^*eNk4t^dT_; zz7fxV^1g;GdT+(Ac!%PWTh{f{PoNI2Q<3a^b!!Idx&!+H@+z_C7cb2Yer(957dl)I zx}Xt1Dsn+##YN;~3-%%Io=E!Xe#g}Q?{ON}w1VTxSeBop=gc^N`RwdQJGXjsT&CT@ zzgNDPl{=%qt~z^u_stzV@P~BDDC((>iPCX9@n_PZ{)cqjJ$xOk+QX&@N}YGfpbM7gf8C(kWGQDrMgl`;|KdgDfW`qj**5UBcB@C7#$7M7mAWNDFwlKZ7M} zEJ|i9WOx!#egCTPoK)b%`4;SupdLRJ(DF#_5PjjD(F2Xm9boL6_`8Ba7ki!<7bWQmra`n%eq1vB zCQ-kV=6I3GoVP2?ORv%&;n_Ad%pS?+tbOf?UMNRI{e)q-Te*bcN7p&jT={du*AE4l zBJ;|4#6rmiIk;N|A3)+tS|L|2!su-kP^D6LscaaYB;E|0k}xSKw+y}6^3<}nA>R=~ zhpnxRnPO1*RQcvzu)waOjzMeBMh!>*veH?n&_M438LX3~P7L-HjC)8D3CbwE%=(4< ziEwImNSK8L0}uVsiJM&M8kQ5EjfJ96YG_`wGBbI@J_aDSYR(}Q3tVhk;4)%|LukVF z&ez-bZgC*(81F21GPZB+g!Q;)CyFpqpALUFdp1|}6QgFXU14s$F`M8V)^|f|&Mtg2 zCcmntNB=d2Gi5h|@&TFq#@FTMElWQ^nPj$Zm}OWqmqBM{u#RmQQ>e9zifxt}aKQIA zJ4%qHOV97cys|V>?*03B!5T|~b5$1GTM-s3ulv|vAM{omvz3o&X4%Z|Gh6QDu!wxw zaVxylHx|Kne|e=dS6IyNSt)#ydrZi2YSi{{(t2ztu~}3dO+nfUEY| zO+M-AHpdL?PQmjE_5)sh%+3~I&DXE-4DD%M!p*->J@}rEsyF~)bv!L`GinQ`QjjyR z=2~<%d-4yS3RY;h;6{1gGmf9VGTKm$h>>TGmpu0o;D!3t- zIs4rM$b}cfuQ*d3n9~ZQdT_F7nr~g(S34i>g4PwHQ|mRt0DKzz zt!RnQ%6rSqf7|{#yU|P)d%*-htFNJNsk~i7{oE*r*qWc@;P>hp7JCP=M`_6#dd?Sx zTT3s#8|A4r7OvW9-)u8{(Vx;d>dVq3TQ3xXI6IMA6E8{{lu@= z!k>GKqF4?47QxeDYj{Sb-BuOK2aytuk@6+@J+d~P>CYd<;)W{;LHx`nV5{$+<3FP} zn{IC}i@aEV_VLzfiZ@y0q0OycCmZ4TRqWJx!=9{f7+bFBckE{h8yH3&GpPvNE#H(d z=D+_TM#?OwLshc?-BA~U)e-30P#Jhd*2Ab*WOAay>56u}`v=0i2WV;-V-^W}LxJJ3c7G%RBL`3Xqx>mrti-XjU`H)UGj}<{T{(wy_dAO55%c1>)vdV)z!X2B%fr zzV+tbeQidq5zkLtujGdUS(d^@azx{4A2P|h2`9E0YjYd%M{6By_GH5rW#csF?1kqEKh}pI0MHuEo|6mpE z*LGvwuwLV(Mcyw<+~*<7@KtZCwGY=gtgSiV46S#%EW`SrwX2r2CW#t6g|kqW<##{z z69Rn?^BGatx4J92*ft1$|6V_O@?_QXGAf%vrYqB7sT0VUh$&Bv1fCBc zYtG3(uWKpgyCRb*;S}J>(3<=JxS~(zdna{eETUUX2a%aD-76Au^|OcB&e`Yp9iBp& zM&xBsd&8Bvd}rp2Kv$#pfO{yol+osevAqVqGp$J<$@l`>e0(cYev~L) zuH9UMC_+2pUi9B@R3ZP|ZP?y(vwEoK;b!M#^K;XYkksrpC_xvtCf)k7L|^0<;&JZf zkPbl=9lG0G7sDGJI=rH}IMkHl8qcSPpJ;fW?v}ViOOX?e-6v zgv!$cKKG_S)cVMg7RzUr>lhdp*mPaWFQw<^%c{6GW62ILNEy0Nxr=T(GHJ{uuHf9b z(WWmCqv*Ah=(st8!)xbcKU;luARAG7@H8{XD+_`LHDEK6NAz2D5iET8)v8= z0e8IEwi$boc0I2tyN+Utwrul`fjWnxD_!4QBP92Up}9^EwbF1*RKRJ3sC}+kw1In3 z1C5T!cj3+x-5q%+eP_Uq6@1$$jE5nFsuKnER`y8t;T$aT)8?VNKFLJ!ZZa}#^QtN#&hOa-l zS!}lRJlg=>GUZE#Bdti&*d^Gbk^XO@A6M2yD2Usi7cWGtw=E73$L-~&waFC1CMT+n zpA?;t*|0Gmlj_bmlOmoib~igB>U3)7-Hj^T)0V48fC%fz7`?#o%bqIq>PwQV!BxCw zAi=Ikk^U@=i*8MZZcz>p+{;18lZzfM7H>p^?>=)>Hr%`QtYC9|9s2lFblJfZjdN+Y zr{Xte=>r-G=9}Swe7iz48Ev+ zRms*f5_xzz?Adyq@n0;i(D730yBJ1|IhrR&jVyh+If0WTRvmXqt|#3xh5pBoqrtAO zqSag*z2?Zk2hDfsL$V0DGIpAT=V^<%kpYH6%FfD$0nDr(_fv`=K33i=*a{_|JlFaO zEVsN6_=UZL&q2RBui94GXAHg{d>JQ#6YMWwrC2H`aKHn-Yi5~(UIzP~Mb9QkTc;M`2_4631+oef7 zkvD8I#;!fzq+`5e$4j>ST=ll2!N(qn@xd60#Ku6pjOJJ}P{w8a_4z*8hW**kgUvLN z2}Kv;Fa4qdP_4+-A4S|JHdM{p7YK%n3G#(iY;|RvSEn)DKQtN&TEjT@{GMi^R2xF z$q0cTR%)-2U*|$RxGnwZnojrY>AL6Q@?*AM$98D~g&<+OvaLC;T@@{+H92}WMcP6s zvN>IqRsF%D!j8g4F@X(E+nqWF-eYlJC=ofv$ucrGMAjo@@c}D!=tTHY3eqf zbvSc}i9JGz5@*}Cx@h9ex+Nc?#nHTX!SR-hUS#9Q&Gx_xU6&1CVx`#bk2t?GP^I#z zn9e)FfQyL%x5z$@g;K5p6>O$Gn2xCu3|SU7yXG+*(O)Pe(Hnrs>B8}m_v1?5?{BMH z042SPhn|G{(C=iSWv*(T@p+kOmrk>0QF|vKplE(`H+9yEQ<26FFxS*;yHRuQ64ii=3a^QGG(NtZezwPaC=5&l^BNq%ihRX=EeP_z(8(I6C}upK0e| zU<5bWhi06-Vkd3ZWbt{`8sRv0(FCb&erDv>k?9SnBkvKDe54;YmR& zya2i;q48vkj|!{_a|cityQdIO0EuQ1L1ZxVc%vDd>=w#!3P zPvUbNRI*P!pNmujDrLP}kS?GvQV*qF+_1rYQSG!Dl)mkmbYUNPwOTqz7-24GJWln@ z(SmHnoQ!bq@SY$z+Pvp1D?T6dNOKn3qq(r?&bjh%@HX&tCjPcTY|8sh*`{-MP-H*m zyzG9J_kTU|AExmywg1rh&oN_7Ox&}T&IV0d$lGNI-Gq`C^T13tzSnt43p3i69VR}i zV~!C0e0pNUEq7yb*9D~p^^GqW>ydtl=<`1KdSH5?hgOa?K+7+a-rM+HdV$eqio%KR zXUx(?HWuFO58ZbLqJ#IzBJ8_?@+V6Dsg^ju>9MN-sP{zAu8f*L!`;>KIY_tI=Z`vP z6IJt}+bDUaCYd*4|EqQYFP%IPChAoFDmJ?J9z^MCPOJ+|YjWZweXZA3o(vV}R>{h! z`i9OvVXP`6Mhjie*HTLBJXW|nzU>f{T!^zNe$y6?oi{`^mO5>{2=dM@a`LdZPw`Hy zy*uGFHe#XbzKqS~o0D+670d6;@e6rE4CJ%X^Vuh+l^3JWLAqP}SQAd#Ji0SP{pdns z=}q^F5AGpX&r%rk89dVIdId+*yfWRaE5Rt*&7jffLL=-iidxSV4dyIp*@aQPs?+Ng zQ&z=SM#uw7)A{&v80_zLgoQd7=k2H}6*4Al=OT3nGB5BLlT~c1b0Qxx-$~H03AHl2 z;HkErA8}mf9Q)gJ4Vk+A~?|p%CJe~Ub zim}Fc0PAT6zpWSrD|)jnlhgZ8cCPW%uTo?PMRA_&_f>|Qf2aJ|%C(nv=N+rxbo@JZJKUw6-Db{EP7UO}&A{rIOJ0j9 z6bp~#u0_TeXCGy!>U5hBw{PS)AqJJM z3I*lej4V{B6_tDUX;PQ6l?@W=EVNeQlrh_09J&S2I0X?w zZ^mH1+`V^+pH^mTXGtQWQSBu4BSEIS2%a0?PO*sS%(`P5=RR>nPFz~H2VKr$k-YUP zhpR(L8m1+SI{rvr)N6cVP!iVu;>#PpT=})C;N?*(NIKEaVmGww#82 zwG50clk%fuf{hn#B$O?GqFbC1YQqQCM7&)W^`RND$FY4Yspd~mJUc+67~k=f;S2e3 zEq%XZJFmZO9`?%zYP+w{Lw-oF-zQZgd_}Q0CH=*#Jh8eLqwGy_DCyBTh^*v8ZdE^d z^V8%Jb7j{QsDo=AC11E%A?GSM`&;ZjTwjt8W-1+EJHfkoZlHU>N~BrR43}a3ik?%` zD*M=69dUvxnIM*bkS_z~XOFuQUEHx6Xsh4V|J6mU?$Y*4M@ruYuV^>gkap)T)3ev< zl{9l=8C+-Uq*3DJevenjikg)Qi@H0J#dgHb23SE zI6w08o;o*`XEAeF)lnn&XeDUQe6ylo&sZNmK&DG3{ zFJ7EruDJH-EGM${G(XoKo4~HnTFDes&DDFa>l|Meo>}&nFjjW+oGaLLnygNM|B`%L z=yN=|7I!g^Fpc=R5OVF)D?$mXvX}iR_N^w5lx}-Cy*>!xtg34$y6^Szvqgk)K}VLH z*NVvGLWO&S+TzQ!oKsI2DZw0Pq_IH6Rhe7s%K=xS^1tHmNR+O)Xfbp-y$4me&vD!7 zdY-<=;tP9f$z_ysWjW!Eeuc{BCx5afRh15eeg;pIA{|fn-*nqt~;(z=%G__2gsK0>$$!7(d#C>~-x1b|{?v?7My4%^Qi7;`J6LRI1`D3oLaN?WoK%0Wab$F1%ZuefT*< zW4q&=M_QQB>0&e6QH3*n13LoU28XXd(R*0Pzy{VE(gHVU1#%<9FEJJ4G#yMFvvx(w zVL{KNqOrkxS9{A~1wqPgVLBP*cAI-SBbAFWf^6XQ`?tjwYM8Q?pDQ{usm2L)1+#AQ zH*g$Gr#HWqcrO2*ymQ5RBkNr6hIjw_;QGd{a@DKy`nAtJYcB1$Y&@7m#(rdZp3UUb z&QTB(V?h?F%5z#qT+Q^1AiSUTLn;;ZGjdP)UvBXLpch@PRNtUsaeB^?Tn@H2YRs}q zXw1EJY*$yjv_$oGmYI>$ZmFhmaOnm4+=6MJmQXZ8DWEs@-1_sTVTh=+=C>u)`<*?V z#;6dZePFEQ znb{)r;a+OHIDdX{2JMMYQj~?8?BWBtmHS;7o+hMZv{Lm~ED3j)i&-`f~pY z!%`CGQhA?*4QnE~odW` zlM>}gHww!7k0${DPoY0MIi34Yo1mW$5q|E3s3Rx9Ay7c*+Ad_e(YWWXqTJwD#38lU z6>F}Xu(5NIjfV|V3jwc6R%IWZaK86iqzfwfxtE{W@?2j%2qI7NE1-UY zNtD0dPZ4D2njM% z{OxY<=ic72x0L3IAYI$~L>_b^^8(WYRpXcriK!Eo*KsZG_Zs1Ses?});zQEr_xAH zf^h}sYZlFI$4BQ=NuzzqxCm@$=N~f)iMzg^PaNclKRwF)n;HE%TIwiys-H7@$T&Sw zVLQwb5eu3S9}(?Vy-i&ybnb9%bMM>Cp^xX`7bzdLg#^*l$W!ne7w*|u8B%0KlO?=! zP+q*iZh7 z{${B}wi;@SM&6?^pXGXp#n`sdhb3x8jp7T}tk-a}H?*7WQv-}oIO@iAuP)SU6J8c?`-h&LOR#)%*Qh36A^Q9l3so;!`73MfM~KjA?pB69 zn5i&fO}#GcXc%{ivAyV-TMcV_Vd|S}D^w{{y6(@YMS8-W|fW^J9vtEgNQV$@C{H^nwJPrg8f*i@@=DZQ9HW2P5H& z;uDIF@^!U?Aex19Xz&ce+VL=E-=JP3DyTeA-8JnUyK;5~?osua;wy#^@5bV-EWF^* z(aW)~F*yH(3ZDFj`O>Gtcwp}O=}XjePtJO)<~+PakpFu0^MGg@VVwKlQ<%vuXD1IU zCl3pKUl%KPvmcKGS`)jRh)mjmR`V)^vyI>23D>ElOJve)G$pP)HDpfG@%}Y>1h)HSospaG zN~_h16Y6`-&L?F9lWwPci8)vQV$;mDP%mbQVk%WxdDOSQJI3IwkYui|1zs~o`Z`cM z(I|1U#3(>DKMBHYxalxnWJ})rif2jFW0$kQ!Q4z_$h(RME09kb==48Z8@Us zPR5=z>Q;?O1mDil1GHSzH>Kx%S&ge4wMHBwnUH+5Wj*2B8KrZINWY|8rRU5php>A^ z<4cal*@9Fd3Ia-_49MN{5?fB4Ez1kUC(wPDL3!MzYr(}kIif%-+fAH-g8fA8qfc6C zC$scMr&O7nC1=#qj|^Q%uEZo{Npi*HB@N4kf>=9@SiOYKQ#T%f5ZCc&#eFY zf%W4=SDa~k0%B#~7kqw!57z9Tk|XAA5k&oU?R_NIO|HAKPia&N-W)8^-gxqe7p+W| zSM>IJrjC@%gEgESM}7i*M(f(~LU7RbT*>jybec2DXMcNbvi@M(dGc^5qJ*%wu_@4W zTs?`}U7Xru&uG{!bVeP1U+I%O+?YcP1QEUkH}35;686Ih3*LnFjkI9GPM?5zz;2WdyEo`^$a(fC|{;uU_BK3O<;Q7MPx6CmaxahdP{6YVuv5 zBj%_(awZ(LS~TI;hmSWmz3UqLQ2XEI%qND}OBT43&dD9ku2p+)%hxrQH0{lZ3!nn` z^VWj!?MW7mwG{yZa`>J+f{PqsGmV*GQ|+-mRak#eEnuEhw?6YCaJ${RHpwDzrw2vY z79h+FITGfEH1R7;7D4M1!GXcylZP#9O(=Z*PqX1M)39kZ4mx|lCWYB%}(10!)=5Y5ovKIvan4xY4JrXx>2%) zb{8j3a3Krcn<#X|?;Hj$yXbJuQu?s!T4ik z0zU5u*8`K=LwO6x`MGyS9|v_Z6O{7;+`qr>-Cs|04~nec$7gS&2u?v8^Di?NUuGN~ zEXNa$^~}v0uP@{U%Plwu`-x9(uWu4;Y)WMP2=TN*0dKzU*^D2iv8U(C9dU1#6Q`M| zP2YWX)WLkX&B!j{Xo@4q=Pm&sg$in|Szn2kQ%7&MEC+9#*}y9z>kl4LnrBoTxeOa= zRT`m=u!Y&dhb_**yJ@J#rf%^okIY@Sqp#dGcb#E3QZQdXmMdQLZKWjC+&_9Z9g^{O z54UzDX!8**p($28DE7tCp)n!Im)&gqQ^WL(J@bCcFSNmB)A2v=qJzq&2mWyvoVs%N z%X&+kR+GiXHmyH?YKR-1+7nP~g#J*%o)~a|Z#l>VeIqD>8!`m;#sc5mal^t#zFpnN zYN1X2dD;%0i*U!|R~m?0V3$_(70Zr3X<^wt#kzo)lC=Z96KchB;<7iAR{El7zm3g> zRD1inXB7sY-dqnyd$A@R3D+M_36!m{&#%;1ofj3Tb#)XT3-?7koBAKMhC5Ge(@r+6 zus3y;-<#6JcO!$hA{-8seU(d4n8DSP%f80@z^=q2lnl!n(pTd->6&x#=fn4z?jG5&V z+3X|yH?vuGU8kL)@g z&1)ABX118SxE``w;w(EH;1akg#1@R3+U^NH3<%h!Ik6GF;I}d`lhON$P;-dtCJ#sm zZ>24cet*|JkfP}W-BI)Q#l2;$9&Sh~ve|jKVLLPWx?UW!B5Hh29rp7Z|N)A+r+;i-i`}F-@Jq3*l&P zyP{fIRYJ{tUSBe8NngK_Gdr^6+r8AF^7$h8x4>6T2Uhb-h0=aOK7okg)D?zpMx|xi zlITGr=ZpekyZj&R(g<34%i?`BZ})3$h(q`t4-u&DOUEd!LzMW= z!KGqRH1|3?`sh~XO8nl7YOQJUZ}-j+)=i!HwrJvOhL21$Ef$L&H82DQoK{U*CR;S*lF*6!Z8U3m(PU+VjQEFe5M z@hQHKd&!g1zJG-#tZs$2t!8~EbG1$Cw5MG9N>P1A$)SxmVP@shQPQf)YV($!8qcL@xiepInS~L5J%ZUHFd&g@v8Z)=0 zIK8B=b+aQ}=;B(ho=W>-Gk0vNJ=(N>eoXYQKqkc#ZrDWoYSa3;F+Q|Ytyfp&Uw}SU za-yS5rQEQ8`DV4&tFO|(c)}f7#liij76WFA7r^xisusHAvx+%eava`~8Zmil3 z`yVjXTFC5gfPd4D2*0+U?5%WO7^`;2j@RyBO%J__1Eqco`;7m$+DX;!yf!`5EDq#G zH^tjec2us-j(NLbe+M_$zxuD+rP)vRRIbgBdAnolYj-A051onw#nDaw3FiMc*!$=0 z{A+g_O%KhB0|n4cN%oUnm1}cjyKdM&gmLG?{;a7Sx+%kcvafP&VQkkOdsw@JH$4n0 z4*WMTJG(KFfj@4_K08M1hNZ0C2{ApCFUCXBK{57npDO zuBkh=y>@5D^w6~!FM$qvWiQuT$-Xca{a?f9*u{H((^RDxe+eBFXD`=LnK3)2<%Z?1 z-HA6n{O`fIrRiTa?WxR|AJcNj=GE>Dm>${{<3-RxsrGXJT^PcCx$mDDY-)GPO%IKW z@mJA7iS}|`l^JtmeQwyl0$Wo4Y`70gybgzhjlS` z!Io-p2N;46RFw;;hAbumzv*I3z>n47E-)xRs4W*z3*kuu&g)`K!3Z_D2Moav8qNjO zLu`|PtGXC7@Ow454-6^*n$HC^Lh_SYnc!JtB*%Sb>gBtIE= zRu6L@Y^e^9gdv1LVn{$cWHA}YriXa|eyk3UfkCf&Y>X+S4Ej3fBH2D}Ca6$i~D0TYn?G@zFr#tDqkfH%Mp;-DQQU=p&J1`N`} zID-M2@b@sN1c)LJFa_ax1q{=}xPYOW@Q*Nr1n6uYUTn()sss3b@!53mH`c@0GBVLZSH zP52-TAqg_f1FS%7UjvKvFrMJ|n(z@AR0?FB2f#w|UjwW3FkWDcCj2W5AqDcv1FS(7 zUjv);Fy3H*790bEN`u1k0P7H*bYPnv#s>`5g3rPb(xAjVz$U~t9oVaf@de+|f-l0L zG9Y9gU>lO34jk6Q_<=38;NM{g8BkRoU>CBO4*aHv@drQFg5zLNSx{RZU?0Mh0i4&v z1b`7*@GTfZ7Brj(IE2_{09W-ef#COA@I4q*4m6(!z(evgfIE7aATUM?egs3vfp+o$ z1ju3r@K_HM3fRb?$0c6w1gn%Dw!|8aTH$h7IfK!Y- znZV2Xn5STbHXOo>xCt`M2T(EEW&-*3F`?l1+VFF{&|4tud;m3LekM>%9}@<~Xv0}} z5w}2I`2bqR#Y~{AJ|-Lt(1AmFp$ed|d;mQoPZm%~AM*?h)q!8)MJRw0^8pNuwpl<; zeav(44IMZSFH{kP%m*+s=4Sy7^)bJIEp^~mc@c`Bs(b(w<6;)jTptqweyjr*4d(-x8EvzHPWqS^;P*Ok30|l&Xg(jn%9x)G^wP&f zfiXI8Xvn01#s2K?2+KG09-49{eFM zLLHP?01#%hMFM;EF)6&#dZ9LNaO#Zd1s{YBe30Ni{i(kO4pmR%>|JpZpP6PB(_FWP zBo3Td0KviZOh>LK`~39 zaa0qZvPIH~zx>_*54%nZ{~ymfi8>N>{{yoi$%Z7G|Ld|D#H3u1)(f$Dgj1(ZFK8AP z^+8hfkw6k5Br%W*f#eJ%SNwae2xNT_oI2@^!~sbMq%t5`0m%vf*Ek_-dO?e@rw@{* zZ*VF_Qd%$EW*MhW0!f6B#6T(pk~5H8@o%*vh+tAEqz4|`T)?THNVj`0Ozndt>mz|A zLP%mD6#~f_NUr$zS`ir1?LG)=_#jX9iA<$jmL7O&lZsO(fh0mmVjvX)$r(tl__taS zDljQ$r3aqa+{LL=rQ0^1Gmu>IZ?z(LU{YwL2jDi6ICaYOrT4Tu-omaQ)d)R35y0G!}nS9VL`yBT4}0OX6liuDu{NFszJ22vrAoPp$uf2$P%ww`h(gY-t? zfTROb8IY`i0U>GQy)(8vxn+eq%IQ)ZAr5+NipkP3n13?x_ld#wok zvIDI)b$jYe8B5c`W&y~EK9BVj5=bJ1BnDC;keq?!ihr*aVMybUpLcz=Axwqe&jA#!zhcWM4}$%d?WOUkvr!fQqb zDH`WJt|}f3jC>8KVczibMt+ER^I`MN2Uz8R$XG?+N}56W+9vEZY`u(keOg6)&0R`s zqH4>{FG{$@!l1>%u%+6&fZ5wFnfJSz@LKyo3b>>YT=E8Nx2QQ25YN`+dDvFEtGRz!JS~qT&JP8UGWp&`;TQ` z?3>;`tL1i=a=UschLA>EpkLopQsU%=_p#k*y1FM%HRNkH2%k8u;qC9vv(LjPEB-?A zZ3MTSW9Nf8{vKLyJGmmAv2W>Dbd^1IY_>&^GfsZ%^X(#1EWT9n>i)n%AC$3GyVEm0#bbxw=CA$XK~rfnN*H+TSwy zCct9txIMF>YAx!)YW+Yfzo^xN@x-+k8s&1iqpe<_uqd^#xz07ahV&O!!Wm}YJ8uRo z)|ZZ=GzQpW#sq@RHzen`C*^nsB*ezIGOT%+F-!L|#B#^x0#u#9t;(`TZ`6QESH|xvOTr{iBL@3cjDMm#v>G z$VJ|YZ@;!3Sl)y$upgZh3S82V5*g47mNZGiG+=4NuQhqpuhetD!0#uQ*z_R_0#_H& zS5}Ub^BC_+E6tW(K}m}tKHe*bnXR>1Y`7fw6$&IxR$@`Bdb|4};&MBVhbs@@adFEv zEJ1Jg);E%74y@<40_DES9>=fG2WzWQp43`7Z*5Ty09cBG0NQF~KnMT=IC=6UpfD3< znBgKAMMDMv_>cntRDj>VEZnRd+y(f5y#GG1jG~_Em>B4G0sx@>O`P(NbPfXE&TjVi zot^FZeH>5M-Mr#28fciJVPl)M=!R{Wu4p#1e6sm>W_v3v;`Tg|^Mt>}K^x%rNtSQW%Kgb;OJASHS>)6fxU>gx_;<`J?Cy{=5Mc#K>6U#qgWx z$TPo+e&Ffu;q3TF3Dy)oFBB64nTQG4ev@E;nC~BPcN`u5Nv55Lm80;#<#KA!LF75N4c*3+gxS$tGxd>S3Bb$=lb_^>_$OZ|B)y&PodwF)O_~Wxqgi9XO`a!UlYZJ z&L)ymL%h!YhNL$3-zWGv+kcn^5_f$+pBR1OKh0GM^!HhQ67&0RD3KQ`Wf2qn=WZll z{C$F--T1>SUb3vWt`obUNYv|ZCG(i$_gQ|<_3yi38)2OL$K5y}mgK*t`3IdpyYYuv ztf4~Vg~V=%fPdeO=}Q2B$t`Cm4=X1R3w>V~D|fR$Qtq$)@wb!Gd-?ZP@Gle4Rs&J| Sn86=zstN)C+{b?W^?v~P>JZTY literal 20072 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 diff --git a/server/vbv_lernwelt/importer/tests/test_import_students.py b/server/vbv_lernwelt/importer/tests/test_import_students.py index cc30ef20..bdcf3912 100644 --- a/server/vbv_lernwelt/importer/tests/test_import_students.py +++ b/server/vbv_lernwelt/importer/tests/test_import_students.py @@ -6,7 +6,7 @@ from openpyxl.reader.excel import load_workbook from vbv_lernwelt.course.creators.test_course import create_test_course from vbv_lernwelt.course.models import CourseSession, CourseSessionUser -from vbv_lernwelt.importer.services import create_or_update_trainer +from vbv_lernwelt.importer.services import create_or_update_student from vbv_lernwelt.importer.utils import calc_header_tuple_list_from_pyxl_sheet test_dir = os.path.dirname(os.path.abspath(__file__)) @@ -15,6 +15,12 @@ test_dir = os.path.dirname(os.path.abspath(__file__)) class ImportStudentsTestCase(TestCase): def setUp(self): self.course = create_test_course(include_vv=False) + self.course_session_a = CourseSession.objects.create( + course=self.course, + title="Deutschschweiz 2023 A", + import_id="DE 2023 A", + group="A", + ) def test_import_excel_file(self): workbook = load_workbook(filename=f"{test_dir}/Schulungen_Teilnehmende.xlsx") @@ -23,51 +29,48 @@ class ImportStudentsTestCase(TestCase): tuple_list = calc_header_tuple_list_from_pyxl_sheet(sheet) for row in tuple_list: print(row) + create_or_update_student(self.course, dict(row)) + + self.assertEqual(CourseSessionUser.objects.count(), 26) -class CreateOrUpdateCourseSessionTestCase(TestCase): +class CreateOrUpdateStudentTestCase(TestCase): def setUp(self): self.course = create_test_course(include_vv=False) self.course_session_a = CourseSession.objects.create( course=self.course, title="Deutschschweiz 2023 A", - import_id="DE 2023", + import_id="DE 2023 A", group="A", ) - self.course_session_a = CourseSession.objects.create( - course=self.course, - title="Deutschschweiz 2023 B", - import_id="DE 2023", - group="B", - ) - def test_create_course_session(self): + def test_create_student(self): row = [ - ("Name", "Hänni"), - ("Vorname", "Fabienne"), - ("Email", "fabienne.haenni@vbv-afa.ch"), + ("Name", "Rascher"), + ("Vorname", "Barbara"), + ("Email", "barbara.rascher@vbv-afa.ch"), ("Sprache", "de"), - ("Generation", "DE 2023"), - ("Klasse", "A, B"), - ("Circles", "Fahrzeug (A, B), Reisen (A), KMU (B)"), - ("Status Referenten", "ok"), - (None, "Schulung D"), - ("Klasse foo", datetime(2023, 6, 6, 0, 0)), + ("Durchführungen", "DE 2023 A"), + ("Datum", datetime(2023, 9, 6, 0, 0)), + (None, "VBV"), + (None, None), + (None, None), + (None, None), + (None, None), ] - create_or_update_trainer(self.course, dict(row)) + create_or_update_student(self.course, dict(row)) self.assertEqual( CourseSessionUser.objects.filter( - user__email="fabienne.haenni@vbv-afa.ch" + user__email="barbara.rascher@vbv-afa.ch" ).count(), - 2, + 1, ) csu = CourseSessionUser.objects.get( course_session=self.course_session_a, ) - self.assertEqual(csu.role, CourseSessionUser.Role.EXPERT) - self.assertEqual(csu.user.email, "fabienne.haenni@vbv-afa.ch") - self.assertEqual(csu.expert.all().first().title, "Fahrzeug") + self.assertEqual(csu.role, CourseSessionUser.Role.MEMBER) + self.assertEqual(csu.user.email, "barbara.rascher@vbv-afa.ch") diff --git a/server/vbv_lernwelt/importer/tests/test_import_trainers.py b/server/vbv_lernwelt/importer/tests/test_import_trainers.py index 2d7478be..383319fa 100644 --- a/server/vbv_lernwelt/importer/tests/test_import_trainers.py +++ b/server/vbv_lernwelt/importer/tests/test_import_trainers.py @@ -14,6 +14,18 @@ test_dir = os.path.dirname(os.path.abspath(__file__)) class ImportTrainerTestCase(TestCase): def setUp(self): self.course = create_test_course(include_vv=False) + self.course_session_a = CourseSession.objects.create( + course=self.course, + title="Deutschschweiz 2023 A", + import_id="DE 2023 A", + group="A", + ) + self.course_session_a = CourseSession.objects.create( + course=self.course, + title="Deutschschweiz 2023 B", + import_id="DE 2023 B", + group="B", + ) def test_import_excel_file(self): workbook = load_workbook( @@ -26,8 +38,10 @@ class ImportTrainerTestCase(TestCase): print(row) create_or_update_trainer(self.course, dict(row)) + self.assertEqual(CourseSessionUser.objects.count(), 4) -class CreateOrUpdateCourseSessionTestCase(TestCase): + +class CreateOrUpdateTrainerTestCase(TestCase): def setUp(self): self.course = create_test_course(include_vv=False) self.course_session_a = CourseSession.objects.create( @@ -43,7 +57,7 @@ class CreateOrUpdateCourseSessionTestCase(TestCase): group="B", ) - def test_create_course_session(self): + def test_create_trainer(self): row = [ ("Name", "Hänni"), ("Vorname", "Fabienne"), diff --git a/server/vbv_lernwelt/importer/tests/test_utils.py b/server/vbv_lernwelt/importer/tests/test_utils.py index 69b9bdc6..69be4b32 100644 --- a/server/vbv_lernwelt/importer/tests/test_utils.py +++ b/server/vbv_lernwelt/importer/tests/test_utils.py @@ -2,10 +2,10 @@ from datetime import date, datetime from unittest import TestCase from vbv_lernwelt.importer.utils import ( - try_parse_date, - try_parse_int, - try_parse_datetime, parse_circle_group_string, + try_parse_date, + try_parse_datetime, + try_parse_int, ) diff --git a/server/vbv_lernwelt/importer/utils.py b/server/vbv_lernwelt/importer/utils.py index a64841b0..d61405d2 100644 --- a/server/vbv_lernwelt/importer/utils.py +++ b/server/vbv_lernwelt/importer/utils.py @@ -1,6 +1,6 @@ import datetime import re -from typing import Any, Tuple, Union, Optional, List +from typing import Any, List, Optional, Tuple, Union from dateutil.parser import parse from six import string_types From 631893c60ff07edf95de7fefcf95666d9a96fb46 Mon Sep 17 00:00:00 2001 From: Daniel Egger Date: Wed, 31 May 2023 22:35:34 +0200 Subject: [PATCH 12/18] Fix in sso login --- server/vbv_lernwelt/sso/views.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/server/vbv_lernwelt/sso/views.py b/server/vbv_lernwelt/sso/views.py index 83407746..ba0c208c 100644 --- a/server/vbv_lernwelt/sso/views.py +++ b/server/vbv_lernwelt/sso/views.py @@ -47,7 +47,6 @@ def _user_data_from_token_data(token: dict) -> dict: return { "first_name": token.get("given_name", ""), "last_name": token.get("family_name", ""), - "username": token.get("preferred_username", first_email), "email": first_email, - "oid": token.get("oid"), + "sso_id": token.get("oid"), } From 183135bcb740c273e3201ee6d0e3aa64a86a83ca Mon Sep 17 00:00:00 2001 From: Daniel Egger Date: Fri, 2 Jun 2023 14:35:08 +0200 Subject: [PATCH 13/18] VBV-409: onboarding anpassungen --- .../migrations/0003_alter_user_avatar_url.py | 18 ++++++++++++++++++ server/vbv_lernwelt/core/models.py | 4 +++- server/vbv_lernwelt/importer/services.py | 4 ++-- .../tests/Schulungen_Teilnehmende.xlsx | Bin 57299 -> 57558 bytes .../importer/tests/test_services.py | 1 - server/vbv_lernwelt/sso/views.py | 7 ++++++- .../static/avatars/myvbv-default-avatar.png | Bin 0 -> 39577 bytes 7 files changed, 29 insertions(+), 5 deletions(-) create mode 100644 server/vbv_lernwelt/core/migrations/0003_alter_user_avatar_url.py create mode 100644 server/vbv_lernwelt/static/avatars/myvbv-default-avatar.png diff --git a/server/vbv_lernwelt/core/migrations/0003_alter_user_avatar_url.py b/server/vbv_lernwelt/core/migrations/0003_alter_user_avatar_url.py new file mode 100644 index 00000000..6df2d2cc --- /dev/null +++ b/server/vbv_lernwelt/core/migrations/0003_alter_user_avatar_url.py @@ -0,0 +1,18 @@ +# Generated by Django 3.2.13 on 2023-06-02 12:32 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('core', '0002_alter_user_managers'), + ] + + operations = [ + migrations.AlterField( + model_name='user', + name='avatar_url', + field=models.CharField(blank=True, default='/static/avatars/myvbv-default-avatar.png', max_length=254), + ), + ] diff --git a/server/vbv_lernwelt/core/models.py b/server/vbv_lernwelt/core/models.py index bd687d08..c1a09d96 100644 --- a/server/vbv_lernwelt/core/models.py +++ b/server/vbv_lernwelt/core/models.py @@ -17,7 +17,9 @@ class User(AbstractUser): # FIXME: look into it... # objects = UserManager() - avatar_url = models.CharField(max_length=254, blank=True, default="") + avatar_url = models.CharField( + max_length=254, blank=True, default="/static/avatars/myvbv-default-avatar.png" + ) email = models.EmailField("email address", unique=True) sso_id = models.UUIDField( "SSO subscriber ID", unique=True, null=True, blank=True, default=None diff --git a/server/vbv_lernwelt/importer/services.py b/server/vbv_lernwelt/importer/services.py index 0588e84c..d75fe9a0 100644 --- a/server/vbv_lernwelt/importer/services.py +++ b/server/vbv_lernwelt/importer/services.py @@ -43,9 +43,9 @@ def create_or_update_user( user.email = email user.sso_id = user.sso_id or sso_id + user.first_name = first_name or user.first_name + user.last_name = last_name or user.last_name user.username = email - user.first_name = first_name - user.last_name = last_name user.set_unusable_password() user.save() diff --git a/server/vbv_lernwelt/importer/tests/Schulungen_Teilnehmende.xlsx b/server/vbv_lernwelt/importer/tests/Schulungen_Teilnehmende.xlsx index 4674281cf4b468e9d56125fe291d0fbf78cc1611..f2f573583f24627772d6f56ae4f9917bc35f8f4d 100644 GIT binary patch delta 16178 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 r=wSb~M_n6{ljyp?g7`m4@G}Qi{!_O)SnA diff --git a/server/vbv_lernwelt/importer/tests/test_services.py b/server/vbv_lernwelt/importer/tests/test_services.py index 3402ef88..c39aded5 100644 --- a/server/vbv_lernwelt/importer/tests/test_services.py +++ b/server/vbv_lernwelt/importer/tests/test_services.py @@ -31,7 +31,6 @@ class CreateOrUpdateUserTestCase(TestCase): create_or_update_user( email="daniel@example.com", - first_name="Daniel", last_name="Egger", sso_id="12229620-81ea-483d-8d96-6ba8be5f9eb7", ) diff --git a/server/vbv_lernwelt/sso/views.py b/server/vbv_lernwelt/sso/views.py index ba0c208c..98aa587e 100644 --- a/server/vbv_lernwelt/sso/views.py +++ b/server/vbv_lernwelt/sso/views.py @@ -36,7 +36,12 @@ def authorize(request): return redirect(f"/{OAUTH_FAIL_REDIRECT}?state=someerror") # to be defined user_data = _user_data_from_token_data(decoded_token) - user = create_or_update_user(**user_data) + user = create_or_update_user( + email=user_data.get("email"), + sso_id=user_data.get("sso_id"), + first_name=user_data.get("first_name", ""), + last_name=user_data.get("last_name", ""), + ) dj_login(request, user) return redirect(f"/") diff --git a/server/vbv_lernwelt/static/avatars/myvbv-default-avatar.png b/server/vbv_lernwelt/static/avatars/myvbv-default-avatar.png new file mode 100644 index 0000000000000000000000000000000000000000..684656def5b9a3f9a77516c06f67b7808c77f5f1 GIT binary patch literal 39577 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 KMNUMnLSTXujuxu` literal 0 HcmV?d00001 From 261952f6058c8ea4b42fa65b1b636aaceb64dd35 Mon Sep 17 00:00:00 2001 From: Daniel Egger Date: Fri, 2 Jun 2023 14:56:03 +0200 Subject: [PATCH 14/18] Change LOGIN_URL for server logout --- client/src/main.ts | 1 + server/config/settings/base.py | 4 +--- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/client/src/main.ts b/client/src/main.ts index bb082423..50a87bb8 100644 --- a/client/src/main.ts +++ b/client/src/main.ts @@ -21,6 +21,7 @@ if (appEnv.startsWith("prod")) { } else { log.setLevel("trace"); } +log.warn(`application started appEnv=${appEnv}`); const i18n = setupI18n(); const app = createApp(App); diff --git a/server/config/settings/base.py b/server/config/settings/base.py index 8330a516..61c931e6 100644 --- a/server/config/settings/base.py +++ b/server/config/settings/base.py @@ -140,9 +140,7 @@ AUTH_USER_MODEL = "core.User" # https://docs.djangoproject.com/en/dev/ref/settings/#login-redirect-url # https://docs.djangoproject.com/en/dev/ref/settings/#login-url -# FIXME make configurable!? -# LOGIN_URL = "/sso/login/" -LOGIN_URL = "/login" +LOGIN_URL = "/login-local" LOGIN_REDIRECT_URL = "/" ALLOW_LOCAL_LOGIN = env.bool("IT_ALLOW_LOCAL_LOGIN", default=DEBUG) From 791413066c17320972088f47a96e5c404fb8a0a0 Mon Sep 17 00:00:00 2001 From: Daniel Egger Date: Fri, 2 Jun 2023 15:05:35 +0200 Subject: [PATCH 15/18] Attach import users as trainers to UK --- .../commands/create_default_courses.py | 29 +++++++++++++++++++ 1 file changed, 29 insertions(+) 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 755db50c..e25bb0fc 100644 --- a/server/vbv_lernwelt/course/management/commands/create_default_courses.py +++ b/server/vbv_lernwelt/course/management/commands/create_default_courses.py @@ -484,3 +484,32 @@ def create_course_training_de(): }, ] cs.save() + + # attach users as trainers to ÜK course + course_uk = Course.objects.filter(id=COURSE_UK).first() + if course_uk: + users = [ + csu.user + for csu in CourseSessionUser.objects.filter( + course_session__course_id=COURSE_UK_TRAINING + ) + ] + + cs = CourseSession.objects.get(course_id=COURSE_UK, title="Bern 2023 a") + + for user in users: + csu, _created = CourseSessionUser.objects.get_or_create( + course_session_id=cs.id, user_id=user.id + ) + csu.role = CourseSessionUser.Role.EXPERT + csu.expert.add( + Circle.objects.get(slug="überbetriebliche-kurse-lp-circle-kickoff") + ) + csu.expert.add( + Circle.objects.get(slug="überbetriebliche-kurse-lp-circle-basis") + ) + csu.expert.add( + Circle.objects.get(slug="überbetriebliche-kurse-lp-circle-fahrzeug") + ) + + csu.save() From b5736ef9ef6c674619750faf704955113bf5accc Mon Sep 17 00:00:00 2001 From: Daniel Egger Date: Fri, 2 Jun 2023 15:14:55 +0200 Subject: [PATCH 16/18] Make link out from teams link --- .../attendanceCourse/AttendanceCourse.vue | 5 ++++- .../core/migrations/0003_alter_user_avatar_url.py | 12 ++++++++---- 2 files changed, 12 insertions(+), 5 deletions(-) diff --git a/client/src/pages/learningPath/learningContentPage/attendanceCourse/AttendanceCourse.vue b/client/src/pages/learningPath/learningContentPage/attendanceCourse/AttendanceCourse.vue index 2373d00e..aec81364 100644 --- a/client/src/pages/learningPath/learningContentPage/attendanceCourse/AttendanceCourse.vue +++ b/client/src/pages/learningPath/learningContentPage/attendanceCourse/AttendanceCourse.vue @@ -7,7 +7,10 @@

Standort

-

{{ location }}

+

+ {{ location }} +

+

{{ location }}

diff --git a/server/vbv_lernwelt/core/migrations/0003_alter_user_avatar_url.py b/server/vbv_lernwelt/core/migrations/0003_alter_user_avatar_url.py index 6df2d2cc..d1e22e74 100644 --- a/server/vbv_lernwelt/core/migrations/0003_alter_user_avatar_url.py +++ b/server/vbv_lernwelt/core/migrations/0003_alter_user_avatar_url.py @@ -6,13 +6,17 @@ from django.db import migrations, models class Migration(migrations.Migration): dependencies = [ - ('core', '0002_alter_user_managers'), + ("core", "0002_alter_user_managers"), ] operations = [ migrations.AlterField( - model_name='user', - name='avatar_url', - field=models.CharField(blank=True, default='/static/avatars/myvbv-default-avatar.png', max_length=254), + model_name="user", + name="avatar_url", + field=models.CharField( + blank=True, + default="/static/avatars/myvbv-default-avatar.png", + max_length=254, + ), ), ] From 9df76ab69a67a57c7f41d39bb1eabfd10cda564e Mon Sep 17 00:00:00 2001 From: Daniel Egger Date: Fri, 2 Jun 2023 16:17:58 +0200 Subject: [PATCH 17/18] Fix unit tests --- server/vbv_lernwelt/importer/tests/test_import_students.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/vbv_lernwelt/importer/tests/test_import_students.py b/server/vbv_lernwelt/importer/tests/test_import_students.py index bdcf3912..500299b0 100644 --- a/server/vbv_lernwelt/importer/tests/test_import_students.py +++ b/server/vbv_lernwelt/importer/tests/test_import_students.py @@ -31,7 +31,7 @@ class ImportStudentsTestCase(TestCase): print(row) create_or_update_student(self.course, dict(row)) - self.assertEqual(CourseSessionUser.objects.count(), 26) + self.assertEqual(CourseSessionUser.objects.count(), 28) class CreateOrUpdateStudentTestCase(TestCase): From 31a83a41447fa77ea0dad4a280acd0de6290c657 Mon Sep 17 00:00:00 2001 From: Daniel Egger Date: Fri, 2 Jun 2023 16:20:18 +0200 Subject: [PATCH 18/18] Export required VITE_* variables --- caprover_deploy.sh | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/caprover_deploy.sh b/caprover_deploy.sh index 2cb0c455..e6bda1e6 100755 --- a/caprover_deploy.sh +++ b/caprover_deploy.sh @@ -26,14 +26,14 @@ function generate_default_app_name() { APP_NAME=${1:-$(generate_default_app_name)} # VITE_* variables need to be present at build time -VITE_APP_ENVIRONMENT="dev-$APP_NAME" +export VITE_APP_ENVIRONMENT="dev-$APP_NAME" if [[ "$APP_NAME" == "myvbv-stage" ]]; then - VITE_OAUTH_API_BASE_URL="https://vbvtst.b2clogin.com/vbvtst.onmicrosoft.com/b2c_1_signupandsignin/oauth2/v2.0/" - VITE_APP_ENVIRONMENT="stage-caprover" + export VITE_OAUTH_API_BASE_URL="https://vbvtst.b2clogin.com/vbvtst.onmicrosoft.com/b2c_1_signupandsignin/oauth2/v2.0/" + export VITE_APP_ENVIRONMENT="stage-caprover" elif [[ "$APP_NAME" == prod* ]]; then - VITE_OAUTH_API_BASE_URL="https://edumgr.b2clogin.com/edumgr.onmicrosoft.com/b2c_1_signupandsignin/oauth2/v2.0/" - VITE_APP_ENVIRONMENT=$APP_NAME + export VITE_OAUTH_API_BASE_URL="https://edumgr.b2clogin.com/edumgr.onmicrosoft.com/b2c_1_signupandsignin/oauth2/v2.0/" + export VITE_APP_ENVIRONMENT=$APP_NAME fi echo "Deploy to $APP_NAME"