From 2c3b77b0d86f8c69dff64057334e26cf6659e5ae Mon Sep 17 00:00:00 2001 From: Daniel Egger Date: Wed, 5 Apr 2023 14:47:00 +0200 Subject: [PATCH 01/11] VBV-234: Bugfix neue Seiten im Wagtail erstellen MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Der `parent` einer Page ist erst in der `save` Funktion vorhanden für eine neue Seite. Deshalb muss man das Überschreiben der `slug` im `save` machen. --- server/vbv_lernwelt/competence/models.py | 18 ++++++++-------- server/vbv_lernwelt/course/models.py | 4 ++-- server/vbv_lernwelt/learnpath/models.py | 24 ++++++++++----------- server/vbv_lernwelt/media_library/models.py | 8 +++---- 4 files changed, 27 insertions(+), 27 deletions(-) diff --git a/server/vbv_lernwelt/competence/models.py b/server/vbv_lernwelt/competence/models.py index fc69abd6..95622c4f 100644 --- a/server/vbv_lernwelt/competence/models.py +++ b/server/vbv_lernwelt/competence/models.py @@ -23,14 +23,14 @@ class CompetenceProfilePage(CourseBasePage): FieldPanel("title", classname="full title"), ] - def full_clean(self, *args, **kwargs): + def get_frontend_url(self): + return f"/course/{self.slug.replace('-competence', '')}/competence" + + def save(self, clean=True, user=None, log_action=False, **kwargs): self.slug = find_available_slug( slugify(f"{self.get_parent().slug}-competence", allow_unicode=True) ) - super(CompetenceProfilePage, self).full_clean(*args, **kwargs) - - def get_frontend_url(self): - return f"/course/{self.slug.replace('-competence', '')}/competence" + super(CompetenceProfilePage, self).save(clean, user, log_action, **kwargs) class CompetencePage(CourseBasePage): @@ -54,14 +54,14 @@ class CompetencePage(CourseBasePage): FieldPanel("competence_id"), ] - def full_clean(self, *args, **kwargs): + def save(self, clean=True, user=None, log_action=False, **kwargs): self.slug = find_available_slug( slugify( f"{self.get_parent().slug}-competence-{self.competence_id}", allow_unicode=True, ) ) - super(CompetencePage, self).full_clean(*args, **kwargs) + super(CompetencePage, self).save(clean, user, log_action, **kwargs) class PerformanceCriteria(CourseBasePage): @@ -80,7 +80,7 @@ class PerformanceCriteria(CourseBasePage): FieldPanel("learning_unit"), ] - def full_clean(self, *args, **kwargs): + def save(self, clean=True, user=None, log_action=False, **kwargs): profile_parent = self.get_ancestors().exact_type(CompetenceProfilePage).last() if self.learning_unit and self.learning_unit.course_category: self.slug = find_available_slug( @@ -96,7 +96,7 @@ class PerformanceCriteria(CourseBasePage): allow_unicode=True, ) ) - super(PerformanceCriteria, self).full_clean(*args, **kwargs) + super(PerformanceCriteria, self).save(clean, user, log_action, **kwargs) @classmethod def get_serializer_class(cls): diff --git a/server/vbv_lernwelt/course/models.py b/server/vbv_lernwelt/course/models.py index 2212a95e..6abf0e9d 100644 --- a/server/vbv_lernwelt/course/models.py +++ b/server/vbv_lernwelt/course/models.py @@ -127,9 +127,9 @@ class CoursePage(CourseBasePage): class Meta: verbose_name = _("Lehrgang-Seite") - def full_clean(self, *args, **kwargs): + def save(self, *args, **kwargs): self.slug = find_available_slug(slugify(self.title, allow_unicode=True)) - super(CoursePage, self).full_clean(*args, **kwargs) + super(CoursePage, self).save(*args, **kwargs) def __str__(self): return f"{self.title}" diff --git a/server/vbv_lernwelt/learnpath/models.py b/server/vbv_lernwelt/learnpath/models.py index 44d67520..7ed6dc51 100644 --- a/server/vbv_lernwelt/learnpath/models.py +++ b/server/vbv_lernwelt/learnpath/models.py @@ -35,11 +35,11 @@ class LearningPath(CourseBasePage): class Meta: verbose_name = "Learning Path" - def full_clean(self, *args, **kwargs): + def save(self, clean=True, user=None, log_action=False, **kwargs): self.slug = find_available_slug( slugify(f"{self.get_parent().slug}-lp", allow_unicode=True) ) - super(LearningPath, self).full_clean(*args, **kwargs) + super(LearningPath, self).save(clean, user, log_action, **kwargs) def __str__(self): return f"{self.title}" @@ -60,9 +60,9 @@ class Topic(CourseBasePage): FieldPanel("is_visible"), ] - def full_clean(self, *args, **kwargs): + def save(self, clean=True, user=None, log_action=False, **kwargs): self.slug = find_slug_with_parent_prefix(self, "topic") - super(Topic, self).full_clean(*args, **kwargs) + super(Topic, self).save(clean, user, log_action, **kwargs) def get_admin_display_title(self): return f"Thema: {self.draft_title}" @@ -104,9 +104,9 @@ class Circle(CourseBasePage): return "ERROR: could not parse slug" return f"/course/{m.group('coursePart')}/learn/{m.group('circlePart')}" - def full_clean(self, *args, **kwargs): + def save(self, clean=True, user=None, log_action=False, **kwargs): self.slug = find_slug_with_parent_prefix(self, "circle") - super(Circle, self).full_clean(*args, **kwargs) + super(Circle, self).save(clean, user, log_action, **kwargs) class Meta: verbose_name = "Circle" @@ -146,9 +146,9 @@ class LearningSequence(CourseBasePage): def get_admin_display_title(self): return f"LS: {self.draft_title}" - def full_clean(self, *args, **kwargs): + def save(self, clean=True, user=None, log_action=False, **kwargs): self.slug = find_slug_with_parent_prefix(self, "ls") - super(LearningSequence, self).full_clean(*args, **kwargs) + super(LearningSequence, self).save(clean, user, log_action, **kwargs) def get_frontend_url(self): r = re.compile( @@ -177,7 +177,7 @@ class LearningUnit(CourseBasePage): def __str__(self): return f"{self.title}" - def full_clean(self, *args, **kwargs): + def save(self, clean=True, user=None, log_action=False, **kwargs): course = None course_parent_page = self.get_ancestors().exact_type(CoursePage).last() if course_parent_page: @@ -194,7 +194,7 @@ class LearningUnit(CourseBasePage): self.slug = find_slug_with_parent_prefix( self, "lu", self.course_category.title ) - super(LearningUnit, self).full_clean(*args, **kwargs) + super(LearningUnit, self).save(clean, user, log_action, **kwargs) def get_frontend_url(self): r = re.compile( @@ -291,9 +291,9 @@ class LearningContent(CourseBasePage): return "ERROR: could not parse slug" return f"/course/{m.group('coursePart')}/learn/{m.group('circlePart')}/{m.group('lcPart')}" - def full_clean(self, *args, **kwargs): + def save(self, clean=True, user=None, log_action=False, **kwargs): self.slug = find_slug_with_parent_prefix(self, "lc") - super(LearningContent, self).full_clean(*args, **kwargs) + super().save(**kwargs) def find_slug_with_parent_prefix(page, type_prefix, slug_postfix=None): diff --git a/server/vbv_lernwelt/media_library/models.py b/server/vbv_lernwelt/media_library/models.py index cd862a1a..763cde4d 100644 --- a/server/vbv_lernwelt/media_library/models.py +++ b/server/vbv_lernwelt/media_library/models.py @@ -23,11 +23,11 @@ class MediaLibraryPage(CourseBasePage): FieldPanel("title", classname="full title"), ] - def full_clean(self, *args, **kwargs): + def save(self, clean=True, user=None, log_action=False, **kwargs): self.slug = find_available_slug( slugify(f"{self.get_parent().slug}-media", allow_unicode=True) ) - super(MediaLibraryPage, self).full_clean(*args, **kwargs) + super(MediaLibraryPage, self).save(clean, user, log_action, **kwargs) def get_frontend_url(self): return f"/media/{self.slug}" @@ -84,11 +84,11 @@ class MediaCategoryPage(CourseBasePage): StreamFieldPanel("body"), ] - def full_clean(self, *args, **kwargs): + def save(self, clean=True, user=None, log_action=False, **kwargs): self.slug = find_available_slug( slugify(f"{self.get_parent().slug}-cat-{self.title}", allow_unicode=True) ) - super(MediaCategoryPage, self).full_clean(*args, **kwargs) + super(MediaCategoryPage, self).save(clean, user, log_action, **kwargs) def get_frontend_url(self): r = re.compile(r"^(?P.+?)-media-cat-(?P.+)$") From 9ecb9a9add3e04e0e86f9d0209e74fc5985492e5 Mon Sep 17 00:00:00 2001 From: Elia Bieri Date: Tue, 14 Mar 2023 15:02:36 +0100 Subject: [PATCH 02/11] Add initial wagtail models --- server/config/settings/base.py | 1 + server/vbv_lernwelt/assignment/__init__.py | 0 server/vbv_lernwelt/assignment/admin.py | 3 + server/vbv_lernwelt/assignment/apps.py | 6 ++ .../assignment/migrations/0001_initial.py | 46 ++++++++++ .../assignment/migrations/__init__.py | 0 server/vbv_lernwelt/assignment/models.py | 84 +++++++++++++++++++ server/vbv_lernwelt/assignment/tests.py | 3 + .../assignment/tests/assignment_factories.py | 55 ++++++++++++ server/vbv_lernwelt/assignment/views.py | 3 + server/vbv_lernwelt/course/models.py | 1 + 11 files changed, 202 insertions(+) create mode 100644 server/vbv_lernwelt/assignment/__init__.py create mode 100644 server/vbv_lernwelt/assignment/admin.py create mode 100644 server/vbv_lernwelt/assignment/apps.py create mode 100644 server/vbv_lernwelt/assignment/migrations/0001_initial.py create mode 100644 server/vbv_lernwelt/assignment/migrations/__init__.py create mode 100644 server/vbv_lernwelt/assignment/models.py create mode 100644 server/vbv_lernwelt/assignment/tests.py create mode 100644 server/vbv_lernwelt/assignment/tests/assignment_factories.py create mode 100644 server/vbv_lernwelt/assignment/views.py diff --git a/server/config/settings/base.py b/server/config/settings/base.py index 0c82e079..5d32205a 100644 --- a/server/config/settings/base.py +++ b/server/config/settings/base.py @@ -113,6 +113,7 @@ LOCAL_APPS = [ "vbv_lernwelt.feedback", "vbv_lernwelt.files", "vbv_lernwelt.notify", + "vbv_lernwelt.assignment", ] # https://docs.djangoproject.com/en/dev/ref/settings/#installed-apps INSTALLED_APPS = DJANGO_APPS + THIRD_PARTY_APPS + LOCAL_APPS diff --git a/server/vbv_lernwelt/assignment/__init__.py b/server/vbv_lernwelt/assignment/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/server/vbv_lernwelt/assignment/admin.py b/server/vbv_lernwelt/assignment/admin.py new file mode 100644 index 00000000..8c38f3f3 --- /dev/null +++ b/server/vbv_lernwelt/assignment/admin.py @@ -0,0 +1,3 @@ +from django.contrib import admin + +# Register your models here. diff --git a/server/vbv_lernwelt/assignment/apps.py b/server/vbv_lernwelt/assignment/apps.py new file mode 100644 index 00000000..d80b629e --- /dev/null +++ b/server/vbv_lernwelt/assignment/apps.py @@ -0,0 +1,6 @@ +from django.apps import AppConfig + + +class AssignmentConfig(AppConfig): + default_auto_field = 'django.db.models.BigAutoField' + name = 'assignment' diff --git a/server/vbv_lernwelt/assignment/migrations/0001_initial.py b/server/vbv_lernwelt/assignment/migrations/0001_initial.py new file mode 100644 index 00000000..7f2bc488 --- /dev/null +++ b/server/vbv_lernwelt/assignment/migrations/0001_initial.py @@ -0,0 +1,46 @@ +# Generated by Django 3.2.13 on 2023-03-28 08:53 + +import assignment.models +from django.db import migrations, models +import django.db.models.deletion +import wagtail.blocks +import wagtail.fields + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ('wagtailcore', '0069_log_entry_jsonfield'), + ('files', '0001_initial'), + ] + + operations = [ + migrations.CreateModel( + name='AssignmentPage', + fields=[ + ('page_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='wagtailcore.page')), + ], + options={ + 'abstract': False, + }, + bases=('wagtailcore.page',), + ), + migrations.CreateModel( + name='Assignment', + fields=[ + ('page_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='wagtailcore.page')), + ('starting_position', models.CharField(max_length=1024)), + ('due', models.DateTimeField()), + ('performance_objectives', wagtail.fields.StreamField([('performance_objective', assignment.models.PerformanceObjectiveBlock())], use_json_field=True)), + ('effort_required', models.CharField(max_length=255)), + ('tasks', wagtail.fields.StreamField([('task', wagtail.blocks.StructBlock([('title', wagtail.blocks.CharBlock()), ('file_submission_required', wagtail.blocks.BooleanBlock()), ('content', wagtail.blocks.StreamBlock([('explanation', assignment.models.ExplanationBlock()), ('user_text_input', assignment.models.UserTextInputBlock()), ('user_confirmation', assignment.models.UserConfirmationBlock())], use_json_field=True))]))], use_json_field=True)), + ('assessment_tool', models.OneToOneField(on_delete=django.db.models.deletion.PROTECT, to='files.uploadfile')), + ], + options={ + 'verbose_name': 'Auftrag', + }, + bases=('wagtailcore.page',), + ), + ] diff --git a/server/vbv_lernwelt/assignment/migrations/__init__.py b/server/vbv_lernwelt/assignment/migrations/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/server/vbv_lernwelt/assignment/models.py b/server/vbv_lernwelt/assignment/models.py new file mode 100644 index 00000000..bed1ba4c --- /dev/null +++ b/server/vbv_lernwelt/assignment/models.py @@ -0,0 +1,84 @@ +from django.db import models +from django.db.models import DateTimeField, CharField +from wagtail import blocks +from wagtail.admin.panels import FieldPanel +from wagtail.blocks import StreamBlock +from wagtail.fields import StreamField +from wagtail.models import Page + +from vbv_lernwelt.files.models import UploadFile + + +class ExplanationBlock(blocks.CharBlock): + class Meta: + icon = "comment" + + +class PerformanceObjectiveBlock(blocks.CharBlock): + class Meta: + icon = "tick" + + +class UserTextInputBlock(blocks.StaticBlock): + class Meta: + icon = "edit" + + +class UserConfirmationBlock(blocks.CharBlock): + confirmation_text = blocks.BooleanBlock() + + class Meta: + icon = "tick-inverse" + + +class TaskBlock(blocks.StructBlock): + title = blocks.CharBlock() + file_submission_required = blocks.BooleanBlock() + content = StreamBlock( + [ + ("explanation", ExplanationBlock()), + ("user_text_input", UserTextInputBlock()), + ("user_confirmation", UserConfirmationBlock()), + ], + use_json_field=True, + ) + + class Meta: + icon = "tasks" + label = "Teilauftrag" + + +class Assignment(Page): + # TODO: Referenz auf durchfhrung + starting_position = CharField(max_length=1024) + due = DateTimeField() # Zwingend teil der Durchführung + performance_objectives = StreamField( + [ + ("performance_objective", PerformanceObjectiveBlock()), + ], + use_json_field=True, + ) + effort_required = CharField(max_length=255) + assessment_tool = models.OneToOneField(UploadFile, on_delete=models.PROTECT) + tasks = StreamField( + [ + ("task", TaskBlock()), + ], + use_json_field=True, + ) + + content_panels = Page.content_panels + [ + FieldPanel("starting_position"), + FieldPanel("due"), + FieldPanel("performance_objectives"), + FieldPanel("effort_required"), + FieldPanel("assessment_tool"), + FieldPanel("tasks"), + ] + + class Meta: + verbose_name = "Auftrag" + + +class AssignmentPage(Page): + subpage_types = ["assignment.Assignment"] diff --git a/server/vbv_lernwelt/assignment/tests.py b/server/vbv_lernwelt/assignment/tests.py new file mode 100644 index 00000000..7ce503c2 --- /dev/null +++ b/server/vbv_lernwelt/assignment/tests.py @@ -0,0 +1,3 @@ +from django.test import TestCase + +# Create your tests here. diff --git a/server/vbv_lernwelt/assignment/tests/assignment_factories.py b/server/vbv_lernwelt/assignment/tests/assignment_factories.py new file mode 100644 index 00000000..1c4d281c --- /dev/null +++ b/server/vbv_lernwelt/assignment/tests/assignment_factories.py @@ -0,0 +1,55 @@ +import wagtail_factories + +from vbv_lernwelt.assignment.models import AssignmentPage, Assignment, TaskBlock + + +class ExplanationBlockFactory(wagtail_factories.StructBlockFactory): + class Meta: + model = "assignment.ExplanationBlock" + + text = """Erläutere die Kundensituation und die Ausgangslage. + Hast du alle Informationen, die du für den Policen-Check benötigst? + Halte die wichtigsten Eckwerte des aktuellen Versicherungsverhältnisse in deiner Dokumentation fest (z.B wie lang wo versichert, Alter des Fahrzeugs, Kundenprofil, etc.) + """ + + +class UserTextInputBlockFactory(wagtail_factories.StructBlockFactory): + class Meta: + model = "assignment.UserTextInputBlock" + + +class UserConfirmationBlockFactory(wagtail_factories.StructBlockFactory): + class Meta: + model = "assignment.UserConfirmationBlock" + + confirmation_text = "Ja, ich habe Motorfahrzeugversicherungspolice von jemandem aus meiner Familie oder meinem Freundeskreis erhalten." + + +class TaskBlockFactory(wagtail_factories.StructBlockFactory): + class Meta: + model = TaskBlock + + title = "Teilauftrag" + file_submission_required = False + content = wagtail_factories.StreamFieldFactory( + [ + ("explanation", ExplanationBlockFactory()), + ("user_text_input", UserTextInputBlockFactory()), + ("user_confirmation", UserConfirmationBlockFactory()), + ], + use_json_field=True, + ) + + +class AssignmentFactory(wagtail_factories.PageFactory): + title = "Auftrag" + + class Meta: + model = Assignment + + +class AssignmentPageFactory(wagtail_factories.PageFactory): + title = "Aufträge" + + class Meta: + model = AssignmentPage diff --git a/server/vbv_lernwelt/assignment/views.py b/server/vbv_lernwelt/assignment/views.py new file mode 100644 index 00000000..91ea44a2 --- /dev/null +++ b/server/vbv_lernwelt/assignment/views.py @@ -0,0 +1,3 @@ +from django.shortcuts import render + +# Create your views here. diff --git a/server/vbv_lernwelt/course/models.py b/server/vbv_lernwelt/course/models.py index 6abf0e9d..bed607c4 100644 --- a/server/vbv_lernwelt/course/models.py +++ b/server/vbv_lernwelt/course/models.py @@ -121,6 +121,7 @@ class CoursePage(CourseBasePage): "learnpath.LearningPath", "competence.CompetenceProfilePage", "media_library.MediaLibraryPage", + "assignment.AssignmentPage", ] course = models.OneToOneField("course.Course", on_delete=models.PROTECT) From 4cf292bbf86975d09e2eaa0eec2f2b4277aeb639 Mon Sep 17 00:00:00 2001 From: Elia Bieri Date: Wed, 29 Mar 2023 15:28:11 +0200 Subject: [PATCH 03/11] Rework model --- prepare_server.sh | 1 + server/vbv_lernwelt/assignment/apps.py | 4 +- .../assignment/creators/create_assignments.py | 29 ++++++++++++++ .../management/commands/__init__.py | 0 .../commands/create_default_assignments.py | 9 +++++ .../assignment/migrations/0001_initial.py | 35 +++++++++-------- server/vbv_lernwelt/assignment/models.py | 32 +++++++++------- server/vbv_lernwelt/assignment/tests.py | 3 -- .../vbv_lernwelt/assignment/tests/__init__.py | 0 .../assignment/tests/assignment_factories.py | 38 ++++++++++++------- .../0013_alter_learningcontent_contents.py | 20 ++++++++++ .../0014_alter_learningcontent_contents.py | 20 ++++++++++ .../0015_alter_learningcontent_contents.py | 20 ++++++++++ .../learnpath/models_learning_unit_content.py | 8 ++-- 14 files changed, 170 insertions(+), 49 deletions(-) create mode 100644 server/vbv_lernwelt/assignment/creators/create_assignments.py create mode 100644 server/vbv_lernwelt/assignment/management/commands/__init__.py create mode 100644 server/vbv_lernwelt/assignment/management/commands/create_default_assignments.py delete mode 100644 server/vbv_lernwelt/assignment/tests.py create mode 100644 server/vbv_lernwelt/assignment/tests/__init__.py create mode 100644 server/vbv_lernwelt/learnpath/migrations/0013_alter_learningcontent_contents.py create mode 100644 server/vbv_lernwelt/learnpath/migrations/0014_alter_learningcontent_contents.py create mode 100644 server/vbv_lernwelt/learnpath/migrations/0015_alter_learningcontent_contents.py diff --git a/prepare_server.sh b/prepare_server.sh index ad375e5b..8fd54393 100755 --- a/prepare_server.sh +++ b/prepare_server.sh @@ -97,6 +97,7 @@ if [ "$SKIP_SETUP" = false ]; then echo "python server/manage.py create_default_courses $course_param" python server/manage.py create_default_courses $course_param python server/manage.py create_default_notifications + python server/manage.py create_default_assignments # make django translations (cd server && python manage.py compilemessages) diff --git a/server/vbv_lernwelt/assignment/apps.py b/server/vbv_lernwelt/assignment/apps.py index d80b629e..5a2ee9f3 100644 --- a/server/vbv_lernwelt/assignment/apps.py +++ b/server/vbv_lernwelt/assignment/apps.py @@ -2,5 +2,5 @@ from django.apps import AppConfig class AssignmentConfig(AppConfig): - default_auto_field = 'django.db.models.BigAutoField' - name = 'assignment' + default_auto_field = "django.db.models.BigAutoField" + name = "vbv_lernwelt.assignment" diff --git a/server/vbv_lernwelt/assignment/creators/create_assignments.py b/server/vbv_lernwelt/assignment/creators/create_assignments.py new file mode 100644 index 00000000..9c5e6ebd --- /dev/null +++ b/server/vbv_lernwelt/assignment/creators/create_assignments.py @@ -0,0 +1,29 @@ +from wagtail_factories import StreamFieldFactory + +from vbv_lernwelt.assignment.tests.assignment_factories import ( + PerformanceObjectiveBlockFactory, + AssignmentPageFactory, + AssignmentFactory, + TaskBlockFactory, +) +from vbv_lernwelt.course.consts import COURSE_UK +from vbv_lernwelt.course.models import CoursePage + + +def create_assignments(): + course_page = CoursePage.objects.get(course_id=COURSE_UK) + assignment_page = AssignmentPageFactory( + parent=course_page, + ) + AssignmentFactory( + parent=assignment_page, + title="Auftrag 1", + performance_objectives=StreamFieldFactory( + { + "performance_objective": PerformanceObjectiveBlockFactory(), + } + ), + effort_required="1 - 2 Stunden", + assessment_document_url="https://www.vbv.ch", + tasks=StreamFieldFactory({"task": TaskBlockFactory()}), + ) diff --git a/server/vbv_lernwelt/assignment/management/commands/__init__.py b/server/vbv_lernwelt/assignment/management/commands/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/server/vbv_lernwelt/assignment/management/commands/create_default_assignments.py b/server/vbv_lernwelt/assignment/management/commands/create_default_assignments.py new file mode 100644 index 00000000..1e2d6fc6 --- /dev/null +++ b/server/vbv_lernwelt/assignment/management/commands/create_default_assignments.py @@ -0,0 +1,9 @@ +import djclick as click + +from vbv_lernwelt.assignment.creators.create_assignments import create_assignments + + +@click.command() +def command(): + print("Creating default assignments") + create_assignments() diff --git a/server/vbv_lernwelt/assignment/migrations/0001_initial.py b/server/vbv_lernwelt/assignment/migrations/0001_initial.py index 7f2bc488..0ccccce3 100644 --- a/server/vbv_lernwelt/assignment/migrations/0001_initial.py +++ b/server/vbv_lernwelt/assignment/migrations/0001_initial.py @@ -1,8 +1,8 @@ -# Generated by Django 3.2.13 on 2023-03-28 08:53 +# Generated by Django 3.2.13 on 2023-03-30 13:30 -import assignment.models from django.db import migrations, models import django.db.models.deletion +import vbv_lernwelt.assignment.models import wagtail.blocks import wagtail.fields @@ -13,10 +13,24 @@ class Migration(migrations.Migration): dependencies = [ ('wagtailcore', '0069_log_entry_jsonfield'), - ('files', '0001_initial'), ] operations = [ + migrations.CreateModel( + name='Assignment', + fields=[ + ('page_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='wagtailcore.page')), + ('starting_position', models.TextField()), + ('performance_objectives', wagtail.fields.StreamField([('performance_objective', wagtail.blocks.StructBlock([('text', wagtail.blocks.TextBlock())]))], use_json_field=True)), + ('effort_required', models.TextField()), + ('assessment_document_url', models.TextField()), + ('tasks', wagtail.fields.StreamField([('task', wagtail.blocks.StructBlock([('title', wagtail.blocks.CharBlock()), ('file_submission_required', wagtail.blocks.BooleanBlock()), ('content', wagtail.blocks.StreamBlock([('explanation', wagtail.blocks.StructBlock([('text', wagtail.blocks.TextBlock())])), ('user_text_input', vbv_lernwelt.assignment.models.UserTextInputBlock()), ('user_confirmation', wagtail.blocks.StructBlock([('text', wagtail.blocks.TextBlock())]))], use_json_field=True))]))], use_json_field=True)), + ], + options={ + 'verbose_name': 'Auftrag', + }, + bases=('wagtailcore.page',), + ), migrations.CreateModel( name='AssignmentPage', fields=[ @@ -28,19 +42,10 @@ class Migration(migrations.Migration): bases=('wagtailcore.page',), ), migrations.CreateModel( - name='Assignment', + name='AssignmentSubmission', fields=[ - ('page_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='wagtailcore.page')), - ('starting_position', models.CharField(max_length=1024)), - ('due', models.DateTimeField()), - ('performance_objectives', wagtail.fields.StreamField([('performance_objective', assignment.models.PerformanceObjectiveBlock())], use_json_field=True)), - ('effort_required', models.CharField(max_length=255)), - ('tasks', wagtail.fields.StreamField([('task', wagtail.blocks.StructBlock([('title', wagtail.blocks.CharBlock()), ('file_submission_required', wagtail.blocks.BooleanBlock()), ('content', wagtail.blocks.StreamBlock([('explanation', assignment.models.ExplanationBlock()), ('user_text_input', assignment.models.UserTextInputBlock()), ('user_confirmation', assignment.models.UserConfirmationBlock())], use_json_field=True))]))], use_json_field=True)), - ('assessment_tool', models.OneToOneField(on_delete=django.db.models.deletion.PROTECT, to='files.uploadfile')), + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('created_at', models.DateTimeField(auto_now_add=True)), ], - options={ - 'verbose_name': 'Auftrag', - }, - bases=('wagtailcore.page',), ), ] diff --git a/server/vbv_lernwelt/assignment/models.py b/server/vbv_lernwelt/assignment/models.py index bed1ba4c..6dc99c17 100644 --- a/server/vbv_lernwelt/assignment/models.py +++ b/server/vbv_lernwelt/assignment/models.py @@ -1,20 +1,26 @@ from django.db import models -from django.db.models import DateTimeField, CharField +from django.db.models import Model, TextField from wagtail import blocks from wagtail.admin.panels import FieldPanel from wagtail.blocks import StreamBlock from wagtail.fields import StreamField from wagtail.models import Page -from vbv_lernwelt.files.models import UploadFile + +class AssignmentSubmission(Model): + created_at = models.DateTimeField(auto_now_add=True) -class ExplanationBlock(blocks.CharBlock): +class ExplanationBlock(blocks.StructBlock): + text = blocks.TextBlock() + class Meta: icon = "comment" -class PerformanceObjectiveBlock(blocks.CharBlock): +class PerformanceObjectiveBlock(blocks.StructBlock): + text = blocks.TextBlock() + class Meta: icon = "tick" @@ -24,8 +30,8 @@ class UserTextInputBlock(blocks.StaticBlock): icon = "edit" -class UserConfirmationBlock(blocks.CharBlock): - confirmation_text = blocks.BooleanBlock() +class UserConfirmationBlock(blocks.StructBlock): + text = blocks.TextBlock() class Meta: icon = "tick-inverse" @@ -49,17 +55,15 @@ class TaskBlock(blocks.StructBlock): class Assignment(Page): - # TODO: Referenz auf durchfhrung - starting_position = CharField(max_length=1024) - due = DateTimeField() # Zwingend teil der Durchführung + starting_position = TextField() performance_objectives = StreamField( [ ("performance_objective", PerformanceObjectiveBlock()), ], use_json_field=True, ) - effort_required = CharField(max_length=255) - assessment_tool = models.OneToOneField(UploadFile, on_delete=models.PROTECT) + effort_required = TextField() + assessment_document_url = TextField() tasks = StreamField( [ ("task", TaskBlock()), @@ -69,16 +73,18 @@ class Assignment(Page): content_panels = Page.content_panels + [ FieldPanel("starting_position"), - FieldPanel("due"), FieldPanel("performance_objectives"), FieldPanel("effort_required"), - FieldPanel("assessment_tool"), + FieldPanel("assessment_document_url"), FieldPanel("tasks"), ] + subpage_types = [] + class Meta: verbose_name = "Auftrag" class AssignmentPage(Page): subpage_types = ["assignment.Assignment"] + parent_page_types = ["course.CoursePage"] diff --git a/server/vbv_lernwelt/assignment/tests.py b/server/vbv_lernwelt/assignment/tests.py deleted file mode 100644 index 7ce503c2..00000000 --- a/server/vbv_lernwelt/assignment/tests.py +++ /dev/null @@ -1,3 +0,0 @@ -from django.test import TestCase - -# Create your tests here. diff --git a/server/vbv_lernwelt/assignment/tests/__init__.py b/server/vbv_lernwelt/assignment/tests/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/server/vbv_lernwelt/assignment/tests/assignment_factories.py b/server/vbv_lernwelt/assignment/tests/assignment_factories.py index 1c4d281c..69e4130a 100644 --- a/server/vbv_lernwelt/assignment/tests/assignment_factories.py +++ b/server/vbv_lernwelt/assignment/tests/assignment_factories.py @@ -1,48 +1,60 @@ import wagtail_factories from vbv_lernwelt.assignment.models import AssignmentPage, Assignment, TaskBlock +from vbv_lernwelt.assignment.models import ( + ExplanationBlock, + UserConfirmationBlock, + PerformanceObjectiveBlock, +) class ExplanationBlockFactory(wagtail_factories.StructBlockFactory): - class Meta: - model = "assignment.ExplanationBlock" - text = """Erläutere die Kundensituation und die Ausgangslage. Hast du alle Informationen, die du für den Policen-Check benötigst? Halte die wichtigsten Eckwerte des aktuellen Versicherungsverhältnisse in deiner Dokumentation fest (z.B wie lang wo versichert, Alter des Fahrzeugs, Kundenprofil, etc.) """ - -class UserTextInputBlockFactory(wagtail_factories.StructBlockFactory): class Meta: - model = "assignment.UserTextInputBlock" + model = ExplanationBlock class UserConfirmationBlockFactory(wagtail_factories.StructBlockFactory): - class Meta: - model = "assignment.UserConfirmationBlock" + text = "Ja, ich habe Motorfahrzeugversicherungspolice von jemandem aus meiner Familie oder meinem Freundeskreis erhalten." - confirmation_text = "Ja, ich habe Motorfahrzeugversicherungspolice von jemandem aus meiner Familie oder meinem Freundeskreis erhalten." + class Meta: + model = UserConfirmationBlock class TaskBlockFactory(wagtail_factories.StructBlockFactory): - class Meta: - model = TaskBlock - title = "Teilauftrag" file_submission_required = False content = wagtail_factories.StreamFieldFactory( [ ("explanation", ExplanationBlockFactory()), - ("user_text_input", UserTextInputBlockFactory()), + ("user_text_input", "static_block"), ("user_confirmation", UserConfirmationBlockFactory()), ], use_json_field=True, ) + class Meta: + model = TaskBlock + + +class PerformanceObjectiveBlockFactory(wagtail_factories.StructBlockFactory): + text = "Die Teilnehmer können die wichtigsten Eckwerte eines Versicherungsverhältnisses erfassen." + + class Meta: + model = PerformanceObjectiveBlock + class AssignmentFactory(wagtail_factories.PageFactory): title = "Auftrag" + starting_position = """Jemand aus deiner Familie oder aus deinem Freundeskreis möchte sein + Versicherungspolice überprüfen lassen. Diese Person kommt nun mit ihrer Police auf dich zu + und bittet dich als Versicherungsprofi, diese kritisch zu überprüfen und ihr gg. Anpassungsvorschläge + zu unterbreiten. In diesem Kompetenznachweis kannst du nun dein Wissen und Können im Bereich + der Motorfahrzeugversicherung unter Beweis stellen.""" class Meta: model = Assignment diff --git a/server/vbv_lernwelt/learnpath/migrations/0013_alter_learningcontent_contents.py b/server/vbv_lernwelt/learnpath/migrations/0013_alter_learningcontent_contents.py new file mode 100644 index 00000000..dc53258c --- /dev/null +++ b/server/vbv_lernwelt/learnpath/migrations/0013_alter_learningcontent_contents.py @@ -0,0 +1,20 @@ +# Generated by Django 3.2.13 on 2023-03-29 13:39 + +from django.db import migrations +import wagtail.blocks +import wagtail.fields + + +class Migration(migrations.Migration): + + dependencies = [ + ('learnpath', '0012_auto_20230309_0711'), + ] + + 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())])), ('placeholder', wagtail.blocks.StructBlock([('description', wagtail.blocks.TextBlock()), ('url', wagtail.blocks.TextBlock())])), ('feedback', wagtail.blocks.StructBlock([]))], use_json_field=None), + ), + ] diff --git a/server/vbv_lernwelt/learnpath/migrations/0014_alter_learningcontent_contents.py b/server/vbv_lernwelt/learnpath/migrations/0014_alter_learningcontent_contents.py new file mode 100644 index 00000000..bc33968f --- /dev/null +++ b/server/vbv_lernwelt/learnpath/migrations/0014_alter_learningcontent_contents.py @@ -0,0 +1,20 @@ +# Generated by Django 3.2.13 on 2023-03-29 13:44 + +from django.db import migrations +import wagtail.blocks +import wagtail.fields + + +class Migration(migrations.Migration): + + dependencies = [ + ('learnpath', '0013_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())])), ('placeholder', wagtail.blocks.StructBlock([('description', wagtail.blocks.TextBlock()), ('url', wagtail.blocks.TextBlock())])), ('feedback', wagtail.blocks.StructBlock([]))], use_json_field=None), + ), + ] diff --git a/server/vbv_lernwelt/learnpath/migrations/0015_alter_learningcontent_contents.py b/server/vbv_lernwelt/learnpath/migrations/0015_alter_learningcontent_contents.py new file mode 100644 index 00000000..3d1c1e58 --- /dev/null +++ b/server/vbv_lernwelt/learnpath/migrations/0015_alter_learningcontent_contents.py @@ -0,0 +1,20 @@ +# Generated by Django 3.2.13 on 2023-03-30 13:30 + +from django.db import migrations +import wagtail.blocks +import wagtail.fields + + +class Migration(migrations.Migration): + + dependencies = [ + ('learnpath', '0014_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([('assignment_slug', wagtail.blocks.TextBlock())])), ('placeholder', wagtail.blocks.StructBlock([('description', wagtail.blocks.TextBlock()), ('url', wagtail.blocks.TextBlock())])), ('feedback', wagtail.blocks.StructBlock([]))], use_json_field=None), + ), + ] diff --git a/server/vbv_lernwelt/learnpath/models_learning_unit_content.py b/server/vbv_lernwelt/learnpath/models_learning_unit_content.py index ce7351a8..5efef576 100644 --- a/server/vbv_lernwelt/learnpath/models_learning_unit_content.py +++ b/server/vbv_lernwelt/learnpath/models_learning_unit_content.py @@ -2,9 +2,11 @@ from wagtail import blocks class AssignmentBlock(blocks.StructBlock): - description = blocks.TextBlock() - url = blocks.TextBlock() - text = blocks.RichTextBlock(required=False) + # TODO: Find way to let user select assignment through foreign key + # Wagtail block data is not stored as "true" database objects, + # but only as JSON text stored against the page, so there's no way to define relations such as ForeignKeys. + # A possible solution are InlinePanels: https://docs.wagtail.org/en/stable/reference/pages/panels.html#inlinepanel + assignment_slug = blocks.TextBlock() class Meta: icon = "media" From 539ddbeaf9ab8e299c6b1bb000e9db3e029a9a1a Mon Sep 17 00:00:00 2001 From: Daniel Egger Date: Tue, 4 Apr 2023 10:29:06 +0200 Subject: [PATCH 04/11] Refactor the models --- .../assignment/creators/create_assignments.py | 4 +- .../assignment/migrations/0001_initial.py | 22 ++-- server/vbv_lernwelt/assignment/models.py | 40 ++++-- .../assignment/tests/assignment_factories.py | 6 +- server/vbv_lernwelt/course/models.py | 2 +- .../0002_alter_learningcontent_contents.py | 124 +----------------- .../0013_alter_learningcontent_contents.py | 20 --- .../0014_alter_learningcontent_contents.py | 20 --- .../0015_alter_learningcontent_contents.py | 20 --- .../learnpath/models_learning_unit_content.py | 10 +- 10 files changed, 52 insertions(+), 216 deletions(-) delete mode 100644 server/vbv_lernwelt/learnpath/migrations/0013_alter_learningcontent_contents.py delete mode 100644 server/vbv_lernwelt/learnpath/migrations/0014_alter_learningcontent_contents.py delete mode 100644 server/vbv_lernwelt/learnpath/migrations/0015_alter_learningcontent_contents.py diff --git a/server/vbv_lernwelt/assignment/creators/create_assignments.py b/server/vbv_lernwelt/assignment/creators/create_assignments.py index 9c5e6ebd..6dd46129 100644 --- a/server/vbv_lernwelt/assignment/creators/create_assignments.py +++ b/server/vbv_lernwelt/assignment/creators/create_assignments.py @@ -2,7 +2,7 @@ from wagtail_factories import StreamFieldFactory from vbv_lernwelt.assignment.tests.assignment_factories import ( PerformanceObjectiveBlockFactory, - AssignmentPageFactory, + AssignmentListPageFactory, AssignmentFactory, TaskBlockFactory, ) @@ -12,7 +12,7 @@ from vbv_lernwelt.course.models import CoursePage def create_assignments(): course_page = CoursePage.objects.get(course_id=COURSE_UK) - assignment_page = AssignmentPageFactory( + assignment_page = AssignmentListPageFactory( parent=course_page, ) AssignmentFactory( diff --git a/server/vbv_lernwelt/assignment/migrations/0001_initial.py b/server/vbv_lernwelt/assignment/migrations/0001_initial.py index 0ccccce3..0173e9b8 100644 --- a/server/vbv_lernwelt/assignment/migrations/0001_initial.py +++ b/server/vbv_lernwelt/assignment/migrations/0001_initial.py @@ -1,4 +1,4 @@ -# Generated by Django 3.2.13 on 2023-03-30 13:30 +# Generated by Django 3.2.13 on 2023-04-04 11:49 from django.db import migrations, models import django.db.models.deletion @@ -20,11 +20,12 @@ class Migration(migrations.Migration): name='Assignment', fields=[ ('page_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='wagtailcore.page')), - ('starting_position', models.TextField()), - ('performance_objectives', wagtail.fields.StreamField([('performance_objective', wagtail.blocks.StructBlock([('text', wagtail.blocks.TextBlock())]))], use_json_field=True)), - ('effort_required', models.TextField()), - ('assessment_document_url', models.TextField()), - ('tasks', wagtail.fields.StreamField([('task', wagtail.blocks.StructBlock([('title', wagtail.blocks.CharBlock()), ('file_submission_required', wagtail.blocks.BooleanBlock()), ('content', wagtail.blocks.StreamBlock([('explanation', wagtail.blocks.StructBlock([('text', wagtail.blocks.TextBlock())])), ('user_text_input', vbv_lernwelt.assignment.models.UserTextInputBlock()), ('user_confirmation', wagtail.blocks.StructBlock([('text', wagtail.blocks.TextBlock())]))], use_json_field=True))]))], use_json_field=True)), + ('starting_position', models.TextField(help_text='Erläuterung der Ausgangslage')), + ('effort_required', models.CharField(blank=True, help_text='Zeitaufwand als Text', max_length=100)), + ('performance_objectives', wagtail.fields.StreamField([('performance_objective', wagtail.blocks.StructBlock([('text', wagtail.blocks.TextBlock())]))], blank=True, help_text='Leistungsziele des Auftrags', use_json_field=True)), + ('assessment_description', models.TextField(blank=True, help_text='Beschreibung der Bewertung')), + ('assessment_document_url', models.CharField(blank=True, help_text='URL zum Beeurteilungsinstrument', max_length=255)), + ('tasks', wagtail.fields.StreamField([('task', wagtail.blocks.StructBlock([('title', wagtail.blocks.TextBlock()), ('file_submission_required', wagtail.blocks.BooleanBlock(required=False)), ('content', wagtail.blocks.StreamBlock([('explanation', wagtail.blocks.StructBlock([('text', wagtail.blocks.TextBlock())])), ('user_text_input', vbv_lernwelt.assignment.models.UserTextInputBlock()), ('user_confirmation', wagtail.blocks.StructBlock([('text', wagtail.blocks.TextBlock())]))], blank=True))]))], blank=True, help_text='Teilaufgaben', use_json_field=True)), ], options={ 'verbose_name': 'Auftrag', @@ -32,7 +33,7 @@ class Migration(migrations.Migration): bases=('wagtailcore.page',), ), migrations.CreateModel( - name='AssignmentPage', + name='AssignmentListPage', fields=[ ('page_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='wagtailcore.page')), ], @@ -41,11 +42,4 @@ class Migration(migrations.Migration): }, bases=('wagtailcore.page',), ), - migrations.CreateModel( - name='AssignmentSubmission', - fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('created_at', models.DateTimeField(auto_now_add=True)), - ], - ), ] diff --git a/server/vbv_lernwelt/assignment/models.py b/server/vbv_lernwelt/assignment/models.py index 6dc99c17..5530180f 100644 --- a/server/vbv_lernwelt/assignment/models.py +++ b/server/vbv_lernwelt/assignment/models.py @@ -1,14 +1,12 @@ from django.db import models -from django.db.models import Model, TextField from wagtail import blocks from wagtail.admin.panels import FieldPanel -from wagtail.blocks import StreamBlock from wagtail.fields import StreamField from wagtail.models import Page -class AssignmentSubmission(Model): - created_at = models.DateTimeField(auto_now_add=True) +# class AssignmentSubmission(modModel): +# created_at = models.DateTimeField(auto_now_add=True) class ExplanationBlock(blocks.StructBlock): @@ -38,15 +36,15 @@ class UserConfirmationBlock(blocks.StructBlock): class TaskBlock(blocks.StructBlock): - title = blocks.CharBlock() - file_submission_required = blocks.BooleanBlock() - content = StreamBlock( + title = blocks.TextBlock() + file_submission_required = blocks.BooleanBlock(required=False) + content = blocks.StreamBlock( [ ("explanation", ExplanationBlock()), ("user_text_input", UserTextInputBlock()), ("user_confirmation", UserConfirmationBlock()), ], - use_json_field=True, + blank=True, ) class Meta: @@ -55,26 +53,42 @@ class TaskBlock(blocks.StructBlock): class Assignment(Page): - starting_position = TextField() + starting_position = models.TextField(help_text="Erläuterung der Ausgangslage") + effort_required = models.CharField( + max_length=100, help_text="Zeitaufwand als Text", blank=True + ) + performance_objectives = StreamField( [ ("performance_objective", PerformanceObjectiveBlock()), ], use_json_field=True, + blank=True, + help_text="Leistungsziele des Auftrags", ) - effort_required = TextField() - assessment_document_url = TextField() + assessment_description = models.TextField( + blank=True, help_text="Beschreibung der Bewertung" + ) + assessment_document_url = models.CharField( + max_length=255, + blank=True, + help_text="URL zum Beeurteilungsinstrument", + ) + tasks = StreamField( [ ("task", TaskBlock()), ], use_json_field=True, + blank=True, + help_text="Teilaufgaben", ) content_panels = Page.content_panels + [ FieldPanel("starting_position"), - FieldPanel("performance_objectives"), FieldPanel("effort_required"), + FieldPanel("performance_objectives"), + FieldPanel("assessment_description"), FieldPanel("assessment_document_url"), FieldPanel("tasks"), ] @@ -85,6 +99,6 @@ class Assignment(Page): verbose_name = "Auftrag" -class AssignmentPage(Page): +class AssignmentListPage(Page): subpage_types = ["assignment.Assignment"] parent_page_types = ["course.CoursePage"] diff --git a/server/vbv_lernwelt/assignment/tests/assignment_factories.py b/server/vbv_lernwelt/assignment/tests/assignment_factories.py index 69e4130a..6a4e1ad1 100644 --- a/server/vbv_lernwelt/assignment/tests/assignment_factories.py +++ b/server/vbv_lernwelt/assignment/tests/assignment_factories.py @@ -1,6 +1,6 @@ import wagtail_factories -from vbv_lernwelt.assignment.models import AssignmentPage, Assignment, TaskBlock +from vbv_lernwelt.assignment.models import Assignment, TaskBlock, AssignmentListPage from vbv_lernwelt.assignment.models import ( ExplanationBlock, UserConfirmationBlock, @@ -60,8 +60,8 @@ class AssignmentFactory(wagtail_factories.PageFactory): model = Assignment -class AssignmentPageFactory(wagtail_factories.PageFactory): +class AssignmentListPageFactory(wagtail_factories.PageFactory): title = "Aufträge" class Meta: - model = AssignmentPage + model = AssignmentListPage diff --git a/server/vbv_lernwelt/course/models.py b/server/vbv_lernwelt/course/models.py index bed607c4..f4457929 100644 --- a/server/vbv_lernwelt/course/models.py +++ b/server/vbv_lernwelt/course/models.py @@ -121,7 +121,7 @@ class CoursePage(CourseBasePage): "learnpath.LearningPath", "competence.CompetenceProfilePage", "media_library.MediaLibraryPage", - "assignment.AssignmentPage", + "assignment.AssignmentListPage", ] course = models.OneToOneField("course.Course", on_delete=models.PROTECT) diff --git a/server/vbv_lernwelt/learnpath/migrations/0002_alter_learningcontent_contents.py b/server/vbv_lernwelt/learnpath/migrations/0002_alter_learningcontent_contents.py index 6e4cf373..559b4eb2 100644 --- a/server/vbv_lernwelt/learnpath/migrations/0002_alter_learningcontent_contents.py +++ b/server/vbv_lernwelt/learnpath/migrations/0002_alter_learningcontent_contents.py @@ -1,132 +1,20 @@ -# Generated by Django 3.2.13 on 2023-04-03 16:05 +# Generated by Django 3.2.13 on 2023-04-04 08:28 +from django.db import migrations import wagtail.blocks import wagtail.fields -from django.db import migrations class Migration(migrations.Migration): dependencies = [ - ("learnpath", "0001_initial"), + ('learnpath', '0001_initial'), ] 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()), - ] - ), - ), - ("feedback", wagtail.blocks.StructBlock([])), - ( - "attendance_day", - wagtail.blocks.StructBlock( - [("description", wagtail.blocks.TextBlock())] - ), - ), - ], - use_json_field=None, - ), + 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()), ('assignment', wagtail.blocks.PageChooserBlock(help_text='Choose the corresponding assignment.', required=True))])), ('placeholder', wagtail.blocks.StructBlock([('description', wagtail.blocks.TextBlock()), ('url', wagtail.blocks.TextBlock())])), ('feedback', wagtail.blocks.StructBlock([])), ('attendance_day', wagtail.blocks.StructBlock([('description', wagtail.blocks.TextBlock())]))], use_json_field=None), ), ] diff --git a/server/vbv_lernwelt/learnpath/migrations/0013_alter_learningcontent_contents.py b/server/vbv_lernwelt/learnpath/migrations/0013_alter_learningcontent_contents.py deleted file mode 100644 index dc53258c..00000000 --- a/server/vbv_lernwelt/learnpath/migrations/0013_alter_learningcontent_contents.py +++ /dev/null @@ -1,20 +0,0 @@ -# Generated by Django 3.2.13 on 2023-03-29 13:39 - -from django.db import migrations -import wagtail.blocks -import wagtail.fields - - -class Migration(migrations.Migration): - - dependencies = [ - ('learnpath', '0012_auto_20230309_0711'), - ] - - 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())])), ('placeholder', wagtail.blocks.StructBlock([('description', wagtail.blocks.TextBlock()), ('url', wagtail.blocks.TextBlock())])), ('feedback', wagtail.blocks.StructBlock([]))], use_json_field=None), - ), - ] diff --git a/server/vbv_lernwelt/learnpath/migrations/0014_alter_learningcontent_contents.py b/server/vbv_lernwelt/learnpath/migrations/0014_alter_learningcontent_contents.py deleted file mode 100644 index bc33968f..00000000 --- a/server/vbv_lernwelt/learnpath/migrations/0014_alter_learningcontent_contents.py +++ /dev/null @@ -1,20 +0,0 @@ -# Generated by Django 3.2.13 on 2023-03-29 13:44 - -from django.db import migrations -import wagtail.blocks -import wagtail.fields - - -class Migration(migrations.Migration): - - dependencies = [ - ('learnpath', '0013_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())])), ('placeholder', wagtail.blocks.StructBlock([('description', wagtail.blocks.TextBlock()), ('url', wagtail.blocks.TextBlock())])), ('feedback', wagtail.blocks.StructBlock([]))], use_json_field=None), - ), - ] diff --git a/server/vbv_lernwelt/learnpath/migrations/0015_alter_learningcontent_contents.py b/server/vbv_lernwelt/learnpath/migrations/0015_alter_learningcontent_contents.py deleted file mode 100644 index 3d1c1e58..00000000 --- a/server/vbv_lernwelt/learnpath/migrations/0015_alter_learningcontent_contents.py +++ /dev/null @@ -1,20 +0,0 @@ -# Generated by Django 3.2.13 on 2023-03-30 13:30 - -from django.db import migrations -import wagtail.blocks -import wagtail.fields - - -class Migration(migrations.Migration): - - dependencies = [ - ('learnpath', '0014_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([('assignment_slug', wagtail.blocks.TextBlock())])), ('placeholder', wagtail.blocks.StructBlock([('description', wagtail.blocks.TextBlock()), ('url', wagtail.blocks.TextBlock())])), ('feedback', wagtail.blocks.StructBlock([]))], use_json_field=None), - ), - ] diff --git a/server/vbv_lernwelt/learnpath/models_learning_unit_content.py b/server/vbv_lernwelt/learnpath/models_learning_unit_content.py index 5efef576..0fb69bd9 100644 --- a/server/vbv_lernwelt/learnpath/models_learning_unit_content.py +++ b/server/vbv_lernwelt/learnpath/models_learning_unit_content.py @@ -1,12 +1,12 @@ from wagtail import blocks +from wagtail.blocks import PageChooserBlock class AssignmentBlock(blocks.StructBlock): - # TODO: Find way to let user select assignment through foreign key - # Wagtail block data is not stored as "true" database objects, - # but only as JSON text stored against the page, so there's no way to define relations such as ForeignKeys. - # A possible solution are InlinePanels: https://docs.wagtail.org/en/stable/reference/pages/panels.html#inlinepanel - assignment_slug = blocks.TextBlock() + description = blocks.TextBlock() + assignment = PageChooserBlock( + required=True, help_text="Choose the corresponding assignment." + ) class Meta: icon = "media" From 13b580468d8cad4dfc82e40487f07edce396f44c Mon Sep 17 00:00:00 2001 From: Daniel Egger Date: Tue, 4 Apr 2023 21:17:33 +0200 Subject: [PATCH 05/11] Create test assignment with tasks in code --- server/requirements/requirements-dev.txt | 2 +- server/requirements/requirements.in | 2 +- server/requirements/requirements.txt | 2 +- .../assignment/creators/create_assignments.py | 277 +++++++++++++++++- server/vbv_lernwelt/assignment/models.py | 15 +- .../assignment/tests/assignment_factories.py | 51 ++-- server/vbv_lernwelt/core/utils.py | 5 + 7 files changed, 318 insertions(+), 36 deletions(-) diff --git a/server/requirements/requirements-dev.txt b/server/requirements/requirements-dev.txt index bb2772fc..3b4ab629 100644 --- a/server/requirements/requirements-dev.txt +++ b/server/requirements/requirements-dev.txt @@ -540,7 +540,7 @@ wagtail==3.0.1 # wagtail-grapple # wagtail-headless-preview # wagtail-localize -wagtail-factories==2.0.1 +wagtail-factories==4.0.0 # via -r requirements.in wagtail-grapple==0.18.0 # via -r requirements.in diff --git a/server/requirements/requirements.in b/server/requirements/requirements.in index 3b108145..08f03594 100644 --- a/server/requirements/requirements.in +++ b/server/requirements/requirements.in @@ -37,7 +37,7 @@ python-json-logger concurrent-log-handler wagtail>=3,<4 -wagtail-factories +wagtail-factories>=4 wagtail-localize wagtail_grapple diff --git a/server/requirements/requirements.txt b/server/requirements/requirements.txt index 584fae19..dfd3c389 100644 --- a/server/requirements/requirements.txt +++ b/server/requirements/requirements.txt @@ -282,7 +282,7 @@ wagtail==3.0.1 # wagtail-grapple # wagtail-headless-preview # wagtail-localize -wagtail-factories==2.0.1 +wagtail-factories==4.0.0 # via -r requirements.in wagtail-grapple==0.18.0 # via -r requirements.in diff --git a/server/vbv_lernwelt/assignment/creators/create_assignments.py b/server/vbv_lernwelt/assignment/creators/create_assignments.py index 6dd46129..4e2a415e 100644 --- a/server/vbv_lernwelt/assignment/creators/create_assignments.py +++ b/server/vbv_lernwelt/assignment/creators/create_assignments.py @@ -1,11 +1,17 @@ -from wagtail_factories import StreamFieldFactory +from wagtail.blocks import StreamValue +from vbv_lernwelt.assignment.models import ( + TaskContentStreamBlock, +) from vbv_lernwelt.assignment.tests.assignment_factories import ( PerformanceObjectiveBlockFactory, AssignmentListPageFactory, AssignmentFactory, TaskBlockFactory, + ExplanationBlockFactory, + UserTextInputBlockFactory, ) +from vbv_lernwelt.core.utils import replace_whitespace from vbv_lernwelt.course.consts import COURSE_UK from vbv_lernwelt.course.models import CoursePage @@ -15,15 +21,268 @@ def create_assignments(): assignment_page = AssignmentListPageFactory( parent=course_page, ) - AssignmentFactory( + + assignment = AssignmentFactory( parent=assignment_page, - title="Auftrag 1", - performance_objectives=StreamFieldFactory( - { - "performance_objective": PerformanceObjectiveBlockFactory(), - } + title="Überprüfen einer Motorfahrzeugs-Versicherungspolice", + effort_required="ca. 5 Stunden", + starting_position=replace_whitespace( + """ + Jemand aus deiner Familie oder aus deinem Freundeskreis möchte sein + Versicherungspolice überprüfen lassen. Diese Person kommt nun mit ihrer Police auf dich zu + und bittet dich als Versicherungsprofi, diese kritisch zu überprüfen und ihr ggf. Anpassungsvorschläge + zu unterbreiten. In diesem Kompetenznachweis kannst du nun dein Wissen und Können im Bereich + der Motorfahrzeugversicherung unter Beweis stellen. + """ ), - effort_required="1 - 2 Stunden", + performance_objectives=[ + ( + "performance_objective", + PerformanceObjectiveBlockFactory( + text="Sie erläutern die Leistungen und Produkte im Versicherungsbereich." + ), + ), + ( + "performance_objective", + PerformanceObjectiveBlockFactory( + text="Sie beurteilen gängige Versicherungslösungen fachkundig." + ), + ), + ], assessment_document_url="https://www.vbv.ch", - tasks=StreamFieldFactory({"task": TaskBlockFactory()}), + assessment_description="Diese geleitete Fallarbeit wird auf Grund des folgenden Beurteilungsintrument bewertet.", ) + + assignment.tasks = [] + assignment.tasks.append( + ( + "task", + TaskBlockFactory( + title="Teilaufgabe 1: Beispiel einer Versicherungspolice finden", + # it is hard to create a StreamValue programmatically, we have to + # create a `StreamValue` manually. Ask the Daniel and/or Ramon + content=StreamValue( + TaskContentStreamBlock(), + stream_data=[ + ( + "explanation", + ExplanationBlockFactory(text="Dies ist ein Beispieltext."), + ), + ( + "user_confirmation", + ExplanationBlockFactory( + text="Ja, ich habe Motorfahrzeugversicherungspolice von jemandem aus meiner Familie oder meinem Freundeskreis erhalten." + ), + ), + ], + ), + ), + ) + ) + + assignment.tasks.append( + ( + "task", + TaskBlockFactory( + title="Teilaufgabe 2: Kundensituation und Ausgangslage", + content=StreamValue( + TaskContentStreamBlock(), + stream_data=[ + ( + "explanation", + ExplanationBlockFactory( + text=replace_whitespace( + """ + Erläutere die Kundensituation und die Ausgangslage. + * Hast du alle Informationen, die du für den Policen-Check benötigst? + * Halte die wichtigsten Eckwerte des aktuellen Versicherungsverhältnisse in deiner Dokumentation fest (z.B wie lang wo versichert, Alter des Fahrzeugs, Kundenprofil, etc.) + """ + ) + ), + ), + ("user_text_input", UserTextInputBlockFactory()), + ], + ), + ), + ) + ) + + assignment.tasks.append( + ( + "task", + TaskBlockFactory( + title="Teilaufgabe 3: Aktuelle Versicherung", + # TODO: add document upload + content=StreamValue( + TaskContentStreamBlock(), + stream_data=[ + ( + "explanation", + ExplanationBlockFactory( + text=replace_whitespace( + """ + Zeige nun detailliert auf, wie dein Kundenbeispiel momentan versichert ist. + """ + ) + ), + ), + ("user_text_input", UserTextInputBlockFactory()), + ], + ), + ), + ) + ) + + assignment.tasks.append( + ( + "task", + TaskBlockFactory( + title="Teilaufgabe 4: Deine Empfehlungen", + content=StreamValue( + TaskContentStreamBlock(), + stream_data=[ + ( + "explanation", + ExplanationBlockFactory( + text=replace_whitespace( + """ + Erarbeite nun basierend auf deinen Erkenntnissen eine Empfehlung für die Person. + """ + ) + ), + ), + ( + "user_text_input", + UserTextInputBlockFactory( + text=replace_whitespace( + """ + Gibt es zusätzliche Deckungen, die du der Person empfehlen würdest? Begründe deine Empfehlung + """ + ) + ), + ), + ( + "user_text_input", + UserTextInputBlockFactory( + text=replace_whitespace( + """ + Gibt es Deckungen, die du streichen würdest? Begründe deine Empfehlung. + """ + ) + ), + ), + ( + "user_text_input", + UserTextInputBlockFactory( + text=replace_whitespace( + """ + Wenn die Person gemäss deiner Einschätzung genau richtig versichert ist, argumentiere, warum dies der Fall ist. + """ + ) + ), + ), + ], + ), + ), + ) + ) + + assignment.tasks.append( + ( + "task", + TaskBlockFactory( + title="Teilaufgabe 5: Reflexion", + content=StreamValue( + TaskContentStreamBlock(), + stream_data=[ + ( + "explanation", + ExplanationBlockFactory( + text=replace_whitespace( + """ + Reflektiere dein Handeln und halte deine Erkenntnisse fest. Frage dich dabei: + """ + ) + ), + ), + ( + "user_text_input", + UserTextInputBlockFactory( + text=replace_whitespace( + """ + War die Bearbeitung dieser geleiteten Fallarbeit erfolgreich? Begründe deine Einschätzung. + """ + ) + ), + ), + ( + "user_text_input", + UserTextInputBlockFactory( + text=replace_whitespace( + """ + Was ist dir bei der Bearbeitung des Auftrags gut gelungen? + """ + ) + ), + ), + ( + "user_text_input", + UserTextInputBlockFactory( + text=replace_whitespace( + """ + Was ist dir bei der Bearbeitung des Auftrags weniger gut gelungen? + """ + ) + ), + ), + ], + ), + ), + ) + ) + + assignment.tasks.append( + ( + "task", + TaskBlockFactory( + title="Teilaufgabe 6: Learnings", + content=StreamValue( + TaskContentStreamBlock(), + stream_data=[ + ( + "explanation", + ExplanationBlockFactory( + text=replace_whitespace( + """ + Leite aus der Teilaufgabe 5 deine persönlichen Learnings ab. + """ + ) + ), + ), + ( + "user_text_input", + UserTextInputBlockFactory( + text=replace_whitespace( + """ + Was würdest du beim nächsten Mal anders machen? + """ + ) + ), + ), + ( + "user_text_input", + UserTextInputBlockFactory( + text=replace_whitespace( + """ + Was hast du beim Bearbeiten des Auftrags Neues gelernt? + """ + ) + ), + ), + ], + ), + ), + ) + ) + + assignment.save() diff --git a/server/vbv_lernwelt/assignment/models.py b/server/vbv_lernwelt/assignment/models.py index 5530180f..4c74134f 100644 --- a/server/vbv_lernwelt/assignment/models.py +++ b/server/vbv_lernwelt/assignment/models.py @@ -24,6 +24,8 @@ class PerformanceObjectiveBlock(blocks.StructBlock): class UserTextInputBlock(blocks.StaticBlock): + text = blocks.TextBlock(blank=True) + class Meta: icon = "edit" @@ -35,15 +37,16 @@ class UserConfirmationBlock(blocks.StructBlock): icon = "tick-inverse" +class TaskContentStreamBlock(blocks.StreamBlock): + explanation = ExplanationBlock() + user_text_input = UserTextInputBlock() + user_confirmation = UserConfirmationBlock() + + class TaskBlock(blocks.StructBlock): title = blocks.TextBlock() file_submission_required = blocks.BooleanBlock(required=False) - content = blocks.StreamBlock( - [ - ("explanation", ExplanationBlock()), - ("user_text_input", UserTextInputBlock()), - ("user_confirmation", UserConfirmationBlock()), - ], + content = TaskContentStreamBlock( blank=True, ) diff --git a/server/vbv_lernwelt/assignment/tests/assignment_factories.py b/server/vbv_lernwelt/assignment/tests/assignment_factories.py index 6a4e1ad1..4326b2e5 100644 --- a/server/vbv_lernwelt/assignment/tests/assignment_factories.py +++ b/server/vbv_lernwelt/assignment/tests/assignment_factories.py @@ -1,18 +1,23 @@ import wagtail_factories +from factory import SubFactory -from vbv_lernwelt.assignment.models import Assignment, TaskBlock, AssignmentListPage +from vbv_lernwelt.assignment.models import ( + Assignment, + TaskBlock, + AssignmentListPage, + TaskContentStreamBlock, + UserTextInputBlock, +) from vbv_lernwelt.assignment.models import ( ExplanationBlock, UserConfirmationBlock, PerformanceObjectiveBlock, ) +from vbv_lernwelt.core.utils import replace_whitespace class ExplanationBlockFactory(wagtail_factories.StructBlockFactory): - text = """Erläutere die Kundensituation und die Ausgangslage. - Hast du alle Informationen, die du für den Policen-Check benötigst? - Halte die wichtigsten Eckwerte des aktuellen Versicherungsverhältnisse in deiner Dokumentation fest (z.B wie lang wo versichert, Alter des Fahrzeugs, Kundenprofil, etc.) - """ + text = "Dies ist ein Beispieltext." class Meta: model = ExplanationBlock @@ -25,17 +30,23 @@ class UserConfirmationBlockFactory(wagtail_factories.StructBlockFactory): model = UserConfirmationBlock +class TaskContentStreamBlockFactory(wagtail_factories.StreamBlockFactory): + explanation = SubFactory(ExplanationBlockFactory) + user_confirmation = SubFactory(UserConfirmationBlockFactory) + + class Meta: + model = TaskContentStreamBlock + + +class UserTextInputBlockFactory(wagtail_factories.StructBlockFactory): + class Meta: + model = UserTextInputBlock + + class TaskBlockFactory(wagtail_factories.StructBlockFactory): title = "Teilauftrag" file_submission_required = False - content = wagtail_factories.StreamFieldFactory( - [ - ("explanation", ExplanationBlockFactory()), - ("user_text_input", "static_block"), - ("user_confirmation", UserConfirmationBlockFactory()), - ], - use_json_field=True, - ) + content = TaskContentStreamBlockFactory() class Meta: model = TaskBlock @@ -50,11 +61,15 @@ class PerformanceObjectiveBlockFactory(wagtail_factories.StructBlockFactory): class AssignmentFactory(wagtail_factories.PageFactory): title = "Auftrag" - starting_position = """Jemand aus deiner Familie oder aus deinem Freundeskreis möchte sein - Versicherungspolice überprüfen lassen. Diese Person kommt nun mit ihrer Police auf dich zu - und bittet dich als Versicherungsprofi, diese kritisch zu überprüfen und ihr gg. Anpassungsvorschläge - zu unterbreiten. In diesem Kompetenznachweis kannst du nun dein Wissen und Können im Bereich - der Motorfahrzeugversicherung unter Beweis stellen.""" + starting_position = replace_whitespace( + """ + Jemand aus deiner Familie oder aus deinem Freundeskreis möchte sein + Versicherungspolice überprüfen lassen. Diese Person kommt nun mit ihrer Police auf dich zu + und bittet dich als Versicherungsprofi, diese kritisch zu überprüfen und ihr gg. Anpassungsvorschläge + zu unterbreiten. In diesem Kompetenznachweis kannst du nun dein Wissen und Können im Bereich + der Motorfahrzeugversicherung unter Beweis stellen. + """ + ) class Meta: model = Assignment diff --git a/server/vbv_lernwelt/core/utils.py b/server/vbv_lernwelt/core/utils.py index 8ad3171f..97ec9a37 100644 --- a/server/vbv_lernwelt/core/utils.py +++ b/server/vbv_lernwelt/core/utils.py @@ -1,4 +1,5 @@ import logging +import re import structlog from django.conf import settings @@ -50,3 +51,7 @@ 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 replace_whitespace(text, replacement=" "): + return re.sub(r"\s+", replacement, text).strip() From b0cc7895491000681da0af3ead4971d3a9050654 Mon Sep 17 00:00:00 2001 From: Daniel Egger Date: Wed, 5 Apr 2023 15:13:41 +0200 Subject: [PATCH 06/11] Attach Assignment to LearningPath --- prepare_server.sh | 1 - .../assignment/creators/create_assignments.py | 17 +- .../commands/create_default_assignments.py | 9 -- .../assignment/migrations/0001_initial.py | 145 ++++++++++++++++-- server/vbv_lernwelt/assignment/models.py | 29 +++- .../assignment/tests/assignment_factories.py | 10 +- .../commands/create_default_courses.py | 2 + .../management/commands/create_uk_course.py | 16 ++ .../0002_alter_learningcontent_contents.py | 127 ++++++++++++++- 9 files changed, 303 insertions(+), 53 deletions(-) delete mode 100644 server/vbv_lernwelt/assignment/management/commands/create_default_assignments.py diff --git a/prepare_server.sh b/prepare_server.sh index 8fd54393..ad375e5b 100755 --- a/prepare_server.sh +++ b/prepare_server.sh @@ -97,7 +97,6 @@ if [ "$SKIP_SETUP" = false ]; then echo "python server/manage.py create_default_courses $course_param" python server/manage.py create_default_courses $course_param python server/manage.py create_default_notifications - python server/manage.py create_default_assignments # make django translations (cd server && python manage.py compilemessages) diff --git a/server/vbv_lernwelt/assignment/creators/create_assignments.py b/server/vbv_lernwelt/assignment/creators/create_assignments.py index 4e2a415e..62b196e6 100644 --- a/server/vbv_lernwelt/assignment/creators/create_assignments.py +++ b/server/vbv_lernwelt/assignment/creators/create_assignments.py @@ -1,23 +1,20 @@ -from wagtail.blocks import StreamValue - -from vbv_lernwelt.assignment.models import ( - TaskContentStreamBlock, -) +from vbv_lernwelt.assignment.models import TaskContentStreamBlock from vbv_lernwelt.assignment.tests.assignment_factories import ( - PerformanceObjectiveBlockFactory, - AssignmentListPageFactory, AssignmentFactory, - TaskBlockFactory, + AssignmentListPageFactory, ExplanationBlockFactory, + PerformanceObjectiveBlockFactory, + TaskBlockFactory, UserTextInputBlockFactory, ) from vbv_lernwelt.core.utils import replace_whitespace from vbv_lernwelt.course.consts import COURSE_UK from vbv_lernwelt.course.models import CoursePage +from wagtail.blocks import StreamValue -def create_assignments(): - course_page = CoursePage.objects.get(course_id=COURSE_UK) +def create_uk_assignments(course_id=COURSE_UK): + course_page = CoursePage.objects.get(course_id=course_id) assignment_page = AssignmentListPageFactory( parent=course_page, ) diff --git a/server/vbv_lernwelt/assignment/management/commands/create_default_assignments.py b/server/vbv_lernwelt/assignment/management/commands/create_default_assignments.py deleted file mode 100644 index 1e2d6fc6..00000000 --- a/server/vbv_lernwelt/assignment/management/commands/create_default_assignments.py +++ /dev/null @@ -1,9 +0,0 @@ -import djclick as click - -from vbv_lernwelt.assignment.creators.create_assignments import create_assignments - - -@click.command() -def command(): - print("Creating default assignments") - create_assignments() diff --git a/server/vbv_lernwelt/assignment/migrations/0001_initial.py b/server/vbv_lernwelt/assignment/migrations/0001_initial.py index 0173e9b8..f4adc435 100644 --- a/server/vbv_lernwelt/assignment/migrations/0001_initial.py +++ b/server/vbv_lernwelt/assignment/migrations/0001_initial.py @@ -1,10 +1,11 @@ # Generated by Django 3.2.13 on 2023-04-04 11:49 -from django.db import migrations, models import django.db.models.deletion -import vbv_lernwelt.assignment.models import wagtail.blocks import wagtail.fields +from django.db import migrations, models + +import vbv_lernwelt.assignment.models class Migration(migrations.Migration): @@ -12,34 +13,144 @@ class Migration(migrations.Migration): initial = True dependencies = [ - ('wagtailcore', '0069_log_entry_jsonfield'), + ("wagtailcore", "0069_log_entry_jsonfield"), ] operations = [ migrations.CreateModel( - name='Assignment', + name="Assignment", fields=[ - ('page_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='wagtailcore.page')), - ('starting_position', models.TextField(help_text='Erläuterung der Ausgangslage')), - ('effort_required', models.CharField(blank=True, help_text='Zeitaufwand als Text', max_length=100)), - ('performance_objectives', wagtail.fields.StreamField([('performance_objective', wagtail.blocks.StructBlock([('text', wagtail.blocks.TextBlock())]))], blank=True, help_text='Leistungsziele des Auftrags', use_json_field=True)), - ('assessment_description', models.TextField(blank=True, help_text='Beschreibung der Bewertung')), - ('assessment_document_url', models.CharField(blank=True, help_text='URL zum Beeurteilungsinstrument', max_length=255)), - ('tasks', wagtail.fields.StreamField([('task', wagtail.blocks.StructBlock([('title', wagtail.blocks.TextBlock()), ('file_submission_required', wagtail.blocks.BooleanBlock(required=False)), ('content', wagtail.blocks.StreamBlock([('explanation', wagtail.blocks.StructBlock([('text', wagtail.blocks.TextBlock())])), ('user_text_input', vbv_lernwelt.assignment.models.UserTextInputBlock()), ('user_confirmation', wagtail.blocks.StructBlock([('text', wagtail.blocks.TextBlock())]))], blank=True))]))], blank=True, help_text='Teilaufgaben', use_json_field=True)), + ( + "page_ptr", + models.OneToOneField( + auto_created=True, + on_delete=django.db.models.deletion.CASCADE, + parent_link=True, + primary_key=True, + serialize=False, + to="wagtailcore.page", + ), + ), + ( + "starting_position", + models.TextField(help_text="Erläuterung der Ausgangslage"), + ), + ( + "effort_required", + models.CharField( + blank=True, help_text="Zeitaufwand als Text", max_length=100 + ), + ), + ( + "performance_objectives", + wagtail.fields.StreamField( + [ + ( + "performance_objective", + wagtail.blocks.StructBlock( + [("text", wagtail.blocks.TextBlock())] + ), + ) + ], + blank=True, + help_text="Leistungsziele des Auftrags", + use_json_field=True, + ), + ), + ( + "assessment_description", + models.TextField( + blank=True, help_text="Beschreibung der Bewertung" + ), + ), + ( + "assessment_document_url", + models.CharField( + blank=True, + help_text="URL zum Beeurteilungsinstrument", + max_length=255, + ), + ), + ( + "tasks", + wagtail.fields.StreamField( + [ + ( + "task", + wagtail.blocks.StructBlock( + [ + ("title", wagtail.blocks.TextBlock()), + ( + "file_submission_required", + wagtail.blocks.BooleanBlock(required=False), + ), + ( + "content", + wagtail.blocks.StreamBlock( + [ + ( + "explanation", + wagtail.blocks.StructBlock( + [ + ( + "text", + wagtail.blocks.TextBlock(), + ) + ] + ), + ), + ( + "user_text_input", + vbv_lernwelt.assignment.models.UserTextInputBlock(), + ), + ( + "user_confirmation", + wagtail.blocks.StructBlock( + [ + ( + "text", + wagtail.blocks.TextBlock(), + ) + ] + ), + ), + ], + blank=True, + ), + ), + ] + ), + ) + ], + blank=True, + help_text="Teilaufgaben", + use_json_field=True, + ), + ), ], options={ - 'verbose_name': 'Auftrag', + "verbose_name": "Auftrag", }, - bases=('wagtailcore.page',), + bases=("wagtailcore.page",), ), migrations.CreateModel( - name='AssignmentListPage', + name="AssignmentListPage", fields=[ - ('page_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='wagtailcore.page')), + ( + "page_ptr", + models.OneToOneField( + auto_created=True, + on_delete=django.db.models.deletion.CASCADE, + parent_link=True, + primary_key=True, + serialize=False, + to="wagtailcore.page", + ), + ), ], options={ - 'abstract': False, + "abstract": False, }, - bases=('wagtailcore.page',), + bases=("wagtailcore.page",), ), ] diff --git a/server/vbv_lernwelt/assignment/models.py b/server/vbv_lernwelt/assignment/models.py index 4c74134f..c7366719 100644 --- a/server/vbv_lernwelt/assignment/models.py +++ b/server/vbv_lernwelt/assignment/models.py @@ -1,9 +1,27 @@ from django.db import models +from slugify import slugify from wagtail import blocks from wagtail.admin.panels import FieldPanel from wagtail.fields import StreamField from wagtail.models import Page +from vbv_lernwelt.core.model_utils import find_available_slug +from vbv_lernwelt.course.models import CourseBasePage + + +class AssignmentListPage(CourseBasePage): + subpage_types = ["assignment.Assignment"] + parent_page_types = ["course.CoursePage"] + + def save(self, clean=True, user=None, log_action=False, **kwargs): + self.slug = find_available_slug( + slugify(f"{self.get_parent().slug}-assignment", allow_unicode=True) + ) + super(AssignmentListPage, self).save(clean, user, log_action, **kwargs) + + def __str__(self): + return f"{self.title}" + # class AssignmentSubmission(modModel): # created_at = models.DateTimeField(auto_now_add=True) @@ -55,7 +73,7 @@ class TaskBlock(blocks.StructBlock): label = "Teilauftrag" -class Assignment(Page): +class Assignment(CourseBasePage): starting_position = models.TextField(help_text="Erläuterung der Ausgangslage") effort_required = models.CharField( max_length=100, help_text="Zeitaufwand als Text", blank=True @@ -101,7 +119,8 @@ class Assignment(Page): class Meta: verbose_name = "Auftrag" - -class AssignmentListPage(Page): - subpage_types = ["assignment.Assignment"] - parent_page_types = ["course.CoursePage"] + def save(self, clean=True, user=None, log_action=False, **kwargs): + self.slug = find_available_slug( + slugify(f"{self.get_parent().slug}-{self.title}", allow_unicode=True) + ) + super(Assignment, self).save(clean, user, log_action, **kwargs) diff --git a/server/vbv_lernwelt/assignment/tests/assignment_factories.py b/server/vbv_lernwelt/assignment/tests/assignment_factories.py index 4326b2e5..c152422d 100644 --- a/server/vbv_lernwelt/assignment/tests/assignment_factories.py +++ b/server/vbv_lernwelt/assignment/tests/assignment_factories.py @@ -3,15 +3,13 @@ from factory import SubFactory from vbv_lernwelt.assignment.models import ( Assignment, - TaskBlock, AssignmentListPage, - TaskContentStreamBlock, - UserTextInputBlock, -) -from vbv_lernwelt.assignment.models import ( ExplanationBlock, - UserConfirmationBlock, PerformanceObjectiveBlock, + TaskBlock, + TaskContentStreamBlock, + UserConfirmationBlock, + UserTextInputBlock, ) from vbv_lernwelt.core.utils import replace_whitespace 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 3f6931f9..e8dcc94c 100644 --- a/server/vbv_lernwelt/course/management/commands/create_default_courses.py +++ b/server/vbv_lernwelt/course/management/commands/create_default_courses.py @@ -1,6 +1,7 @@ import djclick as click from wagtail.models import Page +from vbv_lernwelt.assignment.creators.create_assignments import create_uk_assignments from vbv_lernwelt.competence.create_uk_competence_profile import ( create_uk_competence_profile, create_uk_fr_competence_profile, @@ -137,6 +138,7 @@ def create_course_uk_de(): create_versicherungsvermittlerin_with_categories( course_id=COURSE_UK, title="Überbetriebliche Kurse" ) + create_uk_assignments(course_id=COURSE_UK) create_uk_learning_path(course_id=COURSE_UK) create_uk_competence_profile(course_id=COURSE_UK) create_default_media_library(course_id=COURSE_UK) diff --git a/server/vbv_lernwelt/course/management/commands/create_uk_course.py b/server/vbv_lernwelt/course/management/commands/create_uk_course.py index 0eb08b49..9899333a 100644 --- a/server/vbv_lernwelt/course/management/commands/create_uk_course.py +++ b/server/vbv_lernwelt/course/management/commands/create_uk_course.py @@ -5,6 +5,7 @@ from slugify import slugify from wagtail.models import Locale, Page, Site from wagtail_localize.models import LocaleSynchronization +from vbv_lernwelt.assignment.models import Assignment from vbv_lernwelt.core.admin import User from vbv_lernwelt.course.consts import COURSE_UK, COURSE_UK_FR from vbv_lernwelt.course.models import CoursePage @@ -18,6 +19,7 @@ from vbv_lernwelt.learnpath.tests.learning_path_factories import ( LearningUnitFactory, MediaLibraryBlockFactory, TopicFactory, + AssignmentBlockFactory, ) @@ -282,6 +284,20 @@ damit du erfolgreich mit deinem Lernpfad (durch-)starten kannst. title="Reflexion", parent=circle, ) + LearningContentFactory( + title="Überprüfen einer Motorfahrzeug-Versicherungspolice", + parent=circle, + contents=[ + ( + "assignment", + AssignmentBlockFactory( + assignment=Assignment.objects.get( + slug__startswith="überbetriebliche-kurse-assignment-überprüfen-einer-motorfahrzeugs" + ) + ), + ) + ], + ) LearningContentFactory( title="Feedback", parent=circle, diff --git a/server/vbv_lernwelt/learnpath/migrations/0002_alter_learningcontent_contents.py b/server/vbv_lernwelt/learnpath/migrations/0002_alter_learningcontent_contents.py index 559b4eb2..70d70f10 100644 --- a/server/vbv_lernwelt/learnpath/migrations/0002_alter_learningcontent_contents.py +++ b/server/vbv_lernwelt/learnpath/migrations/0002_alter_learningcontent_contents.py @@ -1,20 +1,137 @@ # Generated by Django 3.2.13 on 2023-04-04 08:28 -from django.db import migrations import wagtail.blocks import wagtail.fields +from django.db import migrations class Migration(migrations.Migration): dependencies = [ - ('learnpath', '0001_initial'), + ("learnpath", "0001_initial"), ] 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()), ('assignment', wagtail.blocks.PageChooserBlock(help_text='Choose the corresponding assignment.', required=True))])), ('placeholder', wagtail.blocks.StructBlock([('description', wagtail.blocks.TextBlock()), ('url', wagtail.blocks.TextBlock())])), ('feedback', wagtail.blocks.StructBlock([])), ('attendance_day', wagtail.blocks.StructBlock([('description', wagtail.blocks.TextBlock())]))], use_json_field=None), + 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()), + ( + "assignment", + wagtail.blocks.PageChooserBlock( + help_text="Choose the corresponding assignment.", + required=True, + ), + ), + ] + ), + ), + ( + "placeholder", + wagtail.blocks.StructBlock( + [ + ("description", wagtail.blocks.TextBlock()), + ("url", wagtail.blocks.TextBlock()), + ] + ), + ), + ("feedback", wagtail.blocks.StructBlock([])), + ( + "attendance_day", + wagtail.blocks.StructBlock( + [("description", wagtail.blocks.TextBlock())] + ), + ), + ], + use_json_field=None, + ), ), ] From 8f84ef7502a0371d572b2f3a908a308a6fdf1d35 Mon Sep 17 00:00:00 2001 From: Daniel Egger Date: Wed, 5 Apr 2023 16:19:37 +0200 Subject: [PATCH 07/11] Adapt api so it can fetch assignment page by id --- client/src/stores/assignments.ts | 41 ++++++++++++++++++++++++ server/config/urls.py | 2 +- server/vbv_lernwelt/assignment/models.py | 9 ++++++ server/vbv_lernwelt/course/views.py | 7 ++-- 4 files changed, 56 insertions(+), 3 deletions(-) create mode 100644 client/src/stores/assignments.ts diff --git a/client/src/stores/assignments.ts b/client/src/stores/assignments.ts new file mode 100644 index 00000000..681fc6a2 --- /dev/null +++ b/client/src/stores/assignments.ts @@ -0,0 +1,41 @@ +import { itGet } from "@/fetchHelpers"; +import type { MediaLibraryPage } from "@/types"; +import log from "loglevel"; +import { defineStore } from "pinia"; + +export type MediaLibraryStoreState = { + mediaLibraryPage: MediaLibraryPage | undefined; + selectedLearningPath: { id: number; name: string }; + availableLearningPaths: { id: number; name: string }[]; +}; + +export const useMediaLibraryStore = defineStore({ + id: "mediaLibrary", + state: () => { + return { + mediaLibraryPage: undefined, + selectedLearningPath: { id: 1, name: "Alle Lehrgänge" }, + availableLearningPaths: [ + { id: 1, name: "Alle Lehrgänge" }, + { id: 2, name: "Versicherungsvermittler/-in" }, + ], + } as MediaLibraryStoreState; + }, + getters: {}, + actions: { + async loadMediaLibraryPage(slug: string, reload = false) { + if (this.mediaLibraryPage && !reload) { + return this.mediaLibraryPage; + } + log.debug("load mediaLibraryPageData"); + const mediaLibraryPageData = await itGet(`/api/course/page/${slug}/`); + + if (!mediaLibraryPageData) { + throw `No mediaLibraryPageData found with: ${slug}`; + } + + this.mediaLibraryPage = mediaLibraryPageData; + return this.mediaLibraryPage; + }, + }, +}); diff --git a/server/config/urls.py b/server/config/urls.py index ab0e1b94..b8f535b0 100644 --- a/server/config/urls.py +++ b/server/config/urls.py @@ -83,7 +83,7 @@ urlpatterns = [ path(r"api/course/sessions/", get_course_sessions, name="get_course_sessions"), path(r"api/course/sessions//users/", get_course_session_users, name="get_course_session_users"), - 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_view, name="mark_course_completion"), diff --git a/server/vbv_lernwelt/assignment/models.py b/server/vbv_lernwelt/assignment/models.py index c7366719..215ff9b9 100644 --- a/server/vbv_lernwelt/assignment/models.py +++ b/server/vbv_lernwelt/assignment/models.py @@ -74,6 +74,15 @@ class TaskBlock(blocks.StructBlock): class Assignment(CourseBasePage): + serialize_field_names = [ + "starting_position", + "effort_required", + "performance_objectives", + "assessment_description", + "assessment_document_url", + "tasks", + ] + starting_position = models.TextField(help_text="Erläuterung der Ausgangslage") effort_required = models.CharField( max_length=100, help_text="Zeitaufwand als Text", blank=True diff --git a/server/vbv_lernwelt/course/views.py b/server/vbv_lernwelt/course/views.py index bdb0a7f2..105fdecb 100644 --- a/server/vbv_lernwelt/course/views.py +++ b/server/vbv_lernwelt/course/views.py @@ -33,9 +33,12 @@ logger = structlog.get_logger(__name__) @api_view(["GET"]) -def course_page_api_view(request, slug): +def course_page_api_view(request, slug_or_id): try: - page = Page.objects.get(slug=slug, locale__language_code="de-CH") + if slug_or_id.isdigit(): + page = Page.objects.get(id=slug_or_id) + else: + page = Page.objects.get(slug=slug_or_id, locale__language_code="de-CH") if not has_course_access_by_page_request(request, page): raise PermissionDenied() From d92b324f8ebcb566382ccb3aa4d42951321e9685 Mon Sep 17 00:00:00 2001 From: Daniel Egger Date: Wed, 5 Apr 2023 17:01:54 +0200 Subject: [PATCH 08/11] Add frontend types and loading code for Assignment --- .../learningContentPage/LearningContent.vue | 3 +- .../assignment/AssignmentView.vue | 40 +++++++++++++ .../blocks/AssignmentBlock.vue | 21 +++++++ client/src/stores/assignmentStore.ts | 31 ++++++++++ client/src/stores/assignments.ts | 41 ------------- client/src/types.ts | 57 +++++++++++++++++++ 6 files changed, 151 insertions(+), 42 deletions(-) create mode 100644 client/src/pages/learningPath/learningContentPage/assignment/AssignmentView.vue create mode 100644 client/src/pages/learningPath/learningContentPage/blocks/AssignmentBlock.vue create mode 100644 client/src/stores/assignmentStore.ts delete mode 100644 client/src/stores/assignments.ts diff --git a/client/src/pages/learningPath/learningContentPage/LearningContent.vue b/client/src/pages/learningPath/learningContentPage/LearningContent.vue index fc7f1836..7f3a0a6d 100644 --- a/client/src/pages/learningPath/learningContentPage/LearningContent.vue +++ b/client/src/pages/learningPath/learningContentPage/LearningContent.vue @@ -6,6 +6,7 @@ import log from "loglevel"; import type { Component } from "vue"; import { computed } from "vue"; +import AssignmentBlock from "@/pages/learningPath/learningContentPage/blocks/AssignmentBlock.vue"; import AttendanceDayBlock from "@/pages/learningPath/learningContentPage/blocks/AttendanceDayBlock.vue"; import DescriptionBlock from "./blocks/DescriptionBlock.vue"; import DescriptionTextBlock from "./blocks/DescriptionTextBlock.vue"; @@ -35,7 +36,7 @@ const block = computed(() => { const COMPONENTS: Record = { placeholder: PlaceholderBlock, video: VideoBlock, - assignment: DescriptionTextBlock, + assignment: AssignmentBlock, resource: DescriptionTextBlock, exercise: IframeBlock, test: IframeBlock, diff --git a/client/src/pages/learningPath/learningContentPage/assignment/AssignmentView.vue b/client/src/pages/learningPath/learningContentPage/assignment/AssignmentView.vue new file mode 100644 index 00000000..829f5c4d --- /dev/null +++ b/client/src/pages/learningPath/learningContentPage/assignment/AssignmentView.vue @@ -0,0 +1,40 @@ + + + diff --git a/client/src/pages/learningPath/learningContentPage/blocks/AssignmentBlock.vue b/client/src/pages/learningPath/learningContentPage/blocks/AssignmentBlock.vue new file mode 100644 index 00000000..0afb1a47 --- /dev/null +++ b/client/src/pages/learningPath/learningContentPage/blocks/AssignmentBlock.vue @@ -0,0 +1,21 @@ + + + diff --git a/client/src/stores/assignmentStore.ts b/client/src/stores/assignmentStore.ts new file mode 100644 index 00000000..78fc4b04 --- /dev/null +++ b/client/src/stores/assignmentStore.ts @@ -0,0 +1,31 @@ +import { itGet } from "@/fetchHelpers"; +import type { Assignment } from "@/types"; +import log from "loglevel"; +import { defineStore } from "pinia"; + +export type AssignmentStoreState = { + assignment: Assignment | undefined; +}; + +export const useAssignmentStore = defineStore({ + id: "assignmentStore", + state: () => { + return { + assignment: undefined, + } as AssignmentStoreState; + }, + getters: {}, + actions: { + async loadAssignment(assignmentId: number) { + log.debug("load assignment", assignmentId); + const assignmentData = await itGet(`/api/course/page/${assignmentId}/`); + + if (!assignmentData) { + throw `No assignment found with: ${assignmentId}`; + } + + this.assignment = assignmentData; + return this.assignment; + }, + }, +}); diff --git a/client/src/stores/assignments.ts b/client/src/stores/assignments.ts deleted file mode 100644 index 681fc6a2..00000000 --- a/client/src/stores/assignments.ts +++ /dev/null @@ -1,41 +0,0 @@ -import { itGet } from "@/fetchHelpers"; -import type { MediaLibraryPage } from "@/types"; -import log from "loglevel"; -import { defineStore } from "pinia"; - -export type MediaLibraryStoreState = { - mediaLibraryPage: MediaLibraryPage | undefined; - selectedLearningPath: { id: number; name: string }; - availableLearningPaths: { id: number; name: string }[]; -}; - -export const useMediaLibraryStore = defineStore({ - id: "mediaLibrary", - state: () => { - return { - mediaLibraryPage: undefined, - selectedLearningPath: { id: 1, name: "Alle Lehrgänge" }, - availableLearningPaths: [ - { id: 1, name: "Alle Lehrgänge" }, - { id: 2, name: "Versicherungsvermittler/-in" }, - ], - } as MediaLibraryStoreState; - }, - getters: {}, - actions: { - async loadMediaLibraryPage(slug: string, reload = false) { - if (this.mediaLibraryPage && !reload) { - return this.mediaLibraryPage; - } - log.debug("load mediaLibraryPageData"); - const mediaLibraryPageData = await itGet(`/api/course/page/${slug}/`); - - if (!mediaLibraryPageData) { - throw `No mediaLibraryPageData found with: ${slug}`; - } - - this.mediaLibraryPage = mediaLibraryPageData; - return this.mediaLibraryPage; - }, - }, -}); diff --git a/client/src/types.ts b/client/src/types.ts index bb6c7ae2..1d31746e 100644 --- a/client/src/types.ts +++ b/client/src/types.ts @@ -285,6 +285,63 @@ export interface MediaLibraryPage extends BaseCourseWagtailPage { readonly children: MediaCategoryPage[]; } +export interface AssignmentPerformanceObjective { + readonly type: "performance_objective"; + readonly id: string; + readonly value: { + text: string; + }; +} + +export interface AssignmentTaskBlockExplanation { + readonly type: "explanation"; + readonly id: string; + readonly value: { + readonly text: string; + }; +} + +export interface AssignmentTaskBlockUserConfirmation { + readonly type: "user_confirmation"; + readonly id: string; + readonly value: { + readonly text: string; + }; +} + +export interface AssignmentTaskBlockUserTextInput { + readonly type: "user_text_input"; + readonly id: string; + readonly value: { + readonly text?: string; + }; +} + +export type AssignmentTaskBlock = + | AssignmentTaskBlockExplanation + | AssignmentTaskBlockUserConfirmation + | AssignmentTaskBlockUserTextInput; + +export interface AssignmentTask { + readonly type: "task"; + readonly id: string; + readonly value: { + title: string; + file_submission_required: boolean; + content: AssignmentTaskBlock[]; + }; +} + +export interface Assignment extends BaseCourseWagtailPage { + readonly type: "assignment.Assignment"; + readonly starting_position: string; + readonly effort_required: string; + readonly performance_objectives: AssignmentPerformanceObjective[]; + readonly assessment_description: string; + readonly assessment_document_url: string; + readonly tasks: AssignmentTask[]; +} + export interface PerformanceCriteria extends BaseCourseWagtailPage { readonly type: "competence.PerformanceCriteria"; readonly competence_id: string; From 80cd70ace627b9a1e05df24f33fc67ff8593518b Mon Sep 17 00:00:00 2001 From: Daniel Egger Date: Thu, 6 Apr 2023 11:33:11 +0200 Subject: [PATCH 09/11] Link courseSessionAssignmentDetails to AssignmentView --- .../assignment/AssignmentView.vue | 62 +++++++++++++------ client/src/stores/courseSessions.ts | 12 ++++ client/src/types.ts | 6 ++ .../commands/create_default_courses.py | 8 +++ .../management/commands/create_uk_course.py | 2 +- ...4_coursesession_assignment_details_list.py | 18 ++++++ server/vbv_lernwelt/course/models.py | 1 + server/vbv_lernwelt/course/serializers.py | 1 + 8 files changed, 90 insertions(+), 20 deletions(-) create mode 100644 server/vbv_lernwelt/course/migrations/0004_coursesession_assignment_details_list.py diff --git a/client/src/pages/learningPath/learningContentPage/assignment/AssignmentView.vue b/client/src/pages/learningPath/learningContentPage/assignment/AssignmentView.vue index 829f5c4d..38610ad3 100644 --- a/client/src/pages/learningPath/learningContentPage/assignment/AssignmentView.vue +++ b/client/src/pages/learningPath/learningContentPage/assignment/AssignmentView.vue @@ -1,27 +1,25 @@ - - + + diff --git a/client/src/stores/courseSessions.ts b/client/src/stores/courseSessions.ts index 5aae6fce..dd2e7204 100644 --- a/client/src/stores/courseSessions.ts +++ b/client/src/stores/courseSessions.ts @@ -3,6 +3,7 @@ import { deleteCircleDocument } from "@/services/files"; import type { CircleDocument, CourseSession, + CourseSessionAssignmentDetails, CourseSessionAttendanceDay, CourseSessionUser, ExpertSessionUser, @@ -224,6 +225,16 @@ export const useCourseSessionsStore = defineStore("courseSessions", () => { } } + function findAssignmentDetails( + contentId: number + ): CourseSessionAssignmentDetails | undefined { + if (currentCourseSession.value) { + return currentCourseSession.value.assignment_details_list.find( + (assignmentDetails) => assignmentDetails.learningContentId === contentId + ); + } + } + return { uniqueCourseSessionsByCourse, currentCourseSession, @@ -238,6 +249,7 @@ export const useCourseSessionsStore = defineStore("courseSessions", () => { startUpload, removeDocument, findAttendanceDay, + findAssignmentDetails, // TODO: only used to be changed by router.afterEach currentCourseSlug, diff --git a/client/src/types.ts b/client/src/types.ts index 1d31746e..67aaf77f 100644 --- a/client/src/types.ts +++ b/client/src/types.ts @@ -405,6 +405,11 @@ export interface CourseSessionAttendanceDay { trainer: string; } +export interface CourseSessionAssignmentDetails { + learningContentId: number; + deadlineDateTimeUtc: string; +} + export interface CourseSession { id: number; created_at: string; @@ -418,6 +423,7 @@ export interface CourseSession { course_url: string; media_library_url: string; attendance_days: CourseSessionAttendanceDay[]; + assignment_details_list: CourseSessionAssignmentDetails[]; documents: CircleDocument[]; users: CourseSessionUser[]; } 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 e8dcc94c..fd779e9b 100644 --- a/server/vbv_lernwelt/course/management/commands/create_default_courses.py +++ b/server/vbv_lernwelt/course/management/commands/create_default_courses.py @@ -158,6 +158,14 @@ def create_course_uk_de(): "trainer": "Roland Grossenbacher, roland.grossenbacher@helvetia.ch", } ], + assignment_details_list=[ + { + "learningContentId": LearningContent.objects.get( + slug="überbetriebliche-kurse-lp-circle-fahrzeug-lc-überprüfen-einer-motorfahrzeug-versicherungspolice" + ).id, + "deadlineDateTimeUtc": "2023-05-30T19:00:00Z", + } + ], ) # figma demo users and data diff --git a/server/vbv_lernwelt/course/management/commands/create_uk_course.py b/server/vbv_lernwelt/course/management/commands/create_uk_course.py index 9899333a..92facbab 100644 --- a/server/vbv_lernwelt/course/management/commands/create_uk_course.py +++ b/server/vbv_lernwelt/course/management/commands/create_uk_course.py @@ -10,6 +10,7 @@ from vbv_lernwelt.core.admin import User from vbv_lernwelt.course.consts import COURSE_UK, COURSE_UK_FR from vbv_lernwelt.course.models import CoursePage from vbv_lernwelt.learnpath.tests.learning_path_factories import ( + AssignmentBlockFactory, AttendanceDayBlockFactory, CircleFactory, FeedbackBlockFactory, @@ -19,7 +20,6 @@ from vbv_lernwelt.learnpath.tests.learning_path_factories import ( LearningUnitFactory, MediaLibraryBlockFactory, TopicFactory, - AssignmentBlockFactory, ) diff --git a/server/vbv_lernwelt/course/migrations/0004_coursesession_assignment_details_list.py b/server/vbv_lernwelt/course/migrations/0004_coursesession_assignment_details_list.py new file mode 100644 index 00000000..6bde4931 --- /dev/null +++ b/server/vbv_lernwelt/course/migrations/0004_coursesession_assignment_details_list.py @@ -0,0 +1,18 @@ +# Generated by Django 3.2.13 on 2023-04-06 09:19 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("course", "0003_auto_20230404_0837"), + ] + + operations = [ + migrations.AddField( + model_name="coursesession", + name="assignment_details_list", + field=models.JSONField(blank=True, default=list), + ), + ] diff --git a/server/vbv_lernwelt/course/models.py b/server/vbv_lernwelt/course/models.py index f4457929..b2903b07 100644 --- a/server/vbv_lernwelt/course/models.py +++ b/server/vbv_lernwelt/course/models.py @@ -186,6 +186,7 @@ class CourseSession(models.Model): end_date = models.DateField(null=True, blank=True) attendance_days = models.JSONField(default=list, blank=True) + assignment_details_list = models.JSONField(default=list, blank=True) additional_json_data = models.JSONField(default=dict, blank=True) diff --git a/server/vbv_lernwelt/course/serializers.py b/server/vbv_lernwelt/course/serializers.py index 701bddac..63061161 100644 --- a/server/vbv_lernwelt/course/serializers.py +++ b/server/vbv_lernwelt/course/serializers.py @@ -83,6 +83,7 @@ class CourseSessionSerializer(serializers.ModelSerializer): "end_date", "additional_json_data", "attendance_days", + "assignment_details_list", "learning_path_url", "competence_url", "media_library_url", From 3cabca8c211b78c0cb10a8cc1badd3aaa0a66693 Mon Sep 17 00:00:00 2001 From: Elia Bieri Date: Tue, 11 Apr 2023 11:31:51 +0200 Subject: [PATCH 10/11] Fix typo --- server/vbv_lernwelt/assignment/migrations/0001_initial.py | 4 ++-- server/vbv_lernwelt/assignment/models.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/server/vbv_lernwelt/assignment/migrations/0001_initial.py b/server/vbv_lernwelt/assignment/migrations/0001_initial.py index f4adc435..e3441d81 100644 --- a/server/vbv_lernwelt/assignment/migrations/0001_initial.py +++ b/server/vbv_lernwelt/assignment/migrations/0001_initial.py @@ -1,4 +1,4 @@ -# Generated by Django 3.2.13 on 2023-04-04 11:49 +# Generated by Django 3.2.13 on 2023-04-11 09:30 import django.db.models.deletion import wagtail.blocks @@ -67,7 +67,7 @@ class Migration(migrations.Migration): "assessment_document_url", models.CharField( blank=True, - help_text="URL zum Beeurteilungsinstrument", + help_text="URL zum Beurteilungsinstrument", max_length=255, ), ), diff --git a/server/vbv_lernwelt/assignment/models.py b/server/vbv_lernwelt/assignment/models.py index 215ff9b9..5f8dac4a 100644 --- a/server/vbv_lernwelt/assignment/models.py +++ b/server/vbv_lernwelt/assignment/models.py @@ -102,7 +102,7 @@ class Assignment(CourseBasePage): assessment_document_url = models.CharField( max_length=255, blank=True, - help_text="URL zum Beeurteilungsinstrument", + help_text="URL zum Beurteilungsinstrument", ) tasks = StreamField( From a3e4a50ba0677cdf33baaeccb04d13b2a2b618cd Mon Sep 17 00:00:00 2001 From: Daniel Egger Date: Thu, 13 Apr 2023 20:03:09 +0200 Subject: [PATCH 11/11] Add unit test to test creation of slug while saving --- server/vbv_lernwelt/assignment/models.py | 6 ++-- server/vbv_lernwelt/competence/models.py | 12 ++++--- server/vbv_lernwelt/course/models.py | 4 ++- server/vbv_lernwelt/learnpath/models.py | 6 ++-- .../learnpath/tests/test_models.py | 33 +++++++++++++++++++ server/vbv_lernwelt/media_library/models.py | 6 ++-- 6 files changed, 56 insertions(+), 11 deletions(-) create mode 100644 server/vbv_lernwelt/learnpath/tests/test_models.py diff --git a/server/vbv_lernwelt/assignment/models.py b/server/vbv_lernwelt/assignment/models.py index 5f8dac4a..205d2099 100644 --- a/server/vbv_lernwelt/assignment/models.py +++ b/server/vbv_lernwelt/assignment/models.py @@ -15,7 +15,8 @@ class AssignmentListPage(CourseBasePage): def save(self, clean=True, user=None, log_action=False, **kwargs): self.slug = find_available_slug( - slugify(f"{self.get_parent().slug}-assignment", allow_unicode=True) + slugify(f"{self.get_parent().slug}-assignment", allow_unicode=True), + ignore_page_id=self.id, ) super(AssignmentListPage, self).save(clean, user, log_action, **kwargs) @@ -130,6 +131,7 @@ class Assignment(CourseBasePage): def save(self, clean=True, user=None, log_action=False, **kwargs): self.slug = find_available_slug( - slugify(f"{self.get_parent().slug}-{self.title}", allow_unicode=True) + slugify(f"{self.get_parent().slug}-{self.title}", allow_unicode=True), + ignore_page_id=self.id, ) super(Assignment, self).save(clean, user, log_action, **kwargs) diff --git a/server/vbv_lernwelt/competence/models.py b/server/vbv_lernwelt/competence/models.py index 95622c4f..2f64a188 100644 --- a/server/vbv_lernwelt/competence/models.py +++ b/server/vbv_lernwelt/competence/models.py @@ -28,7 +28,8 @@ class CompetenceProfilePage(CourseBasePage): def save(self, clean=True, user=None, log_action=False, **kwargs): self.slug = find_available_slug( - slugify(f"{self.get_parent().slug}-competence", allow_unicode=True) + slugify(f"{self.get_parent().slug}-competence", allow_unicode=True), + ignore_page_id=self.id, ) super(CompetenceProfilePage, self).save(clean, user, log_action, **kwargs) @@ -59,7 +60,8 @@ class CompetencePage(CourseBasePage): slugify( f"{self.get_parent().slug}-competence-{self.competence_id}", allow_unicode=True, - ) + ), + ignore_page_id=self.id, ) super(CompetencePage, self).save(clean, user, log_action, **kwargs) @@ -87,14 +89,16 @@ class PerformanceCriteria(CourseBasePage): slugify( f"{profile_parent.slug}-crit-{self.competence_id}-{self.learning_unit.course_category.title}", allow_unicode=True, - ) + ), + ignore_page_id=self.id, ) else: self.slug = find_available_slug( slugify( f"{profile_parent.slug}-crit-{self.competence_id}", allow_unicode=True, - ) + ), + ignore_page_id=self.id, ) super(PerformanceCriteria, self).save(clean, user, log_action, **kwargs) diff --git a/server/vbv_lernwelt/course/models.py b/server/vbv_lernwelt/course/models.py index b2903b07..fa70a344 100644 --- a/server/vbv_lernwelt/course/models.py +++ b/server/vbv_lernwelt/course/models.py @@ -129,7 +129,9 @@ class CoursePage(CourseBasePage): verbose_name = _("Lehrgang-Seite") def save(self, *args, **kwargs): - self.slug = find_available_slug(slugify(self.title, allow_unicode=True)) + self.slug = find_available_slug( + slugify(self.title, allow_unicode=True), ignore_page_id=self.id + ) super(CoursePage, self).save(*args, **kwargs) def __str__(self): diff --git a/server/vbv_lernwelt/learnpath/models.py b/server/vbv_lernwelt/learnpath/models.py index 7ed6dc51..6ef604a5 100644 --- a/server/vbv_lernwelt/learnpath/models.py +++ b/server/vbv_lernwelt/learnpath/models.py @@ -37,7 +37,8 @@ class LearningPath(CourseBasePage): def save(self, clean=True, user=None, log_action=False, **kwargs): self.slug = find_available_slug( - slugify(f"{self.get_parent().slug}-lp", allow_unicode=True) + slugify(f"{self.get_parent().slug}-lp", allow_unicode=True), + ignore_page_id=self.id, ) super(LearningPath, self).save(clean, user, log_action, **kwargs) @@ -307,5 +308,6 @@ def find_slug_with_parent_prefix(page, type_prefix, slug_postfix=None): slug_postfix = page.title return find_available_slug( - slugify(f"{slug_prefix}-{slug_postfix}", allow_unicode=True) + slugify(f"{slug_prefix}-{slug_postfix}", allow_unicode=True), + ignore_page_id=page.id, ) diff --git a/server/vbv_lernwelt/learnpath/tests/test_models.py b/server/vbv_lernwelt/learnpath/tests/test_models.py new file mode 100644 index 00000000..c89786ba --- /dev/null +++ b/server/vbv_lernwelt/learnpath/tests/test_models.py @@ -0,0 +1,33 @@ +from django.test import TestCase + +from vbv_lernwelt.core.create_default_users import create_default_users +from vbv_lernwelt.course.creators.test_course import create_test_course +from vbv_lernwelt.learnpath.models import LearningContent + + +class SaveSlugTestCase(TestCase): + def setUp(self) -> None: + create_default_users() + create_test_course() + + def test_save_willHandleSlug(self): + lc_fachcheck = LearningContent.objects.get(title="Fachcheck Fahrzeug") + self.assertEqual( + lc_fachcheck.slug, "test-lehrgang-lp-circle-analyse-lc-fachcheck-fahrzeug" + ) + + # only changing minutes should not change slug + lc_fachcheck.minutes = 135 + lc_fachcheck.save() + lc_fachcheck = LearningContent.objects.get(id=lc_fachcheck.id) + self.assertEqual( + lc_fachcheck.slug, "test-lehrgang-lp-circle-analyse-lc-fachcheck-fahrzeug" + ) + + # changing title should change slug + lc_fachcheck.title = "Fachcheck Foobar" + lc_fachcheck.save() + lc_fachcheck = LearningContent.objects.get(id=lc_fachcheck.id) + self.assertEqual( + lc_fachcheck.slug, "test-lehrgang-lp-circle-analyse-lc-fachcheck-foobar" + ) diff --git a/server/vbv_lernwelt/media_library/models.py b/server/vbv_lernwelt/media_library/models.py index 763cde4d..5ce5a8d3 100644 --- a/server/vbv_lernwelt/media_library/models.py +++ b/server/vbv_lernwelt/media_library/models.py @@ -25,7 +25,8 @@ class MediaLibraryPage(CourseBasePage): def save(self, clean=True, user=None, log_action=False, **kwargs): self.slug = find_available_slug( - slugify(f"{self.get_parent().slug}-media", allow_unicode=True) + slugify(f"{self.get_parent().slug}-media", allow_unicode=True), + ignore_page_id=self.id, ) super(MediaLibraryPage, self).save(clean, user, log_action, **kwargs) @@ -86,7 +87,8 @@ class MediaCategoryPage(CourseBasePage): def save(self, clean=True, user=None, log_action=False, **kwargs): self.slug = find_available_slug( - slugify(f"{self.get_parent().slug}-cat-{self.title}", allow_unicode=True) + slugify(f"{self.get_parent().slug}-cat-{self.title}", allow_unicode=True), + ignore_page_id=self.id, ) super(MediaCategoryPage, self).save(clean, user, log_action, **kwargs)