diff --git a/README.md b/README.md index 50685426..71693b77 100644 --- a/README.md +++ b/README.md @@ -12,9 +12,6 @@ cd client && npm run dev # reset db and run django dev server ./prepare_server.sh - -# run tailwind cli (for tailwind support on django templates) -cd client && npm run tailwind ``` ## Installation @@ -50,7 +47,7 @@ environment variables. It will also setup the tables for django and run the django development server. ```bash -# will initial`migrate` and `runserver` etc... +# will run `migrate` and `runserver` etc... ./prepare_server.sh # or async server diff --git a/server/config/settings/base.py b/server/config/settings/base.py index d90389fc..6dce5915 100644 --- a/server/config/settings/base.py +++ b/server/config/settings/base.py @@ -515,7 +515,7 @@ if "django_redis.cache.RedisCache" in env("IT_DJANGO_CACHE_BACKEND", default="") CACHES["api_page_cache"] = { "BACKEND": "django.core.cache.backends.db.DatabaseCache", - "LOCATION": "django_cache_learning_path", + "LOCATION": "django_cache_table_api_page", } # OAuth/OpenId Connect diff --git a/server/config/urls.py b/server/config/urls.py index fb7685d4..715f2319 100644 --- a/server/config/urls.py +++ b/server/config/urls.py @@ -20,8 +20,9 @@ from vbv_lernwelt.core.views import ( vue_logout, ) from vbv_lernwelt.course.views import ( + course_page_api_view, + get_course_sessions, mark_course_completion, - page_api_view, request_course_completion, ) from wagtail import urls as wagtail_urls @@ -59,7 +60,9 @@ urlpatterns = [ name="generate_web_component_icons"), # course - path(r"api/course/page//", page_api_view, name="page_api_view"), + path(r"api/course/sessions/", get_course_sessions, name="get_course_sessions"), + path(r"api/course/page//", course_page_api_view, + name="course_page_api_view"), path(r"api/course/completion/mark/", mark_course_completion, name="mark_course_completion"), path(r"api/course/completion//", request_course_completion, diff --git a/server/vbv_lernwelt/competence/create_default_competence_profile.py b/server/vbv_lernwelt/competence/create_default_competence_profile.py index 07c44053..0e9a7ea3 100644 --- a/server/vbv_lernwelt/competence/create_default_competence_profile.py +++ b/server/vbv_lernwelt/competence/create_default_competence_profile.py @@ -5,13 +5,13 @@ from vbv_lernwelt.competence.factories import ( ) from vbv_lernwelt.competence.models import CompetencePage from vbv_lernwelt.course.consts import COURSE_VERSICHERUNGSVERMITTLERIN_ID -from vbv_lernwelt.course.models import Course, CoursePage -from vbv_lernwelt.learnpath.models import LearningUnit +from vbv_lernwelt.course.models import CoursePage +from vbv_lernwelt.learnpath.models import LearningPath, LearningUnit -def create_default_competence_profile(): - course = Course.objects.get(id=COURSE_VERSICHERUNGSVERMITTLERIN_ID) - course_page = CoursePage.objects.get(course_id=COURSE_VERSICHERUNGSVERMITTLERIN_ID) +def create_default_competence_profile(course_id=COURSE_VERSICHERUNGSVERMITTLERIN_ID): + course_page = CoursePage.objects.get(course_id=course_id) + slug_prefix = course_page.get_children().exact_type(LearningPath).first().slug competence_profile_page = CompetenceProfilePageFactory( title="KompetenzNavi", @@ -133,380 +133,470 @@ def create_default_competence_profile(): # Daten anhand von WEVM_Version Oktober 2022 # Einstieg/Beobachten – Selbsteinschätzung «Einkommenssicherung» PerformanceCriteriaFactory( - parent=CompetencePage.objects.get(competence_id="A2"), + parent=CompetencePage.objects.get( + slug__startswith=slug_prefix.replace("-lp", ""), competence_id="A2" + ), competence_id="A2.1", title="Ich bin fähig je nach (Neu-) Kunde Form und Ort für das Gespräch festzulegen.", learning_unit=LearningUnit.objects.get( - slug="versicherungsvermittlerin-lp-circle-einstieg-lu-einkommenssicherung" + slug=f"{slug_prefix}-circle-einstieg-lu-einkommenssicherung" ), ) PerformanceCriteriaFactory( - parent=CompetencePage.objects.get(competence_id="A2"), + parent=CompetencePage.objects.get( + slug__startswith=slug_prefix.replace("-lp", ""), competence_id="A2" + ), competence_id="A2.2", title="Ich bin fähig mir intern und extern die nötigen Informationen über den (Neu-) Kunden zu beschaffen.", learning_unit=LearningUnit.objects.get( - slug="versicherungsvermittlerin-lp-circle-einstieg-lu-einkommenssicherung" + slug=f"{slug_prefix}-circle-einstieg-lu-einkommenssicherung" ), ) PerformanceCriteriaFactory( - parent=CompetencePage.objects.get(competence_id="A2"), + parent=CompetencePage.objects.get( + slug__startswith=slug_prefix.replace("-lp", ""), competence_id="A2" + ), competence_id="A2.3", title="Ich bin fähig die Terminierung auf das Thema Einkommenssicherung auszurichten.", learning_unit=LearningUnit.objects.get( - slug="versicherungsvermittlerin-lp-circle-einstieg-lu-einkommenssicherung" + slug=f"{slug_prefix}-circle-einstieg-lu-einkommenssicherung" ), ) PerformanceCriteriaFactory( - parent=CompetencePage.objects.get(competence_id="A2"), + parent=CompetencePage.objects.get( + slug__startswith=slug_prefix.replace("-lp", ""), competence_id="A2" + ), competence_id="A2.4", title="Ich bin fähig für das zu führende Gespräch eine Agenda zu erstellen.", learning_unit=LearningUnit.objects.get( - slug="versicherungsvermittlerin-lp-circle-einstieg-lu-einkommenssicherung" + slug=f"{slug_prefix}-circle-einstieg-lu-einkommenssicherung" ), ) PerformanceCriteriaFactory( - parent=CompetencePage.objects.get(competence_id="A2"), + parent=CompetencePage.objects.get( + slug__startswith=slug_prefix.replace("-lp", ""), competence_id="A2" + ), competence_id="A2.5", title="Ich bin fähig für das Handlungsfeld «Einkommenssicherung» geeignete Hilfsmittel und Unterlagen zusammenzustellen.", learning_unit=LearningUnit.objects.get( - slug="versicherungsvermittlerin-lp-circle-einstieg-lu-einkommenssicherung" + slug=f"{slug_prefix}-circle-einstieg-lu-einkommenssicherung" ), ) PerformanceCriteriaFactory( - parent=CompetencePage.objects.get(competence_id="B1"), + parent=CompetencePage.objects.get( + slug__startswith=slug_prefix.replace("-lp", ""), competence_id="B1" + ), competence_id="B1.1", title="Ich bin fähig dem Kunden den Gesprächsablauf und den Zeitrahmen (mittels Agenda) aufzuzeigen.", learning_unit=LearningUnit.objects.get( - slug="versicherungsvermittlerin-lp-circle-einstieg-lu-einkommenssicherung" + slug=f"{slug_prefix}-circle-einstieg-lu-einkommenssicherung" ), ) PerformanceCriteriaFactory( - parent=CompetencePage.objects.get(competence_id="B1"), + parent=CompetencePage.objects.get( + slug__startswith=slug_prefix.replace("-lp", ""), competence_id="B1" + ), competence_id="B1.2", title="Ich bin fähig mich beim Kunden korrekt zu identifizieren (VAG 45).", learning_unit=LearningUnit.objects.get( - slug="versicherungsvermittlerin-lp-circle-einstieg-lu-einkommenssicherung" + slug=f"{slug_prefix}-circle-einstieg-lu-einkommenssicherung" ), ) PerformanceCriteriaFactory( - parent=CompetencePage.objects.get(competence_id="B2"), + parent=CompetencePage.objects.get( + slug__startswith=slug_prefix.replace("-lp", ""), competence_id="B2" + ), competence_id="B2.3", title="Ich bin fähig alle erforderlichen Unterlagen einzufordern.", learning_unit=LearningUnit.objects.get( - slug="versicherungsvermittlerin-lp-circle-einstieg-lu-einkommenssicherung" + slug=f"{slug_prefix}-circle-einstieg-lu-einkommenssicherung" ), ) # Einstieg / Anwenden – Selbsteinschätzung «Fahrzeug» PerformanceCriteriaFactory( - parent=CompetencePage.objects.get(competence_id="A1"), + parent=CompetencePage.objects.get( + slug__startswith=slug_prefix.replace("-lp", ""), competence_id="A1" + ), competence_id="A1.6", title="Ich bin fähig im täglichen Kontakt potenzielle Kundinnen und Kunden zu erkennen.", learning_unit=LearningUnit.objects.get( - slug="versicherungsvermittlerin-lp-circle-einstieg-lu-fahrzeug" + slug=f"{slug_prefix}-circle-einstieg-lu-fahrzeug" ), ) PerformanceCriteriaFactory( - parent=CompetencePage.objects.get(competence_id="A2"), + parent=CompetencePage.objects.get( + slug__startswith=slug_prefix.replace("-lp", ""), competence_id="A2" + ), competence_id="A2.1", title="Ich bin fähig je nach (Neu-) Kunde Form und Ort für das Gespräch festzulegen.", learning_unit=LearningUnit.objects.get( - slug="versicherungsvermittlerin-lp-circle-einstieg-lu-fahrzeug" + slug=f"{slug_prefix}-circle-einstieg-lu-fahrzeug" ), ) PerformanceCriteriaFactory( - parent=CompetencePage.objects.get(competence_id="A2"), + parent=CompetencePage.objects.get( + slug__startswith=slug_prefix.replace("-lp", ""), competence_id="A2" + ), competence_id="A2.2", title="Ich bin fähig mir intern und extern die nötigen Informationen über den (Neu-) Kunden zu beschaffen.", learning_unit=LearningUnit.objects.get( - slug="versicherungsvermittlerin-lp-circle-einstieg-lu-fahrzeug" + slug=f"{slug_prefix}-circle-einstieg-lu-fahrzeug" ), ) PerformanceCriteriaFactory( - parent=CompetencePage.objects.get(competence_id="A2"), + parent=CompetencePage.objects.get( + slug__startswith=slug_prefix.replace("-lp", ""), competence_id="A2" + ), competence_id="A2.3", title="Ich bin fähig die Terminierung auf das Thema Fahrzeug auszurichten.", learning_unit=LearningUnit.objects.get( - slug="versicherungsvermittlerin-lp-circle-einstieg-lu-fahrzeug" + slug=f"{slug_prefix}-circle-einstieg-lu-fahrzeug" ), ) PerformanceCriteriaFactory( - parent=CompetencePage.objects.get(competence_id="A2"), + parent=CompetencePage.objects.get( + slug__startswith=slug_prefix.replace("-lp", ""), competence_id="A2" + ), competence_id="A2.4", title="Ich bin fähig für das zu führende Gespräch eine Agenda zu erstellen.", learning_unit=LearningUnit.objects.get( - slug="versicherungsvermittlerin-lp-circle-einstieg-lu-fahrzeug" + slug=f"{slug_prefix}-circle-einstieg-lu-fahrzeug" ), ) PerformanceCriteriaFactory( - parent=CompetencePage.objects.get(competence_id="A2"), + parent=CompetencePage.objects.get( + slug__startswith=slug_prefix.replace("-lp", ""), competence_id="A2" + ), competence_id="A2.5", title="Ich bin fähig für das zu führende Gespräch geeignete Hilfsmittel und Unterlagen zusammenzustellen.", learning_unit=LearningUnit.objects.get( - slug="versicherungsvermittlerin-lp-circle-einstieg-lu-fahrzeug" + slug=f"{slug_prefix}-circle-einstieg-lu-fahrzeug" ), ) # Einstieg / Anwenden – Selbsteinschätzung «Reisen» PerformanceCriteriaFactory( - parent=CompetencePage.objects.get(competence_id="A2"), + parent=CompetencePage.objects.get( + slug__startswith=slug_prefix.replace("-lp", ""), competence_id="A2" + ), competence_id="A2.3", title="Ich bin fähig die Terminierung auf das Thema Reisen auszurichten.", learning_unit=LearningUnit.objects.get( - slug="versicherungsvermittlerin-lp-circle-einstieg-lu-reisen" + slug=f"{slug_prefix}-circle-einstieg-lu-reisen" ), ) PerformanceCriteriaFactory( - parent=CompetencePage.objects.get(competence_id="A2"), + parent=CompetencePage.objects.get( + slug__startswith=slug_prefix.replace("-lp", ""), competence_id="A2" + ), competence_id="A2.4", title="Ich bin fähig für das zu führende Gespräch eine Agenda zu erstellen.", learning_unit=LearningUnit.objects.get( - slug="versicherungsvermittlerin-lp-circle-einstieg-lu-reisen" + slug=f"{slug_prefix}-circle-einstieg-lu-reisen" ), ) PerformanceCriteriaFactory( - parent=CompetencePage.objects.get(competence_id="A2"), + parent=CompetencePage.objects.get( + slug__startswith=slug_prefix.replace("-lp", ""), competence_id="A2" + ), competence_id="A2.5", title="Ich bin fähig für das zu führende Gespräch geeignete Hilfsmittel und Unterlagen zusammenzustellen.", learning_unit=LearningUnit.objects.get( - slug="versicherungsvermittlerin-lp-circle-einstieg-lu-reisen" + slug=f"{slug_prefix}-circle-einstieg-lu-reisen" ), ) # Analyse / Beobachten – Selbsteinschätzung «Einkommenssicherung» PerformanceCriteriaFactory( - parent=CompetencePage.objects.get(competence_id="A1"), + parent=CompetencePage.objects.get( + slug__startswith=slug_prefix.replace("-lp", ""), competence_id="A1" + ), competence_id="A1.5", title="Innerhalb des Handlungsfelds «Einkommenssicherung» bin ich fähig, das Thema Risiko und Sicherheit in einem Gespräch gezielt und auf die Situation des jeweiligen Gesprächspartners bezogen, einfliessen zu lassen.", learning_unit=LearningUnit.objects.get( - slug="versicherungsvermittlerin-lp-circle-analyse-lu-einkommenssicherung" + slug=f"{slug_prefix}-circle-analyse-lu-einkommenssicherung" ), ) PerformanceCriteriaFactory( - parent=CompetencePage.objects.get(competence_id="B1"), + parent=CompetencePage.objects.get( + slug__startswith=slug_prefix.replace("-lp", ""), competence_id="B1" + ), competence_id="B1.3", title="Innerhalb des Handlungsfelds «Einkommenssicherung» bin ich fähig, die Ziele und Pläne des Kunden zu ergründen (SOLL).", learning_unit=LearningUnit.objects.get( - slug="versicherungsvermittlerin-lp-circle-analyse-lu-einkommenssicherung" + slug=f"{slug_prefix}-circle-analyse-lu-einkommenssicherung" ), ) PerformanceCriteriaFactory( - parent=CompetencePage.objects.get(competence_id="B2"), + parent=CompetencePage.objects.get( + slug__startswith=slug_prefix.replace("-lp", ""), competence_id="B2" + ), competence_id="B2.1", title="Innerhalb des Handlungsfelds «Einkommenssicherung» bin ich fähig, die IST-Situation des Kunden mit der geeigneten Gesprächs-/Fragetechnik zu erfassen.", learning_unit=LearningUnit.objects.get( - slug="versicherungsvermittlerin-lp-circle-analyse-lu-einkommenssicherung" + slug=f"{slug_prefix}-circle-analyse-lu-einkommenssicherung" ), ) PerformanceCriteriaFactory( - parent=CompetencePage.objects.get(competence_id="B2"), + parent=CompetencePage.objects.get( + slug__startswith=slug_prefix.replace("-lp", ""), competence_id="B2" + ), competence_id="B2.2", title="Innerhalb des Handlungsfelds «Einkommenssicherung» bin ich fähig, die Risiken aufzuzeigen.", learning_unit=LearningUnit.objects.get( - slug="versicherungsvermittlerin-lp-circle-analyse-lu-einkommenssicherung" + slug=f"{slug_prefix}-circle-analyse-lu-einkommenssicherung" ), ) # Analyse/Anwenden – Selbsteinschätzung «Fahrzeug» PerformanceCriteriaFactory( - parent=CompetencePage.objects.get(competence_id="B1"), + parent=CompetencePage.objects.get( + slug__startswith=slug_prefix.replace("-lp", ""), competence_id="B1" + ), competence_id="B1.3", title="Innerhalb des Handlungsfelds «Fahrzeug» bin ich fähig, die Ziele und Pläne des Kunden zu ergründen (SOLL).", learning_unit=LearningUnit.objects.get( - slug="versicherungsvermittlerin-lp-circle-analyse-lu-fahrzeug" + slug=f"{slug_prefix}-circle-analyse-lu-fahrzeug" ), ) PerformanceCriteriaFactory( - parent=CompetencePage.objects.get(competence_id="B2"), + parent=CompetencePage.objects.get( + slug__startswith=slug_prefix.replace("-lp", ""), competence_id="B2" + ), competence_id="B2.1", title="Innerhalb des Handlungsfelds «Fahrzeug» bin ich fähig, die IST-Situation des Kunden mit der geeigneten Gesprächs-/Fragetechnik zu erfassen.", learning_unit=LearningUnit.objects.get( - slug="versicherungsvermittlerin-lp-circle-analyse-lu-fahrzeug" + slug=f"{slug_prefix}-circle-analyse-lu-fahrzeug" ), ) PerformanceCriteriaFactory( - parent=CompetencePage.objects.get(competence_id="B2"), + parent=CompetencePage.objects.get( + slug__startswith=slug_prefix.replace("-lp", ""), competence_id="B2" + ), competence_id="B2.2", title="Innerhalb des Handlungsfelds «Fahrzeug» bin ich fähig, die Risiken aufzuzeigen.", learning_unit=LearningUnit.objects.get( - slug="versicherungsvermittlerin-lp-circle-analyse-lu-fahrzeug" + slug=f"{slug_prefix}-circle-analyse-lu-fahrzeug" ), ) # Analyse/Anwenden – Selbsteinschätzung «Reisen» PerformanceCriteriaFactory( - parent=CompetencePage.objects.get(competence_id="B1"), + parent=CompetencePage.objects.get( + slug__startswith=slug_prefix.replace("-lp", ""), competence_id="B1" + ), competence_id="B1.3", title="Innerhalb des Handlungsfelds «Reisen» bin ich fähig, die Ziele und Pläne des Kunden zu ergründen (SOLL).", learning_unit=LearningUnit.objects.get( - slug="versicherungsvermittlerin-lp-circle-analyse-lu-reisen" + slug=f"{slug_prefix}-circle-analyse-lu-reisen" ), ) PerformanceCriteriaFactory( - parent=CompetencePage.objects.get(competence_id="B2"), + parent=CompetencePage.objects.get( + slug__startswith=slug_prefix.replace("-lp", ""), competence_id="B2" + ), competence_id="B2.1", title="Innerhalb des Handlungsfelds «Reisen» bin ich fähig, die IST-Situation des Kunden mit der geeigneten Gesprächs-/Fragetechnik zu erfassen.", learning_unit=LearningUnit.objects.get( - slug="versicherungsvermittlerin-lp-circle-analyse-lu-reisen" + slug=f"{slug_prefix}-circle-analyse-lu-reisen" ), ) PerformanceCriteriaFactory( - parent=CompetencePage.objects.get(competence_id="B2"), + parent=CompetencePage.objects.get( + slug__startswith=slug_prefix.replace("-lp", ""), competence_id="B2" + ), competence_id="B2.2", title="Innerhalb des Handlungsfelds «Reisen» bin ich fähig, die Risiken aufzuzeigen.", learning_unit=LearningUnit.objects.get( - slug="versicherungsvermittlerin-lp-circle-analyse-lu-reisen" + slug=f"{slug_prefix}-circle-analyse-lu-reisen" ), ) PerformanceCriteriaFactory( - parent=CompetencePage.objects.get(competence_id="C1"), + parent=CompetencePage.objects.get( + slug__startswith=slug_prefix.replace("-lp", ""), competence_id="C1" + ), competence_id="C1.1", title="Innerhalb des Handlungsfelds «Reisen» durch eine Bestandesaufnahme der aktuellen Policen zu prüfen, ob die Leistungen dem Bedarf des Kunden entsprechen.", learning_unit=LearningUnit.objects.get( - slug="versicherungsvermittlerin-lp-circle-analyse-lu-reisen" + slug=f"{slug_prefix}-circle-analyse-lu-reisen" ), ) # Lösung/Anwenden – Selbsteinschätzung «Fahrzeug» PerformanceCriteriaFactory( - parent=CompetencePage.objects.get(competence_id="B4"), + parent=CompetencePage.objects.get( + slug__startswith=slug_prefix.replace("-lp", ""), competence_id="B4" + ), competence_id="B4.2", title="Innerhalb des Handlungsfelds «Fahrzeug» bin ich fähig, dem Kunden die Vorschläge verständlich zu erläutern und die entsprechenden Informationspflichten zu erfüllen", learning_unit=LearningUnit.objects.get( - slug="versicherungsvermittlerin-lp-circle-lösung-lu-fahrzeug" + slug=f"{slug_prefix}-circle-lösung-lu-fahrzeug" ), ) PerformanceCriteriaFactory( - parent=CompetencePage.objects.get(competence_id="B4"), + parent=CompetencePage.objects.get( + slug__startswith=slug_prefix.replace("-lp", ""), competence_id="B4" + ), competence_id="B4.3", title="Innerhalb des Handlungsfelds «Fahrzeug» bin ich fähig, auf Vorbehalte und/oder Fragen sachlich korrekt und (verhandlungs-)sicher einzugehen.", learning_unit=LearningUnit.objects.get( - slug="versicherungsvermittlerin-lp-circle-lösung-lu-fahrzeug" + slug=f"{slug_prefix}-circle-lösung-lu-fahrzeug" ), ) PerformanceCriteriaFactory( - parent=CompetencePage.objects.get(competence_id="B4"), + parent=CompetencePage.objects.get( + slug__startswith=slug_prefix.replace("-lp", ""), competence_id="B4" + ), competence_id="B4.4", title="Innerhalb des Handlungsfelds «Fahrzeug» bin ich fähig, nötige Anpassungen flexibel vorzunehmen.", learning_unit=LearningUnit.objects.get( - slug="versicherungsvermittlerin-lp-circle-lösung-lu-fahrzeug" + slug=f"{slug_prefix}-circle-lösung-lu-fahrzeug" ), ) PerformanceCriteriaFactory( - parent=CompetencePage.objects.get(competence_id="C1"), + parent=CompetencePage.objects.get( + slug__startswith=slug_prefix.replace("-lp", ""), competence_id="C1" + ), competence_id="C1.1", title="Innerhalb des Handlungsfelds «Fahrzeug» bin ich fähig durch eine Bestandesaufnahme der aktuellen Policen zu prüfen, ob die Leistungen dem Bedarf des Kunden entsprechen", learning_unit=LearningUnit.objects.get( - slug="versicherungsvermittlerin-lp-circle-lösung-lu-fahrzeug" + slug=f"{slug_prefix}-circle-lösung-lu-fahrzeug" ), ) PerformanceCriteriaFactory( - parent=CompetencePage.objects.get(competence_id="C1"), + parent=CompetencePage.objects.get( + slug__startswith=slug_prefix.replace("-lp", ""), competence_id="C1" + ), competence_id="C1.2", title="Innerhalb des Handlungsfelds «Fahrzeug» bin ich fähig, den Kunden bedarfsgerechte Vorschläge für Anpassungen der Versicherungslösung zu unterbreiten (Up-Selling).", learning_unit=LearningUnit.objects.get( - slug="versicherungsvermittlerin-lp-circle-lösung-lu-fahrzeug" + slug=f"{slug_prefix}-circle-lösung-lu-fahrzeug" ), ) # Lösung/Anwenden – Selbsteinschätzung «Reisen» PerformanceCriteriaFactory( - parent=CompetencePage.objects.get(competence_id="B3"), + parent=CompetencePage.objects.get( + slug__startswith=slug_prefix.replace("-lp", ""), competence_id="B3" + ), competence_id="B3.2", title="Innerhalb des Handlungsfelds «Reisen» bin ich fähig, eine Unterversicherung, eine Doppel- oder Überversicherung, einen fehlenden Versicherungsschutz und mögliches Optimierungspotential festzustellen.", learning_unit=LearningUnit.objects.get( - slug="versicherungsvermittlerin-lp-circle-lösung-lu-reisen" + slug=f"{slug_prefix}-circle-lösung-lu-reisen" ), ) PerformanceCriteriaFactory( - parent=CompetencePage.objects.get(competence_id="B4"), + parent=CompetencePage.objects.get( + slug__startswith=slug_prefix.replace("-lp", ""), competence_id="B4" + ), competence_id="B4.2", title="Innerhalb des Handlungsfelds «Reisen» bin ich fähig, dem Kunden die Vorschläge verständlich zu erläutern und die entsprechenden Informationspflichten zu erfüllen.", learning_unit=LearningUnit.objects.get( - slug="versicherungsvermittlerin-lp-circle-lösung-lu-reisen" + slug=f"{slug_prefix}-circle-lösung-lu-reisen" ), ) PerformanceCriteriaFactory( - parent=CompetencePage.objects.get(competence_id="B4"), + parent=CompetencePage.objects.get( + slug__startswith=slug_prefix.replace("-lp", ""), competence_id="B4" + ), competence_id="B4.3", title="Innerhalb des Handlungsfelds «Reisen» bin ich fähig, auf Vorbehalte und/oder Fragen sachlich korrekt und (verhandlungs-)sicher einzugehen.", learning_unit=LearningUnit.objects.get( - slug="versicherungsvermittlerin-lp-circle-lösung-lu-reisen" + slug=f"{slug_prefix}-circle-lösung-lu-reisen" ), ) PerformanceCriteriaFactory( - parent=CompetencePage.objects.get(competence_id="B4"), + parent=CompetencePage.objects.get( + slug__startswith=slug_prefix.replace("-lp", ""), competence_id="B4" + ), competence_id="B4.4", title="Innerhalb des Handlungsfelds «Reisen» bin ich fähig, nötige Anpassungen flexibel vorzunehmen.", learning_unit=LearningUnit.objects.get( - slug="versicherungsvermittlerin-lp-circle-lösung-lu-reisen" + slug=f"{slug_prefix}-circle-lösung-lu-reisen" ), ) PerformanceCriteriaFactory( - parent=CompetencePage.objects.get(competence_id="C1"), + parent=CompetencePage.objects.get( + slug__startswith=slug_prefix.replace("-lp", ""), competence_id="C1" + ), competence_id="C1.2", title="Innerhalb des Handlungsfelds «Reisen» bin ich fähig, den Kunden bedarfsgerechte Vorschläge für Anpassungen der Versicherungslösung zu unterbreiten (Up-Selling).", learning_unit=LearningUnit.objects.get( - slug="versicherungsvermittlerin-lp-circle-lösung-lu-reisen" + slug=f"{slug_prefix}-circle-lösung-lu-reisen" ), ) PerformanceCriteriaFactory( - parent=CompetencePage.objects.get(competence_id="C1"), + parent=CompetencePage.objects.get( + slug__startswith=slug_prefix.replace("-lp", ""), competence_id="C1" + ), competence_id="C1.3", title="Innerhalb des Handlungsfelds «Reisen» bin ich fähig aufgrund des Portfolios passende Zusatzprodukte anzubieten (Cross-Selling).", learning_unit=LearningUnit.objects.get( - slug="versicherungsvermittlerin-lp-circle-lösung-lu-reisen" + slug=f"{slug_prefix}-circle-lösung-lu-reisen" ), ) # Abschluss/Anwenden – Selbsteinschätzung «Fahrzeug» PerformanceCriteriaFactory( - parent=CompetencePage.objects.get(competence_id="A1"), + parent=CompetencePage.objects.get( + slug__startswith=slug_prefix.replace("-lp", ""), competence_id="A1" + ), competence_id="A1.2", title="Innerhalb des Handlungsfelds «Fahrzeug» bin ich fähig, geeignete Personen wie z.B. Garagisten, Architekten, Treuhänder auf die Vermittlung/Zusammenarbeit anzusprechen.", learning_unit=LearningUnit.objects.get( - slug="versicherungsvermittlerin-lp-circle-abschluss-lu-fahrzeug" + slug=f"{slug_prefix}-circle-abschluss-lu-fahrzeug" ), ) PerformanceCriteriaFactory( - parent=CompetencePage.objects.get(competence_id="A4"), + parent=CompetencePage.objects.get( + slug__startswith=slug_prefix.replace("-lp", ""), competence_id="A4" + ), competence_id="A4.1", title="Innerhalb des Handlungsfelds «Fahrzeug» bin ich fähig, Kundendaten in Datenbanken (CRM) korrekt zu erfassen.", learning_unit=LearningUnit.objects.get( - slug="versicherungsvermittlerin-lp-circle-abschluss-lu-fahrzeug" + slug=f"{slug_prefix}-circle-abschluss-lu-fahrzeug" ), ) PerformanceCriteriaFactory( - parent=CompetencePage.objects.get(competence_id="B4"), + parent=CompetencePage.objects.get( + slug__startswith=slug_prefix.replace("-lp", ""), competence_id="B4" + ), competence_id="B4.6", title="Innerhalb des Handlungsfelds «Fahrzeug» bin ich fähig, Anträge korrekt auszufüllen.", learning_unit=LearningUnit.objects.get( - slug="versicherungsvermittlerin-lp-circle-abschluss-lu-fahrzeug" + slug=f"{slug_prefix}-circle-abschluss-lu-fahrzeug" ), ) PerformanceCriteriaFactory( - parent=CompetencePage.objects.get(competence_id="C1"), + parent=CompetencePage.objects.get( + slug__startswith=slug_prefix.replace("-lp", ""), competence_id="C1" + ), competence_id="C1.3", title="Innerhalb des Handlungsfelds «Fahrzeug» bin ich fähig, aufgrund des Portfolios passende Zusatzprodukte anzubieten (Cross-Selling).", learning_unit=LearningUnit.objects.get( - slug="versicherungsvermittlerin-lp-circle-abschluss-lu-fahrzeug" + slug=f"{slug_prefix}-circle-abschluss-lu-fahrzeug" ), ) # Abschluss / Anwenden – Selbsteinschätzung «Reisen» PerformanceCriteriaFactory( - parent=CompetencePage.objects.get(competence_id="B4"), + parent=CompetencePage.objects.get( + slug__startswith=slug_prefix.replace("-lp", ""), competence_id="B4" + ), competence_id="B4.6", title="Innerhalb des Handlungsfelds «Reisen» bin ich fähig, Anträge korrekt auszufüllen.", learning_unit=LearningUnit.objects.get( - slug="versicherungsvermittlerin-lp-circle-abschluss-lu-reisen" + slug=f"{slug_prefix}-circle-abschluss-lu-reisen" ), ) PerformanceCriteriaFactory( - parent=CompetencePage.objects.get(competence_id="C3"), + parent=CompetencePage.objects.get( + slug__startswith=slug_prefix.replace("-lp", ""), competence_id="C3" + ), competence_id="C3.1", title="Innerhalb des Handlungsfelds «Reisen» bin ich fähig, Kunden die Vorgehensweise für die Meldung des Schadens nachvollziehbar zu erläutern und sie bei Bedarf zu unterstützen.", learning_unit=LearningUnit.objects.get( - slug="versicherungsvermittlerin-lp-circle-abschluss-lu-reisen" + slug=f"{slug_prefix}-circle-abschluss-lu-reisen" ), ) diff --git a/server/vbv_lernwelt/competence/models.py b/server/vbv_lernwelt/competence/models.py index f9779974..81abb841 100644 --- a/server/vbv_lernwelt/competence/models.py +++ b/server/vbv_lernwelt/competence/models.py @@ -6,10 +6,16 @@ from wagtail.fields import StreamField from wagtail.models import Page from vbv_lernwelt.core.model_utils import find_available_slug -from vbv_lernwelt.core.serializer_helpers import get_it_serializer_class +from vbv_lernwelt.course.models import CourseBasePage -class CompetenceProfilePage(Page): +class CompetenceProfilePage(CourseBasePage): + serialize_field_names = [ + "course", + "circles", + "children", + ] + parent_page_types = ["course.CoursePage"] subpage_types = ["competence.CompetencePage"] @@ -26,19 +32,13 @@ class CompetenceProfilePage(Page): def get_frontend_url(self): return f"/competence/{self.slug}" - @classmethod - def get_serializer_class(cls): - return get_it_serializer_class( - cls, - [ - "course", - "circles", - "children", - ], - ) +class CompetencePage(CourseBasePage): + serialize_field_names = [ + "competence_id", + "children", + ] -class CompetencePage(Page): parent_page_types = ["competence.CompetenceProfilePage"] subpage_types = ["competence.PerformanceCriteria"] competence_id = models.TextField(default="A1") @@ -63,18 +63,8 @@ class CompetencePage(Page): ) super(CompetencePage, self).full_clean(*args, **kwargs) - @classmethod - def get_serializer_class(cls): - return get_it_serializer_class( - cls, - [ - "competence_id", - "children", - ], - ) - -class PerformanceCriteria(Page): +class PerformanceCriteria(CourseBasePage): parent_page_types = ["competence.CompetenceProfilePage"] competence_id = models.TextField(default="A1.1") learning_unit = models.ForeignKey( diff --git a/server/vbv_lernwelt/competence/serializers.py b/server/vbv_lernwelt/competence/serializers.py index 062009bf..8fb287bd 100644 --- a/server/vbv_lernwelt/competence/serializers.py +++ b/server/vbv_lernwelt/competence/serializers.py @@ -1,19 +1,14 @@ from rest_framework import serializers from vbv_lernwelt.competence.models import PerformanceCriteria -from vbv_lernwelt.core.serializer_helpers import get_it_serializer_class +from vbv_lernwelt.course.serializer_helpers import get_course_serializer_class from vbv_lernwelt.course.serializers import CourseCategorySerializer class PerformanceCriteriaSerializer( - get_it_serializer_class( + get_course_serializer_class( PerformanceCriteria, - [ - "id", - "title", - "slug", - "type", - "translation_key", + field_names=[ "competence_id", "learning_unit", "circle", @@ -43,14 +38,9 @@ class PerformanceCriteriaSerializer( class PerformanceCriteriaLearningPathSerializer( - get_it_serializer_class( + get_course_serializer_class( PerformanceCriteria, - [ - "id", - "title", - "slug", - "type", - "translation_key", + field_names=[ "competence_id", ], ) diff --git a/server/vbv_lernwelt/competence/tests/test_api.py b/server/vbv_lernwelt/competence/tests/test_api.py index 492fa46a..6bc36d1c 100644 --- a/server/vbv_lernwelt/competence/tests/test_api.py +++ b/server/vbv_lernwelt/competence/tests/test_api.py @@ -10,8 +10,8 @@ class CompetenceAPITestCase(APITestCase): def setUp(self) -> None: create_default_users() create_test_course() - self.user = User.objects.get(username="student") - self.client.login(username="student", password="test") + self.user = User.objects.get(username="admin") + self.client.login(username="admin", password="test") def test_get_compentence_page(self): slug = "test-lehrgang-competence" diff --git a/server/vbv_lernwelt/core/create_default_users.py b/server/vbv_lernwelt/core/create_default_users.py index 1de09d00..111b9b27 100644 --- a/server/vbv_lernwelt/core/create_default_users.py +++ b/server/vbv_lernwelt/core/create_default_users.py @@ -3,6 +3,55 @@ from django.contrib.auth.models import Group from vbv_lernwelt.core.models import User +default_users = [ + { + "email": "student", + "first_name": "Student", + "last_name": "Meier", + "avatar_url": "/static/avatars/avatar_iterativ.png", + }, + { + "email": "daniel.egger@iterativ.ch", + "first_name": "Daniel", + "last_name": "Egger", + "avatar_url": "/static/avatars/avatar_iterativ.png", + }, + { + "email": "axel.manderbach@lernetz.ch", + "first_name": "Axel", + "last_name": "Manderbach", + "avatar_url": "/static/avatars/avatar_axel.jpg", + }, + { + "email": "christoph.bosshard@vbv-afa.ch", + "first_name": "Christoph", + "last_name": "Bosshard", + "avatar_url": "/static/avatars/avatar_christoph.png", + "password": "myvbv1234", + }, + { + "email": "alexandra.vangelista@lernetz.ch", + "first_name": "Alexandra", + "last_name": "Vangelista", + "avatar_url": "/static/avatars/avatar_alexandra.png", + "password": "myvbv1234", + }, + { + "email": "chantal.rosenberg@vbv-afa.ch", + "first_name": "Chantal", + "last_name": "Rosenberg", + "avatar_url": "/static/avatars/avatar_chantal.png", + "password": "myvbv1234", + }, + { + "email": "bianca.muster@eiger-versicherungen.ch", + "first_name": "Bianca", + "last_name": "Muster", + "avatar_url": "/static/avatars/avatar_bianca.png", + "password": "myvbv1234", + }, +] + def create_default_users(user_model=User, group_model=Group, default_password=None): if default_password is None: @@ -54,57 +103,29 @@ def create_default_users(user_model=User, group_model=Group, default_password=No avatar_url="/static/avatars/avatar_iterativ.png", ) + for user_data in default_users: + _create_student_user(**user_data) + _create_student_user( - email="student", - first_name="Student", - last_name="Meier", - avatar_url="/static/avatars/avatar_iterativ.png", + email="expertvv.analyse@vbv-afa.ch", + first_name="Expert", + last_name="Analyse", + ) + _create_student_user( + email="expertvv.einstieg@vbv-afa.ch", + first_name="Expert", + last_name="Einstieg", ) _create_student_user( - email="daniel.egger@iterativ.ch", - first_name="Daniel", - last_name="Egger", - avatar_url="/static/avatars/avatar_iterativ.png", + email="trainer-uk1.analyse@vbv-afa.ch", + first_name="Trainer", + last_name="Analyse", ) - _create_student_user( - email="axel.manderbach@lernetz.ch", - first_name="Axel", - last_name="Manderbach", - avatar_url="/static/avatars/avatar_axel.jpg", - ) - - _create_student_user( - email="christoph.bosshard@vbv-afa.ch", - first_name="Christoph", - last_name="Bosshard", - avatar_url="/static/avatars/avatar_christoph.png", - password="myvbv1234", - ) - - _create_student_user( - email="alexandra.vangelista@lernetz.ch", - first_name="Alexandra", - last_name="Vangelista", - avatar_url="/static/avatars/avatar_alexandra.png", - password="myvbv1234", - ) - - _create_student_user( - email="chantal.rosenberg@vbv-afa.ch", - first_name="Chantal", - last_name="Rosenberg", - avatar_url="/static/avatars/avatar_chantal.png", - password="myvbv1234", - ) - - _create_student_user( - email="bianca.muster@eiger-versicherungen.ch", - first_name="Bianca", - last_name="Muster", - avatar_url="/static/avatars/avatar_bianca.png", - password="myvbv1234", + email="trainer-uk1.einstieg@vbv-afa.ch", + first_name="Trainer", + last_name="Einstieg", ) diff --git a/server/vbv_lernwelt/core/serializer_helpers.py b/server/vbv_lernwelt/core/serializer_helpers.py index b1d1ef61..2f643c5c 100644 --- a/server/vbv_lernwelt/core/serializer_helpers.py +++ b/server/vbv_lernwelt/core/serializer_helpers.py @@ -1,41 +1,46 @@ import wagtail.api.v2.serializers as wagtail_serializers from rest_framework.fields import SerializerMethodField -from vbv_lernwelt.course.models import CoursePage -from vbv_lernwelt.course.serializers import CourseCategorySerializer, CourseSerializer from vbv_lernwelt.learnpath.utils import get_wagtail_type -def get_it_serializer_class(model, field_names): - base_field_names = [ - "id", - "title", - "slug", - "type", - "translation_key", - "frontend_url", - ] +def get_it_serializer_class( + model, field_names=None, base_field_names=None, base_class=None +): + if field_names is None: + field_names = [] + + if base_field_names is None: + base_field_names = [ + "id", + "title", + "slug", + "type", + "translation_key", + "frontend_url", + ] + + if base_class is None: + base_class = ItWagtailBaseSerializer + return wagtail_serializers.get_serializer_class( model, field_names=base_field_names + field_names, meta_fields=[], - base=ItBaseSerializer, + base=base_class, ) -class ItTypeField(wagtail_serializers.TypeField): +class ItWagtailTypeField(wagtail_serializers.TypeField): def to_representation(self, obj): name = get_wagtail_type(obj) return name -class ItBaseSerializer(wagtail_serializers.BaseSerializer): - type = ItTypeField(read_only=True) +class ItWagtailBaseSerializer(wagtail_serializers.BaseSerializer): + type = ItWagtailTypeField(read_only=True) children = SerializerMethodField() - course = SerializerMethodField() - course_category = CourseCategorySerializer(read_only=True) frontend_url = SerializerMethodField() - circles = SerializerMethodField() meta_fields = [] @@ -54,36 +59,6 @@ class ItBaseSerializer(wagtail_serializers.BaseSerializer): for c in children ] - def get_course(self, obj): - if hasattr(obj, "course"): - return CourseSerializer(obj.course).data - else: - course_parent_page = obj.get_ancestors().exact_type(CoursePage).last() - if course_parent_page: - return CourseSerializer(course_parent_page.specific.course).data - return "" - - def get_circles(self, obj): - course_parent_page = obj.get_ancestors().exact_type(CoursePage).last() - - if course_parent_page: - from vbv_lernwelt.learnpath.models import Circle, LearningPath - - circles = ( - course_parent_page.get_children() - .exact_type(LearningPath) - .first() - .get_children() - .exact_type(Circle) - ) - - return [ - {"id": c.id, "title": c.title, "translation_key": c.translation_key} - for c in circles - ] - - return [] - def get_frontend_url(self, obj): if hasattr(obj, "get_frontend_url"): return obj.get_frontend_url() diff --git a/server/vbv_lernwelt/core/utils.py b/server/vbv_lernwelt/core/utils.py index 8ad3171f..61de5b11 100644 --- a/server/vbv_lernwelt/core/utils.py +++ b/server/vbv_lernwelt/core/utils.py @@ -2,6 +2,7 @@ import logging import structlog from django.conf import settings +from django.core.cache import caches from rest_framework.throttling import UserRateThrottle from structlog.types import EventDict @@ -50,3 +51,16 @@ def first_true(iterable, default=False, pred=None): # first_true([a,b,c], x) --> a or b or c or x # first_true([a,b], x, f) --> a if f(a) else b if f(b) else x return next(filter(pred, iterable), default) + + +def get_api_page_cache(): + return caches["api_page_cache"] + + +def api_page_cache_get_or_set(key, func, timeout=60 * 60 * 8): + cache = get_api_page_cache() + value = cache.get(key) + if value is None: + value = func() + cache.set(key, value, timeout=timeout) + return value diff --git a/server/vbv_lernwelt/course/admin.py b/server/vbv_lernwelt/course/admin.py index 8c38f3f3..c1788250 100644 --- a/server/vbv_lernwelt/course/admin.py +++ b/server/vbv_lernwelt/course/admin.py @@ -1,3 +1,61 @@ from django.contrib import admin -# Register your models here. +from vbv_lernwelt.course.models import CourseSession, CourseSessionUser +from vbv_lernwelt.learnpath.models import Circle + + +@admin.register(CourseSession) +class CourseSessionAdmin(admin.ModelAdmin): + date_hierarchy = "created_at" + list_display = [ + "title", + "course", + "start_date", + "end_date", + "created_at", + "updated_at", + ] + + +@admin.register(CourseSessionUser) +class CourseSessionUserAdmin(admin.ModelAdmin): + date_hierarchy = "created_at" + list_display = [ + "course_session", + "user", + "created_at", + "updated_at", + ] + search_fields = [ + "user__first_name", + "user__last_name", + "user__email", + "course_session__title", + ] + list_filter = [ + "course_session__course", + "course_session", + ] + + fieldsets = [ + (None, {"fields": ("user", "course_session")}), + ( + "Expert/Trainer", + { + "fields": ("expert",), + "description": "Expert/Trainer kann erst ausgewählt werden, wenn der Kurs ausgewählt und bereits einmal gespeichert wurde.", + }, + ), + ] + + def formfield_for_manytomany(self, db_field, request, **kwargs): + if db_field.name == "expert": + if request.resolver_match.kwargs.get("object_id"): + object_id = int(request.resolver_match.kwargs.get("object_id")) + csu = CourseSessionUser.objects.get(id=object_id) + kwargs["queryset"] = Circle.objects.descendant_of( + csu.course_session.course.coursepage + ) + else: + kwargs["queryset"] = Circle.objects.none() + return super().formfield_for_manytomany(db_field, request, **kwargs) diff --git a/server/vbv_lernwelt/course/consts.py b/server/vbv_lernwelt/course/consts.py index c48afa93..9ab911c1 100644 --- a/server/vbv_lernwelt/course/consts.py +++ b/server/vbv_lernwelt/course/consts.py @@ -1,2 +1,3 @@ COURSE_TEST_ID = -1 COURSE_VERSICHERUNGSVERMITTLERIN_ID = -2 +COURSE_UK1 = -3 diff --git a/server/vbv_lernwelt/course/creators/versicherungsvermittlerin.py b/server/vbv_lernwelt/course/creators/versicherungsvermittlerin.py index a23109b2..66fb771e 100644 --- a/server/vbv_lernwelt/course/creators/versicherungsvermittlerin.py +++ b/server/vbv_lernwelt/course/creators/versicherungsvermittlerin.py @@ -6,7 +6,12 @@ from vbv_lernwelt.course.consts import COURSE_VERSICHERUNGSVERMITTLERIN_ID from vbv_lernwelt.course.factories import CoursePageFactory -def create_versicherungsvermittlerin_with_categories(apps=None, schema_editor=None): +def create_versicherungsvermittlerin_with_categories( + apps=None, + schema_editor=None, + course_id=COURSE_VERSICHERUNGSVERMITTLERIN_ID, + title="Versicherungsvermittler/in", +): if apps is not None: Course = apps.get_model("course", "Course") CourseCategory = apps.get_model("course", "CourseCategory") @@ -15,8 +20,8 @@ def create_versicherungsvermittlerin_with_categories(apps=None, schema_editor=No from vbv_lernwelt.course.models import Course, CourseCategory course, _ = Course.objects.get_or_create( - id=COURSE_VERSICHERUNGSVERMITTLERIN_ID, - title="Versicherungsvermittler/in", + id=course_id, + title=title, category_name="Handlungsfeld", ) @@ -48,7 +53,7 @@ def create_versicherungsvermittlerin_with_categories(apps=None, schema_editor=No site.save() course_page = CoursePageFactory( - title="Versicherungsvermittler/in", + title=title, parent=site.root_page, course=course, ) 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 66b42e80..8bfa5c33 100644 --- a/server/vbv_lernwelt/course/management/commands/create_default_courses.py +++ b/server/vbv_lernwelt/course/management/commands/create_default_courses.py @@ -3,13 +3,22 @@ import djclick as click from vbv_lernwelt.competence.create_default_competence_profile import ( create_default_competence_profile, ) +from vbv_lernwelt.core.create_default_users import default_users +from vbv_lernwelt.core.models import User +from vbv_lernwelt.course.consts import ( + COURSE_TEST_ID, + COURSE_UK1, + COURSE_VERSICHERUNGSVERMITTLERIN_ID, +) from vbv_lernwelt.course.creators.test_course import create_test_course from vbv_lernwelt.course.creators.versicherungsvermittlerin import ( create_versicherungsvermittlerin_with_categories, ) +from vbv_lernwelt.course.models import CourseSession, CourseSessionUser from vbv_lernwelt.learnpath.create_default_learning_path import ( create_default_learning_path, ) +from vbv_lernwelt.learnpath.models import Circle from vbv_lernwelt.media_library.create_default_documents import ( create_default_collections, create_default_documents, @@ -21,16 +30,79 @@ from vbv_lernwelt.media_library.create_default_media_library import ( @click.command() def command(): + # Versicherungsvermittler/in create_versicherungsvermittlerin_with_categories() - create_default_learning_path() - create_default_competence_profile() - # media library + # media library data create_default_collections() create_default_documents() + create_default_media_library() + # Versicherungsvermittler/in + create_versicherungsvermittlerin_with_categories( + course_id=COURSE_UK1, title="Überbetriebliche Kurse" + ) + create_default_learning_path(course_id=COURSE_UK1) + create_default_competence_profile(course_id=COURSE_UK1) + create_default_media_library(course_id=COURSE_UK1) + # test course create_test_course() + + # course sessions + CourseSession.objects.create( + course_id=COURSE_TEST_ID, + title="Test Lehrgang Session", + ) + + # course session Versicherungsvermittler/in + cs = CourseSession.objects.create( + course_id=COURSE_VERSICHERUNGSVERMITTLERIN_ID, + title="Versicherungsvermittler/in", + ) + for user_data in default_users: + CourseSessionUser.objects.create( + course_session=cs, + user=User.objects.get(username=user_data["email"]), + ) + csu = CourseSessionUser.objects.create( + course_session=cs, + user=User.objects.get(username="expertvv.einstieg@vbv-afa.ch"), + ) + csu.expert.add( + Circle.objects.get(slug="versicherungsvermittlerin-lp-circle-einstieg") + ) + + csu = CourseSessionUser.objects.create( + course_session=cs, + user=User.objects.get(username="expertvv.analyse@vbv-afa.ch"), + ) + csu.expert.add( + Circle.objects.get(slug="versicherungsvermittlerin-lp-circle-analyse") + ) + + # course session Überbetriebliche Kurse Lehrjahr 1 + cs = CourseSession.objects.create( + course_id=COURSE_UK1, + title="Überbetriebliche Kurse Region Bern Lehrjahr 1", + ) + for user_data in default_users: + CourseSessionUser.objects.create( + course_session=cs, + user=User.objects.get(username=user_data["email"]), + ) + + csu = CourseSessionUser.objects.create( + course_session=cs, + user=User.objects.get(username="trainer-uk1.einstieg@vbv-afa.ch"), + ) + csu.expert.add(Circle.objects.get(slug="überbetriebliche-kurse-lp-circle-einstieg")) + + csu = CourseSessionUser.objects.create( + course_session=cs, + user=User.objects.get(username="trainer-uk1.analyse@vbv-afa.ch"), + ) + csu.expert.add(Circle.objects.get(slug="überbetriebliche-kurse-lp-circle-analyse")) diff --git a/server/vbv_lernwelt/course/migrations/0002_auto_20221014_0933.py b/server/vbv_lernwelt/course/migrations/0002_auto_20221014_0933.py new file mode 100644 index 00000000..ebddd71c --- /dev/null +++ b/server/vbv_lernwelt/course/migrations/0002_auto_20221014_0933.py @@ -0,0 +1,79 @@ +# Generated by Django 3.2.13 on 2022-10-14 07:33 + +import django.db.models.deletion +from django.conf import settings +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ("course", "0001_initial"), + ] + + operations = [ + migrations.CreateModel( + name="CourseSession", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("created_at", models.DateTimeField(auto_now_add=True)), + ("updated_at", models.DateTimeField(auto_now=True)), + ("title", models.TextField()), + ("start_date", models.DateField(blank=True, null=True)), + ("end_date", models.DateField(blank=True, null=True)), + ("additional_json_data", models.JSONField(default=dict)), + ( + "course", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, to="course.course" + ), + ), + ], + ), + migrations.CreateModel( + name="CourseSessionUser", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("created_at", models.DateTimeField(auto_now_add=True)), + ("updated_at", models.DateTimeField(auto_now=True)), + ( + "course_session", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + to="course.coursesession", + ), + ), + ( + "user", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + to=settings.AUTH_USER_MODEL, + ), + ), + ], + ), + migrations.AddConstraint( + model_name="coursesessionuser", + constraint=models.UniqueConstraint( + fields=("course_session", "user"), + name="course_session_user_unique_course_session_user", + ), + ), + ] diff --git a/server/vbv_lernwelt/course/migrations/0003_alter_coursepage_course.py b/server/vbv_lernwelt/course/migrations/0003_alter_coursepage_course.py new file mode 100644 index 00000000..25335d46 --- /dev/null +++ b/server/vbv_lernwelt/course/migrations/0003_alter_coursepage_course.py @@ -0,0 +1,21 @@ +# Generated by Django 3.2.13 on 2022-11-07 13:30 + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("course", "0002_auto_20221014_0933"), + ] + + operations = [ + migrations.AlterField( + model_name="coursepage", + name="course", + field=models.OneToOneField( + on_delete=django.db.models.deletion.PROTECT, to="course.course" + ), + ), + ] diff --git a/server/vbv_lernwelt/course/migrations/0004_coursesessionuser_expert.py b/server/vbv_lernwelt/course/migrations/0004_coursesessionuser_expert.py new file mode 100644 index 00000000..39d4df15 --- /dev/null +++ b/server/vbv_lernwelt/course/migrations/0004_coursesessionuser_expert.py @@ -0,0 +1,19 @@ +# Generated by Django 3.2.13 on 2022-11-07 15:51 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("learnpath", "0008_alter_learningcontent_contents"), + ("course", "0003_alter_coursepage_course"), + ] + + operations = [ + migrations.AddField( + model_name="coursesessionuser", + name="expert", + field=models.ManyToManyField(related_name="expert", to="learnpath.Circle"), + ), + ] diff --git a/server/vbv_lernwelt/course/migrations/0005_alter_coursesessionuser_expert.py b/server/vbv_lernwelt/course/migrations/0005_alter_coursesessionuser_expert.py new file mode 100644 index 00000000..5696038d --- /dev/null +++ b/server/vbv_lernwelt/course/migrations/0005_alter_coursesessionuser_expert.py @@ -0,0 +1,21 @@ +# Generated by Django 3.2.13 on 2022-11-07 15:53 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("learnpath", "0008_alter_learningcontent_contents"), + ("course", "0004_coursesessionuser_expert"), + ] + + operations = [ + migrations.AlterField( + model_name="coursesessionuser", + name="expert", + field=models.ManyToManyField( + blank=True, related_name="expert", to="learnpath.Circle" + ), + ), + ] diff --git a/server/vbv_lernwelt/course/models.py b/server/vbv_lernwelt/course/models.py index e2d94596..9109f257 100644 --- a/server/vbv_lernwelt/course/models.py +++ b/server/vbv_lernwelt/course/models.py @@ -6,6 +6,7 @@ from wagtail.models import Page from vbv_lernwelt.core.model_utils import find_available_slug from vbv_lernwelt.core.models import User +from vbv_lernwelt.course.serializer_helpers import get_course_serializer_class class Course(models.Model): @@ -17,6 +18,30 @@ class Course(models.Model): class Meta: verbose_name = _("Lehrgang") + def get_learning_path_url(self): + from vbv_lernwelt.learnpath.models import LearningPath + + learning_path_page = ( + self.coursepage.get_children().exact_type(LearningPath).first() + ) + return learning_path_page.specific.get_frontend_url() + + def get_competence_url(self): + from vbv_lernwelt.competence.models import CompetenceProfilePage + + competence_page = ( + self.coursepage.get_children().exact_type(CompetenceProfilePage).first() + ) + return competence_page.specific.get_frontend_url() + + def get_media_library_url(self): + from vbv_lernwelt.media_library.models import MediaLibraryPage + + media_library_page = ( + self.coursepage.get_children().exact_type(MediaLibraryPage).first() + ) + return media_library_page.specific.get_frontend_url() + def __str__(self): return f"{self.title}" @@ -31,10 +56,66 @@ class CourseCategory(models.Model): return f"{self.course} / {self.title}" -class CoursePage(Page): +class CourseBasePage(Page): + class Meta: + abstract = True + + serialize_field_names = [] + serialize_base_field_names = [ + "id", + "title", + "slug", + "type", + "translation_key", + "frontend_url", + ] + + def get_course_parent(self): + return self.get_ancestors(inclusive=True).exact_type(CoursePage).last() + + def get_course(self): + course_parent_page = self.get_course_parent() + if course_parent_page: + return course_parent_page.specific.course + return None + + def get_circles(self): + course_parent_page = self.get_course_parent() + + if course_parent_page: + from vbv_lernwelt.learnpath.models import Circle, LearningPath + + circles = ( + course_parent_page.get_children() + .exact_type(LearningPath) + .first() + .get_children() + .exact_type(Circle) + ) + return circles + + return None + + @classmethod + def get_serializer_class(cls): + return get_course_serializer_class( + cls, + field_names=cls.serialize_field_names, + base_field_names=cls.serialize_base_field_names, + ) + + def __str__(self): + return f"{self.title}" + + +class CoursePage(CourseBasePage): content_panels = Page.content_panels - subpage_types = ["learnpath.LearningPath", "media_library.MediaLibraryPage"] - course = models.ForeignKey("course.Course", on_delete=models.PROTECT) + subpage_types = [ + "learnpath.LearningPath", + "competence.CompetenceProfilePage", + "media_library.MediaLibraryPage", + ] + course = models.OneToOneField("course.Course", on_delete=models.PROTECT) class Meta: verbose_name = _("Lehrgang-Seite") @@ -81,3 +162,52 @@ class CourseCompletion(models.Model): name="course_completion_unique_user_page_key", ) ] + + +class CourseSession(models.Model): + """ + Die Durchführung eines Kurses + Benutzer die an eine CourseSession gehängt sind können diesen Lehrgang sehen + Das anhängen kann via CourseSessionUser oder "Schulklasse (TODO)" geschehen + """ + + created_at = models.DateTimeField(auto_now_add=True) + updated_at = models.DateTimeField(auto_now=True) + + course = models.ForeignKey("course.Course", on_delete=models.CASCADE) + title = models.TextField() + + start_date = models.DateField(null=True, blank=True) + end_date = models.DateField(null=True, blank=True) + + additional_json_data = models.JSONField(default=dict) + + def __str__(self): + return f"{self.title}" + + +class CourseSessionUser(models.Model): + """ + Ein Benutzer der an einer CourseSession teilnimmt + """ + + created_at = models.DateTimeField(auto_now_add=True) + updated_at = models.DateTimeField(auto_now=True) + + course_session = models.ForeignKey("course.CourseSession", on_delete=models.CASCADE) + user = models.ForeignKey(User, on_delete=models.CASCADE) + + expert = models.ManyToManyField( + "learnpath.Circle", related_name="expert", blank=True + ) + + class Meta: + constraints = [ + UniqueConstraint( + fields=[ + "course_session", + "user", + ], + name="course_session_user_unique_course_session_user", + ) + ] diff --git a/server/vbv_lernwelt/course/permissions.py b/server/vbv_lernwelt/course/permissions.py new file mode 100644 index 00000000..d15fe28e --- /dev/null +++ b/server/vbv_lernwelt/course/permissions.py @@ -0,0 +1,26 @@ +from vbv_lernwelt.course.models import CourseSession, CourseSessionUser + + +def has_course_access_by_page_request(request, obj): + return has_course_access(request.user, obj.specific.get_course()) + + +def has_course_access(user, course): + if user.is_superuser: + return True + + if CourseSessionUser.objects.filter( + course_session__course_id=course.id, user=user + ).exists(): + return True + + return False + + +def course_sessions_for_user_qs(user): + if user.is_superuser: + return CourseSession.objects.all() + + course_sessions = CourseSession.objects.filter(coursesessionuser__user=user) + + return course_sessions diff --git a/server/vbv_lernwelt/course/serializer_helpers.py b/server/vbv_lernwelt/course/serializer_helpers.py new file mode 100644 index 00000000..a188f7f3 --- /dev/null +++ b/server/vbv_lernwelt/course/serializer_helpers.py @@ -0,0 +1,44 @@ +from rest_framework.fields import SerializerMethodField + +from vbv_lernwelt.core.serializer_helpers import ( + get_it_serializer_class, + ItWagtailBaseSerializer, +) + + +class CourseBaseSerializer(ItWagtailBaseSerializer): + course = SerializerMethodField() + course_category = SerializerMethodField() + circles = SerializerMethodField() + + meta_fields = [] + + def get_course(self, obj): + course = obj.get_course() + if course: + from vbv_lernwelt.course.serializers import CourseSerializer + + return CourseSerializer(course).data + return "" + + def get_course_category(self, obj): + from vbv_lernwelt.course.serializers import CourseCategorySerializer + + return CourseCategorySerializer(obj.course_category).data + + def get_circles(self, obj): + circles = obj.get_circles() + + if circles: + return [ + {"id": c.id, "title": c.title, "translation_key": c.translation_key} + for c in circles + ] + + return [] + + +def get_course_serializer_class(model, field_names=None, base_field_names=None): + return get_it_serializer_class( + model, field_names, base_field_names, CourseBaseSerializer + ) diff --git a/server/vbv_lernwelt/course/serializers.py b/server/vbv_lernwelt/course/serializers.py index 49bf8b6d..7aec84cc 100644 --- a/server/vbv_lernwelt/course/serializers.py +++ b/server/vbv_lernwelt/course/serializers.py @@ -1,6 +1,13 @@ from rest_framework import serializers -from vbv_lernwelt.course.models import Course, CourseCategory, CourseCompletion +from vbv_lernwelt.course.models import ( + Course, + CourseCategory, + CourseCompletion, + CourseSession, + CourseSessionUser, +) +from vbv_lernwelt.learnpath.models import Circle class CourseSerializer(serializers.ModelSerializer): @@ -34,3 +41,60 @@ class CourseCompletionSerializer(serializers.ModelSerializer): "completion_status", "additional_json_data", ] + + +class CourseSessionSerializer(serializers.ModelSerializer): + learning_path_url = serializers.SerializerMethodField() + competence_url = serializers.SerializerMethodField() + media_library_url = serializers.SerializerMethodField() + course = serializers.SerializerMethodField() + experts = serializers.SerializerMethodField() + + def get_course(self, obj): + return CourseSerializer(obj.course).data + + def get_learning_path_url(self, obj): + return obj.course.get_learning_path_url() + + def get_media_library_url(self, obj): + return obj.course.get_media_library_url() + + def get_competence_url(self, obj): + return obj.course.get_competence_url() + + def get_experts(self, obj): + expert_relations = CourseSessionUser.objects.filter( + expert__in=Circle.objects.descendant_of(obj.course.coursepage) + ) + expert_result = [] + for er in expert_relations: + for circle in er.expert.all(): + expert_result.append( + { + "user_id": er.user.id, + "user_email": er.user.email, + "user_first_name": er.user.first_name, + "user_last_name": er.user.last_name, + "circle_id": circle.id, + "circle_slug": circle.slug, + "circle_translation_key": circle.translation_key, + } + ) + return expert_result + + class Meta: + model = CourseSession + fields = [ + "id", + "created_at", + "updated_at", + "course", + "title", + "start_date", + "end_date", + "additional_json_data", + "learning_path_url", + "competence_url", + "media_library_url", + "experts", + ] diff --git a/server/vbv_lernwelt/course/tests/test_completion_api.py b/server/vbv_lernwelt/course/tests/test_completion_api.py index 6de3072b..385292af 100644 --- a/server/vbv_lernwelt/course/tests/test_completion_api.py +++ b/server/vbv_lernwelt/course/tests/test_completion_api.py @@ -14,8 +14,8 @@ class CourseCompletionApiTestCase(APITestCase): def setUp(self) -> None: create_default_users() create_test_course() - self.user = User.objects.get(username="student") - self.client.login(username="student", password="test") + self.user = User.objects.get(username="admin") + self.client.login(username="admin", password="test") def test_completeLearningContent_works(self): learning_content = LearningContent.objects.get(title="Fachcheck Fahrzeug") diff --git a/server/vbv_lernwelt/course/tests/test_course_session_api.py b/server/vbv_lernwelt/course/tests/test_course_session_api.py new file mode 100644 index 00000000..43a7f43b --- /dev/null +++ b/server/vbv_lernwelt/course/tests/test_course_session_api.py @@ -0,0 +1,53 @@ +import json + +from rest_framework.test import APITestCase + +from vbv_lernwelt.core.create_default_users import create_default_users +from vbv_lernwelt.core.models import User +from vbv_lernwelt.course.consts import COURSE_TEST_ID +from vbv_lernwelt.course.creators.test_course import create_test_course +from vbv_lernwelt.course.models import CourseSession, CourseSessionUser + + +class CourseCompletionApiTestCase(APITestCase): + def setUp(self) -> None: + create_default_users() + create_test_course() + + self.user = User.objects.get(username="student") + + self.course_session = CourseSession.objects.create( + course_id=COURSE_TEST_ID, + title="Test Lehrgang Session", + ) + + self.client.login(username="student", password="test") + + def test_api_noCourseSession_withoutCourseSessionUser(self): + response = self.client.get(f"/api/course/sessions/") + + self.assertEqual(response.status_code, 200) + self.assertEqual(len(response.json()), 0) + + def test_api_courseSession_withCourseSessionUser(self): + csu = CourseSessionUser.objects.create( + course_session=self.course_session, + user=self.user, + ) + response = self.client.get(f"/api/course/sessions/") + + self.assertEqual(response.status_code, 200) + self.assertEqual(len(response.json()), 1) + + print(json.dumps(response.json(), indent=4)) + self.assertEqual(response.json()[0]["id"], self.course_session.id) + + def test_api_superUser_canAccessEveryCourseSession(self): + self.client.login(username="admin", password="test") + response = self.client.get(f"/api/course/sessions/") + + self.assertEqual(response.status_code, 200) + self.assertEqual(len(response.json()), 1) + + print(json.dumps(response.json(), indent=4)) + self.assertEqual(response.json()[0]["id"], self.course_session.id) diff --git a/server/vbv_lernwelt/course/views.py b/server/vbv_lernwelt/course/views.py index bd480b6f..8b2ea11d 100644 --- a/server/vbv_lernwelt/course/views.py +++ b/server/vbv_lernwelt/course/views.py @@ -1,23 +1,39 @@ import structlog -from django.views.decorators.cache import cache_page from rest_framework.decorators import api_view +from rest_framework.exceptions import PermissionDenied from rest_framework.response import Response from wagtail.models import Page -from vbv_lernwelt.course.models import CourseCompletion, CoursePage -from vbv_lernwelt.course.serializers import CourseCompletionSerializer +from vbv_lernwelt.core.utils import api_page_cache_get_or_set +from vbv_lernwelt.course.models import CourseCompletion +from vbv_lernwelt.course.permissions import ( + course_sessions_for_user_qs, + has_course_access_by_page_request, +) +from vbv_lernwelt.course.serializers import ( + CourseCompletionSerializer, + CourseSessionSerializer, +) from vbv_lernwelt.learnpath.utils import get_wagtail_type logger = structlog.get_logger(__name__) @api_view(["GET"]) -@cache_page(60 * 60 * 8, cache="api_page_cache") -def page_api_view(request, slug): +def course_page_api_view(request, slug): try: page = Page.objects.get(slug=slug, locale__language_code="de-CH") - serializer = page.specific.get_serializer_class()(page.specific) - return Response(serializer.data) + if not has_course_access_by_page_request(request, page): + raise PermissionDenied() + + data = api_page_cache_get_or_set( + key=request.get_full_path(), + func=lambda: page.specific.get_serializer_class()(page.specific).data, + ) + + return Response(data) + except PermissionDenied as e: + raise e except Exception as e: logger.error(e) return Response({"error": str(e)}, status=404) @@ -25,48 +41,77 @@ def page_api_view(request, slug): @api_view(["GET"]) def request_course_completion(request, course_id): - response_data = CourseCompletionSerializer( - CourseCompletion.objects.filter(user=request.user, course_id=course_id), - many=True, - ).data + try: + response_data = CourseCompletionSerializer( + CourseCompletion.objects.filter(user=request.user, course_id=course_id), + many=True, + ).data - return Response(status=200, data=response_data) + return Response(status=200, data=response_data) + except PermissionDenied as e: + raise e + except Exception as e: + logger.error(e) + return Response({"error": str(e)}, status=404) @api_view(["POST"]) def mark_course_completion(request): - page_key = request.data.get("page_key") - completion_status = request.data.get("completion_status", "success") + try: + page_key = request.data.get("page_key") + completion_status = request.data.get("completion_status", "success") - page = Page.objects.get(translation_key=page_key, locale__language_code="de-CH") - page_type = get_wagtail_type(page.specific) - course = CoursePage.objects.ancestor_of(page).first().specific.course + page = Page.objects.get(translation_key=page_key, locale__language_code="de-CH") + if not has_course_access_by_page_request(request, page): + raise PermissionDenied() - cc, created = CourseCompletion.objects.get_or_create( - user=request.user, - page_key=page_key, - course_id=course.id, - ) - cc.page_slug = page.slug - cc.page_type = page_type - cc.completion_status = completion_status - cc.save() + page_type = get_wagtail_type(page.specific) + course = page.specific.get_course() - response_data = CourseCompletionSerializer( - CourseCompletion.objects.filter(user=request.user, course_id=course.id), - many=True, - ).data + cc, created = CourseCompletion.objects.get_or_create( + user=request.user, + page_key=page_key, + course_id=course.id, + ) + cc.page_slug = page.slug + cc.page_type = page_type + cc.completion_status = completion_status + cc.save() - logger.debug( - "mark_course_completion successful", - label="completion_api", - page_key=page_key, - page_type=page_type, - page_slug=page.slug, - page_title=page.title, - user_id=request.user.id, - course_id=course.id, - completion_status=completion_status, - ) + response_data = CourseCompletionSerializer( + CourseCompletion.objects.filter(user=request.user, course_id=course.id), + many=True, + ).data - return Response(status=200, data=response_data) + logger.debug( + "mark_course_completion successful", + label="completion_api", + page_key=page_key, + page_type=page_type, + page_slug=page.slug, + page_title=page.title, + user_id=request.user.id, + course_id=course.id, + completion_status=completion_status, + ) + + return Response(status=200, data=response_data) + except PermissionDenied as e: + raise e + except Exception as e: + logger.error(e) + return Response({"error": str(e)}, status=404) + + +@api_view(["GET"]) +def get_course_sessions(request): + try: + course_sessions = course_sessions_for_user_qs(request.user) + return Response( + status=200, data=CourseSessionSerializer(course_sessions, many=True).data + ) + except PermissionDenied as e: + raise e + except Exception as e: + logger.error(e) + return Response({"error": str(e)}, status=404) diff --git a/server/vbv_lernwelt/learnpath/create_default_learning_path.py b/server/vbv_lernwelt/learnpath/create_default_learning_path.py index 5b99b602..36541b8a 100644 --- a/server/vbv_lernwelt/learnpath/create_default_learning_path.py +++ b/server/vbv_lernwelt/learnpath/create_default_learning_path.py @@ -28,7 +28,9 @@ from vbv_lernwelt.learnpath.tests.learning_path_factories import ( READY_HF = ["Fahrzeug", "Reisen"] -def create_default_learning_path(user=None, skip_locales=True): +def create_default_learning_path( + course_id=COURSE_VERSICHERUNGSVERMITTLERIN_ID, user=None, skip_locales=True +): if user is None: user = User.objects.get(username="info@iterativ.ch") @@ -41,7 +43,7 @@ def create_default_learning_path(user=None, skip_locales=True): site.port = 8000 site.save() - course_page = CoursePage.objects.get(course_id=COURSE_VERSICHERUNGSVERMITTLERIN_ID) + course_page = CoursePage.objects.get(course_id=course_id) lp = LearningPathFactory( title="Lernpfad", parent=course_page, diff --git a/server/vbv_lernwelt/learnpath/migrations/0008_alter_learningcontent_contents.py b/server/vbv_lernwelt/learnpath/migrations/0008_alter_learningcontent_contents.py new file mode 100644 index 00000000..ce15e8c4 --- /dev/null +++ b/server/vbv_lernwelt/learnpath/migrations/0008_alter_learningcontent_contents.py @@ -0,0 +1,125 @@ +# Generated by Django 3.2.13 on 2022-11-07 13:30 + +import wagtail.blocks +import wagtail.fields +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ("learnpath", "0007_alter_learningcontent_contents"), + ] + + operations = [ + migrations.AlterField( + model_name="learningcontent", + name="contents", + field=wagtail.fields.StreamField( + [ + ( + "video", + wagtail.blocks.StructBlock( + [ + ("description", wagtail.blocks.TextBlock()), + ("url", wagtail.blocks.TextBlock()), + ] + ), + ), + ( + "resource", + wagtail.blocks.StructBlock( + [ + ("description", wagtail.blocks.TextBlock()), + ("url", wagtail.blocks.TextBlock()), + ("text", wagtail.blocks.RichTextBlock(required=False)), + ] + ), + ), + ( + "exercise", + wagtail.blocks.StructBlock( + [ + ("description", wagtail.blocks.TextBlock()), + ("url", wagtail.blocks.TextBlock()), + ] + ), + ), + ( + "learningmodule", + wagtail.blocks.StructBlock( + [ + ("description", wagtail.blocks.TextBlock()), + ("url", wagtail.blocks.TextBlock()), + ] + ), + ), + ( + "online_training", + wagtail.blocks.StructBlock( + [ + ("description", wagtail.blocks.TextBlock()), + ("url", wagtail.blocks.TextBlock()), + ] + ), + ), + ( + "media_library", + wagtail.blocks.StructBlock( + [ + ("description", wagtail.blocks.TextBlock()), + ("url", wagtail.blocks.TextBlock()), + ] + ), + ), + ( + "document", + wagtail.blocks.StructBlock( + [ + ("description", wagtail.blocks.TextBlock()), + ("url", wagtail.blocks.TextBlock()), + ] + ), + ), + ( + "test", + wagtail.blocks.StructBlock( + [ + ("description", wagtail.blocks.TextBlock()), + ("url", wagtail.blocks.TextBlock()), + ] + ), + ), + ( + "book", + wagtail.blocks.StructBlock( + [ + ("description", wagtail.blocks.TextBlock()), + ("url", wagtail.blocks.TextBlock()), + ] + ), + ), + ( + "assignment", + wagtail.blocks.StructBlock( + [ + ("description", wagtail.blocks.TextBlock()), + ("url", wagtail.blocks.TextBlock()), + ("text", wagtail.blocks.RichTextBlock(required=False)), + ] + ), + ), + ( + "placeholder", + wagtail.blocks.StructBlock( + [ + ("description", wagtail.blocks.TextBlock()), + ("url", wagtail.blocks.TextBlock()), + ] + ), + ), + ], + use_json_field=None, + ), + ), + ] diff --git a/server/vbv_lernwelt/learnpath/models.py b/server/vbv_lernwelt/learnpath/models.py index bdef9301..8a57f875 100644 --- a/server/vbv_lernwelt/learnpath/models.py +++ b/server/vbv_lernwelt/learnpath/models.py @@ -8,8 +8,7 @@ from wagtail.images.blocks import ImageChooserBlock from wagtail.models import Page from vbv_lernwelt.core.model_utils import find_available_slug -from vbv_lernwelt.core.serializer_helpers import get_it_serializer_class -from vbv_lernwelt.course.models import CoursePage +from vbv_lernwelt.course.models import CourseBasePage, CoursePage from vbv_lernwelt.learnpath.models_learning_unit_content import ( AssignmentBlock, BookBlock, @@ -25,7 +24,8 @@ from vbv_lernwelt.learnpath.models_learning_unit_content import ( ) -class LearningPath(Page): +class LearningPath(CourseBasePage): + serialize_field_names = ["children", "course"] content_panels = Page.content_panels subpage_types = ["learnpath.Circle", "learnpath.Topic"] parent_page_types = ["course.CoursePage"] @@ -45,19 +45,10 @@ class LearningPath(Page): def get_frontend_url(self): return f"/learn/{self.slug}" - @classmethod - def get_serializer_class(cls): - return get_it_serializer_class( - cls, - [ - "children", - "course", - ], - ) +class Topic(CourseBasePage): + serialize_field_names = ["is_visible"] -class Topic(Page): - # title = models.TextField(default='') is_visible = models.BooleanField(default=True) parent_page_types = ["learnpath.LearningPath"] @@ -67,27 +58,10 @@ class Topic(Page): FieldPanel("is_visible"), ] - # content_panels = Page.content_panels + [ - # FieldPanel('is_visible', classname="full"), - # PageChooserPanel('learning_path', 'learnpath.LearningPath'), - # ] - - # parent_page_types = ['learnpath.LearningPath'] - # subpage_types = ['learnpath.Circle'] - def full_clean(self, *args, **kwargs): self.slug = find_slug_with_parent_prefix(self, "topic") super(Topic, self).full_clean(*args, **kwargs) - @classmethod - def get_serializer_class(cls): - return get_it_serializer_class( - cls, - field_names=[ - "is_visible", - ], - ) - def get_admin_display_title(self): return f"Thema: {self.draft_title}" @@ -109,7 +83,7 @@ class PersonBlock(blocks.StructBlock): icon = "user" -class Circle(Page): +class Circle(CourseBasePage): parent_page_types = ["learnpath.LearningPath"] subpage_types = [ "learnpath.LearningSequence", @@ -117,6 +91,16 @@ class Circle(Page): "learnpath.LearningContent", ] + serialize_field_names = [ + "children", + "description", + "goal_description", + "goals", + "job_situation_description", + "job_situations", + "experts", + ] + description = models.TextField(default="", blank=True) goal_description = models.TextField(default="", blank=True) @@ -148,21 +132,6 @@ class Circle(Page): FieldPanel("experts"), ] - @classmethod - def get_serializer_class(cls): - return get_it_serializer_class( - cls, - field_names=[ - "children", - "description", - "goal_description", - "goals", - "job_situation_description", - "job_situations", - "experts", - ], - ) - def get_frontend_url(self): short_slug = self.slug.replace(f"{self.get_parent().slug}-circle-", "") return f"{self.get_parent().specific.get_frontend_url()}/{short_slug}" @@ -178,7 +147,9 @@ class Circle(Page): return f"{self.title}" -class LearningSequence(Page): +class LearningSequence(CourseBasePage): + serialize_field_names = ["icon"] + parent_page_types = ["learnpath.Circle"] subpage_types = [] @@ -194,10 +165,6 @@ class LearningSequence(Page): def __str__(self): return f"{self.title}" - @classmethod - def get_serializer_class(cls): - return get_it_serializer_class(cls, field_names=["icon"]) - def get_admin_display_title(self): return f"{self.icon} {self.draft_title}" @@ -220,7 +187,7 @@ class LearningSequence(Page): return f"{self.get_parent().specific.get_frontend_url()}#{short_slug}" -class LearningUnit(Page): +class LearningUnit(CourseBasePage): parent_page_types = ["learnpath.Circle"] subpage_types = [] course_category = models.ForeignKey( @@ -277,7 +244,12 @@ class LearningUnit(Page): return f'{self.draft_title}' -class LearningContent(Page): +class LearningContent(CourseBasePage): + serialize_field_names = [ + "minutes", + "contents", + ] + parent_page_types = ["learnpath.Circle"] subpage_types = [] @@ -335,19 +307,6 @@ class LearningContent(Page): self.slug = find_slug_with_parent_prefix(self, "lc") super(LearningContent, self).full_clean(*args, **kwargs) - @classmethod - def get_serializer_class(cls): - return get_it_serializer_class( - cls, - field_names=[ - "minutes", - "contents", - ], - ) - - def __str__(self): - return f"{self.title}" - def find_slug_with_parent_prefix(page, type_prefix, slug_postfix=None): parent_slug = page.get_ancestors().exact_type(LearningPath, Circle).last().slug diff --git a/server/vbv_lernwelt/learnpath/serializers.py b/server/vbv_lernwelt/learnpath/serializers.py index 3cb8d2f3..3cfa7587 100644 --- a/server/vbv_lernwelt/learnpath/serializers.py +++ b/server/vbv_lernwelt/learnpath/serializers.py @@ -3,14 +3,14 @@ from rest_framework.fields import SerializerMethodField from vbv_lernwelt.competence.serializers import ( PerformanceCriteriaLearningPathSerializer, ) -from vbv_lernwelt.core.serializer_helpers import get_it_serializer_class +from vbv_lernwelt.course.serializer_helpers import get_course_serializer_class from vbv_lernwelt.learnpath.models import LearningUnit class LearningUnitSerializer( - get_it_serializer_class( + get_course_serializer_class( LearningUnit, - [ + field_names=[ "evaluate_url", "course_category", "children", @@ -30,9 +30,9 @@ class LearningUnitSerializer( class LearningUnitPerformanceCriteriaSerializer( - get_it_serializer_class( + get_course_serializer_class( LearningUnit, - [ + field_names=[ "evaluate_url", "course_category", ], diff --git a/server/vbv_lernwelt/learnpath/tests/test_api.py b/server/vbv_lernwelt/learnpath/tests/test_api.py index 9b5761bf..c68c0588 100644 --- a/server/vbv_lernwelt/learnpath/tests/test_api.py +++ b/server/vbv_lernwelt/learnpath/tests/test_api.py @@ -2,7 +2,9 @@ from rest_framework.test import APITestCase from vbv_lernwelt.core.admin import User from vbv_lernwelt.core.create_default_users import create_default_users +from vbv_lernwelt.course.consts import COURSE_TEST_ID from vbv_lernwelt.course.creators.test_course import create_test_course +from vbv_lernwelt.course.models import CourseSession, CourseSessionUser from vbv_lernwelt.learnpath.models import LearningPath @@ -10,19 +12,44 @@ class TestRetrieveLearingPathContents(APITestCase): def setUp(self) -> None: create_default_users() create_test_course() - self.user = User.objects.get(username="student") - self.client.login(username="student", password="test") + self.slug = "test-lehrgang-lp" + self.learning_path = LearningPath.objects.get(slug=self.slug) def test_get_learnpath_page(self): - slug = "test-lehrgang-lp" - learning_path = LearningPath.objects.get(slug=slug) - response = self.client.get(f"/api/course/page/{slug}/") + self.user = User.objects.get(username="admin") + self.client.login(username="admin", password="test") + + response = self.client.get(f"/api/course/page/{self.slug}/") self.assertEqual(response.status_code, 200) data = response.json() - self.assertEqual(learning_path.title, data["title"]) + self.assertEqual(self.learning_path.title, data["title"]) # topics and circles self.assertEqual(4, len(data["children"])) # circle "analyse" contents self.assertEqual(14, len(data["children"][3]["children"])) + + def test_normalUser_withoutCourseSession_cannotAccess(self): + self.user = User.objects.get(username="student") + self.client.login(username="student", password="test") + + response = self.client.get(f"/api/course/page/{self.slug}/") + + self.assertEqual(response.status_code, 403) + + def test_normalUser_withCourseSession_canAccess(self): + self.user = User.objects.get(username="student") + self.client.login(username="student", password="test") + + course_session = CourseSession.objects.create( + course_id=COURSE_TEST_ID, + title="Test Lehrgang Session", + ) + CourseSessionUser.objects.create( + course_session=course_session, + user=self.user, + ) + + response = self.client.get(f"/api/course/page/{self.slug}/") + self.assertEqual(response.status_code, 200) diff --git a/server/vbv_lernwelt/media_library/create_default_media_library.py b/server/vbv_lernwelt/media_library/create_default_media_library.py index 13162e11..246bd6d3 100644 --- a/server/vbv_lernwelt/media_library/create_default_media_library.py +++ b/server/vbv_lernwelt/media_library/create_default_media_library.py @@ -17,9 +17,9 @@ from vbv_lernwelt.media_library.tests.media_library_factories import ( ) -def create_default_media_library(): - course = Course.objects.get(id=COURSE_VERSICHERUNGSVERMITTLERIN_ID) - course_page = CoursePage.objects.get(course_id=COURSE_VERSICHERUNGSVERMITTLERIN_ID) +def create_default_media_library(course_id=COURSE_VERSICHERUNGSVERMITTLERIN_ID): + course = Course.objects.get(id=course_id) + course_page = CoursePage.objects.get(course_id=course_id) media_lib_page = MediaLibraryPageFactory( title="Mediathek", diff --git a/server/vbv_lernwelt/media_library/models.py b/server/vbv_lernwelt/media_library/models.py index e3e1fe2a..534c2a96 100644 --- a/server/vbv_lernwelt/media_library/models.py +++ b/server/vbv_lernwelt/media_library/models.py @@ -7,11 +7,13 @@ from wagtail.fields import StreamField from wagtail.models import Page from vbv_lernwelt.core.model_utils import find_available_slug -from vbv_lernwelt.core.serializer_helpers import get_it_serializer_class +from vbv_lernwelt.course.models import CourseBasePage from vbv_lernwelt.media_library.content_blocks import MediaContentCollection -class MediaLibraryPage(Page): +class MediaLibraryPage(CourseBasePage): + serialize_field_names = ["course", "children"] + parent_page_types = ["course.CoursePage"] subpage_types = ["media_library.MediaCategoryPage"] @@ -28,22 +30,23 @@ class MediaLibraryPage(Page): def get_frontend_url(self): return f"/media/{self.slug}" - @classmethod - def get_serializer_class(cls): - return get_it_serializer_class( - cls, - [ - "course", - "children", - ], - ) - -class MediaCategoryPage(Page): +class MediaCategoryPage(CourseBasePage): """ Handlungsfeld. zB. Fahrzeug """ + serialize_field_names = [ + "course_category", + "introduction_text", + "overview_icon", + "detail_image", + "description_title", + "description_text", + "items", + "body", + ] + course_category = models.ForeignKey( "course.CourseCategory", on_delete=models.SET_NULL, null=True, blank=True ) @@ -89,22 +92,6 @@ class MediaCategoryPage(Page): short_slug = self.slug.replace(f"{self.get_parent().slug}-cat-", "") return f"{self.get_parent().specific.get_frontend_url()}/category/{short_slug}" - @classmethod - def get_serializer_class(cls): - return get_it_serializer_class( - cls, - field_names=[ - "course_category", - "introduction_text", - "overview_icon", - "detail_image", - "description_title", - "description_text", - "items", - "body", - ], - ) - class LibraryDocument(AbstractDocument): # Todo: check https://filepreviews.io/ diff --git a/server/vbv_lernwelt/media_library/tests/test_api.py b/server/vbv_lernwelt/media_library/tests/test_api.py index b9729f54..9a8ce011 100644 --- a/server/vbv_lernwelt/media_library/tests/test_api.py +++ b/server/vbv_lernwelt/media_library/tests/test_api.py @@ -10,8 +10,8 @@ class MediaLibraryAPITestCase(APITestCase): def setUp(self) -> None: create_default_users() create_test_course() - self.user = User.objects.get(username="student") - self.client.login(username="student", password="test") + self.user = User.objects.get(username="admin") + self.client.login(username="admin", password="test") def test_get_media_library_page(self): slug = "test-lehrgang-media"