From f8d00040f64023710073bc663d2b1b3192ca9528 Mon Sep 17 00:00:00 2001 From: Daniel Egger Date: Fri, 14 Oct 2022 09:33:57 +0200 Subject: [PATCH 01/13] Add basic model --- .../migrations/0002_auto_20221014_0933.py | 43 +++++++++++++++++++ server/vbv_lernwelt/course/models.py | 42 ++++++++++++++++++ 2 files changed, 85 insertions(+) create mode 100644 server/vbv_lernwelt/course/migrations/0002_auto_20221014_0933.py 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..0bdb9a5f --- /dev/null +++ b/server/vbv_lernwelt/course/migrations/0002_auto_20221014_0933.py @@ -0,0 +1,43 @@ +# Generated by Django 3.2.13 on 2022-10-14 07:33 + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion + + +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/models.py b/server/vbv_lernwelt/course/models.py index e2d94596..9095a0ad 100644 --- a/server/vbv_lernwelt/course/models.py +++ b/server/vbv_lernwelt/course/models.py @@ -81,3 +81,45 @@ 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) + + +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) + + class Meta: + constraints = [ + UniqueConstraint( + fields=[ + "course_session", + "user", + ], + name="course_session_user_unique_course_session_user", + ) + ] From d8148158a19c49f040129bb83fcfbb4a651e3c93 Mon Sep 17 00:00:00 2001 From: Daniel Egger Date: Fri, 14 Oct 2022 10:32:53 +0200 Subject: [PATCH 02/13] Refactor serialization code --- server/vbv_lernwelt/competence/models.py | 38 +++----- server/vbv_lernwelt/competence/serializers.py | 24 ++--- .../vbv_lernwelt/core/serializer_helpers.py | 71 +++++--------- server/vbv_lernwelt/course/models.py | 56 ++++++++++- server/vbv_lernwelt/course/permissions.py | 12 +++ .../vbv_lernwelt/course/serializer_helpers.py | 42 +++++++++ server/vbv_lernwelt/course/serializers.py | 6 +- server/vbv_lernwelt/learnpath/models.py | 93 ++++++------------- server/vbv_lernwelt/learnpath/serializers.py | 10 +- server/vbv_lernwelt/media_library/models.py | 45 ++++----- 10 files changed, 206 insertions(+), 191 deletions(-) create mode 100644 server/vbv_lernwelt/course/permissions.py create mode 100644 server/vbv_lernwelt/course/serializer_helpers.py 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..a50eb387 100644 --- a/server/vbv_lernwelt/competence/serializers.py +++ b/server/vbv_lernwelt/competence/serializers.py @@ -1,19 +1,16 @@ 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.serializers import CourseCategorySerializer +from vbv_lernwelt.course.serializers import ( + CourseCategorySerializer, +) +from vbv_lernwelt.course.serializer_helpers import get_course_serializer_class 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 +40,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/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/course/models.py b/server/vbv_lernwelt/course/models.py index 9095a0ad..daffc51e 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): @@ -31,7 +32,60 @@ 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 LearningPath + from vbv_lernwelt.learnpath.models import Circle + + 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) diff --git a/server/vbv_lernwelt/course/permissions.py b/server/vbv_lernwelt/course/permissions.py new file mode 100644 index 00000000..f766ae37 --- /dev/null +++ b/server/vbv_lernwelt/course/permissions.py @@ -0,0 +1,12 @@ +from rest_framework import permissions + + +class CourseAccessPermission(permissions.BasePermission): + def has_object_permission(self, request, view, obj): + # Read permissions are allowed to any request, + # so we'll always allow GET, HEAD or OPTIONS requests. + if request.method in permissions.SAFE_METHODS: + return True + + # Instance must have an attribute named `owner`. + return obj.owner == request.user diff --git a/server/vbv_lernwelt/course/serializer_helpers.py b/server/vbv_lernwelt/course/serializer_helpers.py new file mode 100644 index 00000000..6ba4c494 --- /dev/null +++ b/server/vbv_lernwelt/course/serializer_helpers.py @@ -0,0 +1,42 @@ +from rest_framework.fields import SerializerMethodField + +from vbv_lernwelt.core.serializer_helpers import ItWagtailBaseSerializer, \ + get_it_serializer_class + + +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..869b04c0 100644 --- a/server/vbv_lernwelt/course/serializers.py +++ b/server/vbv_lernwelt/course/serializers.py @@ -1,6 +1,10 @@ from rest_framework import serializers -from vbv_lernwelt.course.models import Course, CourseCategory, CourseCompletion +from vbv_lernwelt.course.models import ( + Course, + CourseCategory, + CourseCompletion, +) class CourseSerializer(serializers.ModelSerializer): diff --git a/server/vbv_lernwelt/learnpath/models.py b/server/vbv_lernwelt/learnpath/models.py index bdef9301..876be769 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 CoursePage, CourseBasePage 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/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/ From d8577c70d54ac93479b7ee69dff46632db9e0c49 Mon Sep 17 00:00:00 2001 From: Daniel Egger Date: Fri, 14 Oct 2022 12:03:59 +0200 Subject: [PATCH 03/13] Limit page access to users which can access course --- server/config/settings/base.py | 2 +- server/config/urls.py | 5 +- server/vbv_lernwelt/core/utils.py | 14 +++ server/vbv_lernwelt/course/permissions.py | 27 +++-- .../course/tests/test_completion_api.py | 4 +- server/vbv_lernwelt/course/views.py | 107 +++++++++++------- 6 files changed, 104 insertions(+), 55 deletions(-) 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..bf7bf863 100644 --- a/server/config/urls.py +++ b/server/config/urls.py @@ -21,7 +21,7 @@ from vbv_lernwelt.core.views import ( ) from vbv_lernwelt.course.views import ( mark_course_completion, - page_api_view, + course_page_api_view, request_course_completion, ) from wagtail import urls as wagtail_urls @@ -59,7 +59,8 @@ urlpatterns = [ name="generate_web_component_icons"), # course - path(r"api/course/page//", page_api_view, name="page_api_view"), + 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/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/permissions.py b/server/vbv_lernwelt/course/permissions.py index f766ae37..34f73d0c 100644 --- a/server/vbv_lernwelt/course/permissions.py +++ b/server/vbv_lernwelt/course/permissions.py @@ -1,12 +1,21 @@ -from rest_framework import permissions +from vbv_lernwelt.course.models import CourseSessionUser -class CourseAccessPermission(permissions.BasePermission): - def has_object_permission(self, request, view, obj): - # Read permissions are allowed to any request, - # so we'll always allow GET, HEAD or OPTIONS requests. - if request.method in permissions.SAFE_METHODS: - return True +def has_course_access(request, obj): + if request.user.is_superuser: + return True - # Instance must have an attribute named `owner`. - return obj.owner == request.user + course = obj.specific.get_course() + + # attached to CourseSession + course_session = CourseSessionUser.objects.filter( + course_session__course_id=course.id, user=request.user + ).exists() + + if course_session: + return True + + # TODO is trainer/expert of session + + # TODO check school class access + return False 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/views.py b/server/vbv_lernwelt/course/views.py index bd480b6f..2e8202d2 100644 --- a/server/vbv_lernwelt/course/views.py +++ b/server/vbv_lernwelt/course/views.py @@ -1,10 +1,12 @@ 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.core.utils import api_page_cache_get_or_set +from vbv_lernwelt.course.models import CourseCompletion +from vbv_lernwelt.course.permissions import has_course_access from vbv_lernwelt.course.serializers import CourseCompletionSerializer from vbv_lernwelt.learnpath.utils import get_wagtail_type @@ -12,12 +14,20 @@ 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(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 +35,63 @@ 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(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) From c61ea10c7d86ac99acfc1003a5092508a447aba5 Mon Sep 17 00:00:00 2001 From: Daniel Egger Date: Fri, 14 Oct 2022 18:28:15 +0200 Subject: [PATCH 04/13] Format code --- server/config/urls.py | 2 +- server/vbv_lernwelt/competence/serializers.py | 4 +- .../migrations/0002_auto_20221014_0933.py | 74 ++++++++++++++----- server/vbv_lernwelt/course/models.py | 3 +- .../vbv_lernwelt/course/serializer_helpers.py | 6 +- server/vbv_lernwelt/course/serializers.py | 6 +- server/vbv_lernwelt/learnpath/models.py | 2 +- 7 files changed, 64 insertions(+), 33 deletions(-) diff --git a/server/config/urls.py b/server/config/urls.py index bf7bf863..1ab6159f 100644 --- a/server/config/urls.py +++ b/server/config/urls.py @@ -20,8 +20,8 @@ from vbv_lernwelt.core.views import ( vue_logout, ) from vbv_lernwelt.course.views import ( - mark_course_completion, course_page_api_view, + mark_course_completion, request_course_completion, ) from wagtail import urls as wagtail_urls diff --git a/server/vbv_lernwelt/competence/serializers.py b/server/vbv_lernwelt/competence/serializers.py index a50eb387..8fb287bd 100644 --- a/server/vbv_lernwelt/competence/serializers.py +++ b/server/vbv_lernwelt/competence/serializers.py @@ -1,10 +1,8 @@ from rest_framework import serializers from vbv_lernwelt.competence.models import PerformanceCriteria -from vbv_lernwelt.course.serializers import ( - CourseCategorySerializer, -) from vbv_lernwelt.course.serializer_helpers import get_course_serializer_class +from vbv_lernwelt.course.serializers import CourseCategorySerializer class PerformanceCriteriaSerializer( diff --git a/server/vbv_lernwelt/course/migrations/0002_auto_20221014_0933.py b/server/vbv_lernwelt/course/migrations/0002_auto_20221014_0933.py index 0bdb9a5f..ebddd71c 100644 --- a/server/vbv_lernwelt/course/migrations/0002_auto_20221014_0933.py +++ b/server/vbv_lernwelt/course/migrations/0002_auto_20221014_0933.py @@ -1,43 +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 -import django.db.models.deletion class Migration(migrations.Migration): dependencies = [ migrations.swappable_dependency(settings.AUTH_USER_MODEL), - ('course', '0001_initial'), + ("course", "0001_initial"), ] operations = [ migrations.CreateModel( - name='CourseSession', + 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')), + ( + "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', + 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)), + ( + "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'), + 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/models.py b/server/vbv_lernwelt/course/models.py index daffc51e..fa4f55fd 100644 --- a/server/vbv_lernwelt/course/models.py +++ b/server/vbv_lernwelt/course/models.py @@ -59,8 +59,7 @@ class CourseBasePage(Page): course_parent_page = self.get_course_parent() if course_parent_page: - from vbv_lernwelt.learnpath.models import LearningPath - from vbv_lernwelt.learnpath.models import Circle + from vbv_lernwelt.learnpath.models import Circle, LearningPath circles = ( course_parent_page.get_children() diff --git a/server/vbv_lernwelt/course/serializer_helpers.py b/server/vbv_lernwelt/course/serializer_helpers.py index 6ba4c494..a188f7f3 100644 --- a/server/vbv_lernwelt/course/serializer_helpers.py +++ b/server/vbv_lernwelt/course/serializer_helpers.py @@ -1,7 +1,9 @@ from rest_framework.fields import SerializerMethodField -from vbv_lernwelt.core.serializer_helpers import ItWagtailBaseSerializer, \ - get_it_serializer_class +from vbv_lernwelt.core.serializer_helpers import ( + get_it_serializer_class, + ItWagtailBaseSerializer, +) class CourseBaseSerializer(ItWagtailBaseSerializer): diff --git a/server/vbv_lernwelt/course/serializers.py b/server/vbv_lernwelt/course/serializers.py index 869b04c0..49bf8b6d 100644 --- a/server/vbv_lernwelt/course/serializers.py +++ b/server/vbv_lernwelt/course/serializers.py @@ -1,10 +1,6 @@ from rest_framework import serializers -from vbv_lernwelt.course.models import ( - Course, - CourseCategory, - CourseCompletion, -) +from vbv_lernwelt.course.models import Course, CourseCategory, CourseCompletion class CourseSerializer(serializers.ModelSerializer): diff --git a/server/vbv_lernwelt/learnpath/models.py b/server/vbv_lernwelt/learnpath/models.py index 876be769..8a57f875 100644 --- a/server/vbv_lernwelt/learnpath/models.py +++ b/server/vbv_lernwelt/learnpath/models.py @@ -8,7 +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.course.models import CoursePage, CourseBasePage +from vbv_lernwelt.course.models import CourseBasePage, CoursePage from vbv_lernwelt.learnpath.models_learning_unit_content import ( AssignmentBlock, BookBlock, From b36b0e8d967532462e208fe45b6e4f25b37103e9 Mon Sep 17 00:00:00 2001 From: Daniel Egger Date: Mon, 7 Nov 2022 14:12:04 +0100 Subject: [PATCH 05/13] Fix test by assigning admin user --- server/vbv_lernwelt/competence/tests/test_api.py | 4 ++-- server/vbv_lernwelt/learnpath/tests/test_api.py | 4 ++-- server/vbv_lernwelt/media_library/tests/test_api.py | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) 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/learnpath/tests/test_api.py b/server/vbv_lernwelt/learnpath/tests/test_api.py index 9b5761bf..49379e84 100644 --- a/server/vbv_lernwelt/learnpath/tests/test_api.py +++ b/server/vbv_lernwelt/learnpath/tests/test_api.py @@ -10,8 +10,8 @@ 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.user = User.objects.get(username="admin") + self.client.login(username="admin", password="test") def test_get_learnpath_page(self): slug = "test-lehrgang-lp" 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" From 05e02449c8914f8e1c43da1e3ada26b35d68c671 Mon Sep 17 00:00:00 2001 From: Daniel Egger Date: Mon, 7 Nov 2022 14:21:06 +0100 Subject: [PATCH 06/13] Refactor course access check --- server/vbv_lernwelt/course/permissions.py | 12 +++++++----- server/vbv_lernwelt/course/views.py | 8 +++++--- 2 files changed, 12 insertions(+), 8 deletions(-) diff --git a/server/vbv_lernwelt/course/permissions.py b/server/vbv_lernwelt/course/permissions.py index 34f73d0c..84c2ea40 100644 --- a/server/vbv_lernwelt/course/permissions.py +++ b/server/vbv_lernwelt/course/permissions.py @@ -1,15 +1,17 @@ from vbv_lernwelt.course.models import CourseSessionUser -def has_course_access(request, obj): - if request.user.is_superuser: - return True +def has_course_access_by_page_request(request, obj): + return has_course_access(request.user, obj.specific.get_course()) - course = obj.specific.get_course() + +def has_course_access(user, course): + if user.is_superuser: + return True # attached to CourseSession course_session = CourseSessionUser.objects.filter( - course_session__course_id=course.id, user=request.user + course_session__course_id=course.id, user=user ).exists() if course_session: diff --git a/server/vbv_lernwelt/course/views.py b/server/vbv_lernwelt/course/views.py index 2e8202d2..8405f31b 100644 --- a/server/vbv_lernwelt/course/views.py +++ b/server/vbv_lernwelt/course/views.py @@ -6,7 +6,9 @@ from wagtail.models import Page 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 has_course_access +from vbv_lernwelt.course.permissions import ( + has_course_access_by_page_request, +) from vbv_lernwelt.course.serializers import CourseCompletionSerializer from vbv_lernwelt.learnpath.utils import get_wagtail_type @@ -17,7 +19,7 @@ logger = structlog.get_logger(__name__) def course_page_api_view(request, slug): try: page = Page.objects.get(slug=slug, locale__language_code="de-CH") - if not has_course_access(request, page): + if not has_course_access_by_page_request(request, page): raise PermissionDenied() data = api_page_cache_get_or_set( @@ -56,7 +58,7 @@ def mark_course_completion(request): completion_status = request.data.get("completion_status", "success") page = Page.objects.get(translation_key=page_key, locale__language_code="de-CH") - if not has_course_access(request, page): + if not has_course_access_by_page_request(request, page): raise PermissionDenied() page_type = get_wagtail_type(page.specific) From 17eaf80d2c0b9d56babc3c735a934c26debe0fb7 Mon Sep 17 00:00:00 2001 From: Daniel Egger Date: Mon, 7 Nov 2022 14:54:43 +0100 Subject: [PATCH 07/13] Add CourseSession serializer and view --- server/config/urls.py | 2 + .../commands/create_default_courses.py | 16 ++- .../0003_alter_coursepage_course.py | 21 +++ server/vbv_lernwelt/course/models.py | 32 ++++- server/vbv_lernwelt/course/permissions.py | 13 +- server/vbv_lernwelt/course/serializers.py | 42 +++++- server/vbv_lernwelt/course/views.py | 20 ++- .../0008_alter_learningcontent_contents.py | 125 ++++++++++++++++++ 8 files changed, 265 insertions(+), 6 deletions(-) create mode 100644 server/vbv_lernwelt/course/migrations/0003_alter_coursepage_course.py create mode 100644 server/vbv_lernwelt/learnpath/migrations/0008_alter_learningcontent_contents.py diff --git a/server/config/urls.py b/server/config/urls.py index 1ab6159f..9e8dbc6f 100644 --- a/server/config/urls.py +++ b/server/config/urls.py @@ -21,6 +21,7 @@ from vbv_lernwelt.core.views import ( ) from vbv_lernwelt.course.views import ( course_page_api_view, + get_course_sessions, mark_course_completion, request_course_completion, ) @@ -59,6 +60,7 @@ urlpatterns = [ name="generate_web_component_icons"), # course + 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, 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..f7bdbab2 100644 --- a/server/vbv_lernwelt/course/management/commands/create_default_courses.py +++ b/server/vbv_lernwelt/course/management/commands/create_default_courses.py @@ -3,10 +3,15 @@ import djclick as click from vbv_lernwelt.competence.create_default_competence_profile import ( create_default_competence_profile, ) +from vbv_lernwelt.course.consts import ( + COURSE_TEST_ID, + 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 from vbv_lernwelt.learnpath.create_default_learning_path import ( create_default_learning_path, ) @@ -24,7 +29,6 @@ def command(): create_versicherungsvermittlerin_with_categories() create_default_learning_path() - create_default_competence_profile() # media library @@ -34,3 +38,13 @@ def command(): # test course create_test_course() + + # course sessions + CourseSession.objects.create( + course_id=COURSE_TEST_ID, + title="Test Lehrgang Session", + ) + CourseSession.objects.create( + course_id=COURSE_VERSICHERUNGSVERMITTLERIN_ID, + title="Versicherungsvermittler/in Session", + ) 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/models.py b/server/vbv_lernwelt/course/models.py index fa4f55fd..e0ed641b 100644 --- a/server/vbv_lernwelt/course/models.py +++ b/server/vbv_lernwelt/course/models.py @@ -18,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}" @@ -86,8 +110,12 @@ class CourseBasePage(Page): 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") diff --git a/server/vbv_lernwelt/course/permissions.py b/server/vbv_lernwelt/course/permissions.py index 84c2ea40..aafad8d2 100644 --- a/server/vbv_lernwelt/course/permissions.py +++ b/server/vbv_lernwelt/course/permissions.py @@ -1,4 +1,4 @@ -from vbv_lernwelt.course.models import CourseSessionUser +from vbv_lernwelt.course.models import CourseSession, CourseSessionUser def has_course_access_by_page_request(request, obj): @@ -21,3 +21,14 @@ def has_course_access(user, course): # TODO check school class access return False + + +def course_sessions_for_user_qs(user): + if user.is_superuser: + return CourseSession.objects.all() + + course_sessions = CourseSession.objects.filter( + course_session_user__user=user + ).distinct() + + return course_sessions diff --git a/server/vbv_lernwelt/course/serializers.py b/server/vbv_lernwelt/course/serializers.py index 49bf8b6d..6d2840a9 100644 --- a/server/vbv_lernwelt/course/serializers.py +++ b/server/vbv_lernwelt/course/serializers.py @@ -1,6 +1,11 @@ from rest_framework import serializers -from vbv_lernwelt.course.models import Course, CourseCategory, CourseCompletion +from vbv_lernwelt.course.models import ( + Course, + CourseCategory, + CourseCompletion, + CourseSession, +) class CourseSerializer(serializers.ModelSerializer): @@ -34,3 +39,38 @@ 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() + + 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() + + 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", + ] diff --git a/server/vbv_lernwelt/course/views.py b/server/vbv_lernwelt/course/views.py index 8405f31b..8b2ea11d 100644 --- a/server/vbv_lernwelt/course/views.py +++ b/server/vbv_lernwelt/course/views.py @@ -7,9 +7,13 @@ from wagtail.models import Page 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 +from vbv_lernwelt.course.serializers import ( + CourseCompletionSerializer, + CourseSessionSerializer, +) from vbv_lernwelt.learnpath.utils import get_wagtail_type logger = structlog.get_logger(__name__) @@ -97,3 +101,17 @@ def mark_course_completion(request): 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/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, + ), + ), + ] From a119cc122d1bd5bd9caf6fe3835f6728c5aa20a6 Mon Sep 17 00:00:00 2001 From: Daniel Egger Date: Mon, 7 Nov 2022 16:48:58 +0100 Subject: [PATCH 08/13] Add CourseSession admin model --- README.md | 5 +--- server/vbv_lernwelt/course/admin.py | 36 +++++++++++++++++++++++++++- server/vbv_lernwelt/course/models.py | 3 +++ 3 files changed, 39 insertions(+), 5 deletions(-) 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/vbv_lernwelt/course/admin.py b/server/vbv_lernwelt/course/admin.py index 8c38f3f3..59789610 100644 --- a/server/vbv_lernwelt/course/admin.py +++ b/server/vbv_lernwelt/course/admin.py @@ -1,3 +1,37 @@ from django.contrib import admin -# Register your models here. +from vbv_lernwelt.course.models import CourseSession, CourseSessionUser + + +@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", + ] diff --git a/server/vbv_lernwelt/course/models.py b/server/vbv_lernwelt/course/models.py index e0ed641b..bcc93401 100644 --- a/server/vbv_lernwelt/course/models.py +++ b/server/vbv_lernwelt/course/models.py @@ -182,6 +182,9 @@ class CourseSession(models.Model): additional_json_data = models.JSONField(default=dict) + def __str__(self): + return f"{self.title}" + class CourseSessionUser(models.Model): """ From efd8892d6a7322463b5ef2753ec7e5c8920d12aa Mon Sep 17 00:00:00 2001 From: Daniel Egger Date: Mon, 7 Nov 2022 17:43:29 +0100 Subject: [PATCH 09/13] Select Circle expert/trainer on the admin field (not dynamic) --- server/vbv_lernwelt/course/admin.py | 24 +++++++++++++++++++ .../0004_coursesessionuser_expert.py | 19 +++++++++++++++ .../0005_alter_coursesessionuser_expert.py | 19 +++++++++++++++ server/vbv_lernwelt/course/models.py | 4 ++++ server/vbv_lernwelt/course/permissions.py | 2 +- server/vbv_lernwelt/course/serializers.py | 20 ++++++++++++++++ 6 files changed, 87 insertions(+), 1 deletion(-) create mode 100644 server/vbv_lernwelt/course/migrations/0004_coursesessionuser_expert.py create mode 100644 server/vbv_lernwelt/course/migrations/0005_alter_coursesessionuser_expert.py diff --git a/server/vbv_lernwelt/course/admin.py b/server/vbv_lernwelt/course/admin.py index 59789610..c1788250 100644 --- a/server/vbv_lernwelt/course/admin.py +++ b/server/vbv_lernwelt/course/admin.py @@ -1,6 +1,7 @@ from django.contrib import admin from vbv_lernwelt.course.models import CourseSession, CourseSessionUser +from vbv_lernwelt.learnpath.models import Circle @admin.register(CourseSession) @@ -35,3 +36,26 @@ class CourseSessionUserAdmin(admin.ModelAdmin): "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/migrations/0004_coursesessionuser_expert.py b/server/vbv_lernwelt/course/migrations/0004_coursesessionuser_expert.py new file mode 100644 index 00000000..5827c37e --- /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..4f26fea8 --- /dev/null +++ b/server/vbv_lernwelt/course/migrations/0005_alter_coursesessionuser_expert.py @@ -0,0 +1,19 @@ +# 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 bcc93401..9109f257 100644 --- a/server/vbv_lernwelt/course/models.py +++ b/server/vbv_lernwelt/course/models.py @@ -197,6 +197,10 @@ class CourseSessionUser(models.Model): 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( diff --git a/server/vbv_lernwelt/course/permissions.py b/server/vbv_lernwelt/course/permissions.py index aafad8d2..67335bb8 100644 --- a/server/vbv_lernwelt/course/permissions.py +++ b/server/vbv_lernwelt/course/permissions.py @@ -29,6 +29,6 @@ def course_sessions_for_user_qs(user): course_sessions = CourseSession.objects.filter( course_session_user__user=user - ).distinct() + ).select_related("coursesessionuser") return course_sessions diff --git a/server/vbv_lernwelt/course/serializers.py b/server/vbv_lernwelt/course/serializers.py index 6d2840a9..e4507eea 100644 --- a/server/vbv_lernwelt/course/serializers.py +++ b/server/vbv_lernwelt/course/serializers.py @@ -5,7 +5,9 @@ from vbv_lernwelt.course.models import ( CourseCategory, CourseCompletion, CourseSession, + CourseSessionUser, ) +from vbv_lernwelt.learnpath.models import Circle class CourseSerializer(serializers.ModelSerializer): @@ -46,6 +48,7 @@ class CourseSessionSerializer(serializers.ModelSerializer): 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 @@ -59,6 +62,22 @@ class CourseSessionSerializer(serializers.ModelSerializer): 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, + "circle_id": circle.id, + "circle_translation_key": circle.translation_key, + } + ) + return expert_result + class Meta: model = CourseSession fields = [ @@ -73,4 +92,5 @@ class CourseSessionSerializer(serializers.ModelSerializer): "learning_path_url", "competence_url", "media_library_url", + "experts", ] From e6e5b77d0e856ad9856c3b129ea242c014e6b088 Mon Sep 17 00:00:00 2001 From: Daniel Egger Date: Mon, 7 Nov 2022 18:30:19 +0100 Subject: [PATCH 10/13] Create default `CourseSessionUser`s --- .../vbv_lernwelt/core/create_default_users.py | 104 ++++++++++-------- .../commands/create_default_courses.py | 28 ++++- server/vbv_lernwelt/course/serializers.py | 4 + 3 files changed, 87 insertions(+), 49 deletions(-) diff --git a/server/vbv_lernwelt/core/create_default_users.py b/server/vbv_lernwelt/core/create_default_users.py index 1de09d00..bf3ce21e 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@vbv-afa.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,18 @@ def create_default_users(user_model=User, group_model=Group, default_password=No avatar_url="/static/avatars/avatar_iterativ.png", ) - _create_student_user( - email="student", - first_name="Student", - last_name="Meier", - avatar_url="/static/avatars/avatar_iterativ.png", - ) + for user_data in default_users: + _create_student_user(**user_data) _create_student_user( - email="daniel.egger@iterativ.ch", - first_name="Daniel", - last_name="Egger", - avatar_url="/static/avatars/avatar_iterativ.png", + email="expertvv.analyse@vbv-afa.ch", + first_name="Expert", + 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="expertvv.einstieg@vbv-afa.ch", + first_name="Expert", + last_name="Einstieg", ) 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 f7bdbab2..64458598 100644 --- a/server/vbv_lernwelt/course/management/commands/create_default_courses.py +++ b/server/vbv_lernwelt/course/management/commands/create_default_courses.py @@ -3,6 +3,8 @@ 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_VERSICHERUNGSVERMITTLERIN_ID, @@ -11,10 +13,11 @@ 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 +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, @@ -44,7 +47,28 @@ def command(): course_id=COURSE_TEST_ID, title="Test Lehrgang Session", ) - CourseSession.objects.create( + + cs = CourseSession.objects.create( course_id=COURSE_VERSICHERUNGSVERMITTLERIN_ID, title="Versicherungsvermittler/in Session", ) + 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") + ) diff --git a/server/vbv_lernwelt/course/serializers.py b/server/vbv_lernwelt/course/serializers.py index e4507eea..7aec84cc 100644 --- a/server/vbv_lernwelt/course/serializers.py +++ b/server/vbv_lernwelt/course/serializers.py @@ -72,7 +72,11 @@ class CourseSessionSerializer(serializers.ModelSerializer): 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, } ) From 93a0fdaadb244af06d190085db014b1bc2373d5c Mon Sep 17 00:00:00 2001 From: Daniel Egger Date: Tue, 8 Nov 2022 14:59:25 +0100 Subject: [PATCH 11/13] =?UTF-8?q?Add=20=C3=9CK1=20course?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- server/config/urls.py | 2 +- .../create_default_competence_profile.py | 280 ++++++++++++------ .../vbv_lernwelt/core/create_default_users.py | 11 + server/vbv_lernwelt/course/consts.py | 1 + .../creators/versicherungsvermittlerin.py | 13 +- .../commands/create_default_courses.py | 40 ++- .../0004_coursesessionuser_expert.py | 10 +- .../0005_alter_coursesessionuser_expert.py | 12 +- .../learnpath/create_default_learning_path.py | 6 +- .../create_default_media_library.py | 6 +- 10 files changed, 263 insertions(+), 118 deletions(-) diff --git a/server/config/urls.py b/server/config/urls.py index 9e8dbc6f..715f2319 100644 --- a/server/config/urls.py +++ b/server/config/urls.py @@ -61,7 +61,7 @@ urlpatterns = [ # course path(r"api/course/sessions/", get_course_sessions, name="get_course_sessions"), - path(r"api/course/page//", course_page_api_view, + 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"), 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/core/create_default_users.py b/server/vbv_lernwelt/core/create_default_users.py index bf3ce21e..492ee958 100644 --- a/server/vbv_lernwelt/core/create_default_users.py +++ b/server/vbv_lernwelt/core/create_default_users.py @@ -117,6 +117,17 @@ def create_default_users(user_model=User, group_model=Group, default_password=No last_name="Einstieg", ) + _create_student_user( + email="trainer-uk1.analyse@vbv-afa.ch", + first_name="Trainer", + last_name="Analyse", + ) + _create_student_user( + email="trainer-uk1.einstieg@vbv-afa.ch", + first_name="Trainer", + last_name="Einstieg", + ) + def _get_or_create_user(user_model, *args, **kwargs): username = kwargs.get("username", None) 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 64458598..8bfa5c33 100644 --- a/server/vbv_lernwelt/course/management/commands/create_default_courses.py +++ b/server/vbv_lernwelt/course/management/commands/create_default_courses.py @@ -7,6 +7,7 @@ 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 @@ -29,16 +30,25 @@ 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() @@ -48,9 +58,10 @@ def command(): title="Test Lehrgang Session", ) + # course session Versicherungsvermittler/in cs = CourseSession.objects.create( course_id=COURSE_VERSICHERUNGSVERMITTLERIN_ID, - title="Versicherungsvermittler/in Session", + title="Versicherungsvermittler/in", ) for user_data in default_users: CourseSessionUser.objects.create( @@ -72,3 +83,26 @@ def command(): 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/0004_coursesessionuser_expert.py b/server/vbv_lernwelt/course/migrations/0004_coursesessionuser_expert.py index 5827c37e..39d4df15 100644 --- a/server/vbv_lernwelt/course/migrations/0004_coursesessionuser_expert.py +++ b/server/vbv_lernwelt/course/migrations/0004_coursesessionuser_expert.py @@ -6,14 +6,14 @@ from django.db import migrations, models class Migration(migrations.Migration): dependencies = [ - ('learnpath', '0008_alter_learningcontent_contents'), - ('course', '0003_alter_coursepage_course'), + ("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'), + 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 index 4f26fea8..5696038d 100644 --- a/server/vbv_lernwelt/course/migrations/0005_alter_coursesessionuser_expert.py +++ b/server/vbv_lernwelt/course/migrations/0005_alter_coursesessionuser_expert.py @@ -6,14 +6,16 @@ from django.db import migrations, models class Migration(migrations.Migration): dependencies = [ - ('learnpath', '0008_alter_learningcontent_contents'), - ('course', '0004_coursesessionuser_expert'), + ("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'), + model_name="coursesessionuser", + name="expert", + field=models.ManyToManyField( + blank=True, related_name="expert", to="learnpath.Circle" + ), ), ] 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/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", From 6f6fbc3cf832bc3acb53ac1b57c1fd799fbc9da2 Mon Sep 17 00:00:00 2001 From: Daniel Egger Date: Wed, 9 Nov 2022 10:29:11 +0100 Subject: [PATCH 12/13] Replace email from Bianca --- server/vbv_lernwelt/core/create_default_users.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/vbv_lernwelt/core/create_default_users.py b/server/vbv_lernwelt/core/create_default_users.py index 492ee958..111b9b27 100644 --- a/server/vbv_lernwelt/core/create_default_users.py +++ b/server/vbv_lernwelt/core/create_default_users.py @@ -44,7 +44,7 @@ default_users = [ "password": "myvbv1234", }, { - "email": "bianca.muster@vbv-afa.ch", + "email": "bianca.muster@eiger-versicherungen.ch", "first_name": "Bianca", "last_name": "Muster", "avatar_url": "/static/avatars/avatar_bianca.png", From 2295ede996d18efc4229c1363f4047f4d35b07f5 Mon Sep 17 00:00:00 2001 From: Daniel Egger Date: Wed, 9 Nov 2022 10:50:13 +0100 Subject: [PATCH 13/13] Add python unit tests --- server/vbv_lernwelt/course/permissions.py | 14 ++--- .../course/tests/test_course_session_api.py | 53 +++++++++++++++++++ .../vbv_lernwelt/learnpath/tests/test_api.py | 37 +++++++++++-- 3 files changed, 88 insertions(+), 16 deletions(-) create mode 100644 server/vbv_lernwelt/course/tests/test_course_session_api.py diff --git a/server/vbv_lernwelt/course/permissions.py b/server/vbv_lernwelt/course/permissions.py index 67335bb8..d15fe28e 100644 --- a/server/vbv_lernwelt/course/permissions.py +++ b/server/vbv_lernwelt/course/permissions.py @@ -9,17 +9,11 @@ def has_course_access(user, course): if user.is_superuser: return True - # attached to CourseSession - course_session = CourseSessionUser.objects.filter( + if CourseSessionUser.objects.filter( course_session__course_id=course.id, user=user - ).exists() - - if course_session: + ).exists(): return True - # TODO is trainer/expert of session - - # TODO check school class access return False @@ -27,8 +21,6 @@ def course_sessions_for_user_qs(user): if user.is_superuser: return CourseSession.objects.all() - course_sessions = CourseSession.objects.filter( - course_session_user__user=user - ).select_related("coursesessionuser") + course_sessions = CourseSession.objects.filter(coursesessionuser__user=user) return course_sessions 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/learnpath/tests/test_api.py b/server/vbv_lernwelt/learnpath/tests/test_api.py index 49379e84..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.slug = "test-lehrgang-lp" + self.learning_path = LearningPath.objects.get(slug=self.slug) + + def test_get_learnpath_page(self): self.user = User.objects.get(username="admin") self.client.login(username="admin", password="test") - 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}/") + 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)