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..38610ad3 --- /dev/null +++ b/client/src/pages/learningPath/learningContentPage/assignment/AssignmentView.vue @@ -0,0 +1,64 @@ + + + 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/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 bb6c7ae2..67aaf77f 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; @@ -348,6 +405,11 @@ export interface CourseSessionAttendanceDay { trainer: string; } +export interface CourseSessionAssignmentDetails { + learningContentId: number; + deadlineDateTimeUtc: string; +} + export interface CourseSession { id: number; created_at: string; @@ -361,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/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/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/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/__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..5a2ee9f3 --- /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 = "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..62b196e6 --- /dev/null +++ b/server/vbv_lernwelt/assignment/creators/create_assignments.py @@ -0,0 +1,285 @@ +from vbv_lernwelt.assignment.models import TaskContentStreamBlock +from vbv_lernwelt.assignment.tests.assignment_factories import ( + AssignmentFactory, + 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_uk_assignments(course_id=COURSE_UK): + course_page = CoursePage.objects.get(course_id=course_id) + assignment_page = AssignmentListPageFactory( + parent=course_page, + ) + + assignment = AssignmentFactory( + parent=assignment_page, + 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. + """ + ), + 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", + 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/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/migrations/0001_initial.py b/server/vbv_lernwelt/assignment/migrations/0001_initial.py new file mode 100644 index 00000000..e3441d81 --- /dev/null +++ b/server/vbv_lernwelt/assignment/migrations/0001_initial.py @@ -0,0 +1,156 @@ +# Generated by Django 3.2.13 on 2023-04-11 09:30 + +import django.db.models.deletion +import wagtail.blocks +import wagtail.fields +from django.db import migrations, models + +import vbv_lernwelt.assignment.models + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ("wagtailcore", "0069_log_entry_jsonfield"), + ] + + 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(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 Beurteilungsinstrument", + 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", + }, + bases=("wagtailcore.page",), + ), + migrations.CreateModel( + 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", + ), + ), + ], + options={ + "abstract": False, + }, + 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..5f8dac4a --- /dev/null +++ b/server/vbv_lernwelt/assignment/models.py @@ -0,0 +1,135 @@ +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) + + +class ExplanationBlock(blocks.StructBlock): + text = blocks.TextBlock() + + class Meta: + icon = "comment" + + +class PerformanceObjectiveBlock(blocks.StructBlock): + text = blocks.TextBlock() + + class Meta: + icon = "tick" + + +class UserTextInputBlock(blocks.StaticBlock): + text = blocks.TextBlock(blank=True) + + class Meta: + icon = "edit" + + +class UserConfirmationBlock(blocks.StructBlock): + text = blocks.TextBlock() + + class Meta: + 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 = TaskContentStreamBlock( + blank=True, + ) + + class Meta: + icon = "tasks" + label = "Teilauftrag" + + +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 + ) + + performance_objectives = StreamField( + [ + ("performance_objective", PerformanceObjectiveBlock()), + ], + use_json_field=True, + blank=True, + help_text="Leistungsziele des Auftrags", + ) + 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 Beurteilungsinstrument", + ) + + tasks = StreamField( + [ + ("task", TaskBlock()), + ], + use_json_field=True, + blank=True, + help_text="Teilaufgaben", + ) + + content_panels = Page.content_panels + [ + FieldPanel("starting_position"), + FieldPanel("effort_required"), + FieldPanel("performance_objectives"), + FieldPanel("assessment_description"), + FieldPanel("assessment_document_url"), + FieldPanel("tasks"), + ] + + subpage_types = [] + + class Meta: + verbose_name = "Auftrag" + + 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/__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 new file mode 100644 index 00000000..c152422d --- /dev/null +++ b/server/vbv_lernwelt/assignment/tests/assignment_factories.py @@ -0,0 +1,80 @@ +import wagtail_factories +from factory import SubFactory + +from vbv_lernwelt.assignment.models import ( + Assignment, + AssignmentListPage, + ExplanationBlock, + PerformanceObjectiveBlock, + TaskBlock, + TaskContentStreamBlock, + UserConfirmationBlock, + UserTextInputBlock, +) +from vbv_lernwelt.core.utils import replace_whitespace + + +class ExplanationBlockFactory(wagtail_factories.StructBlockFactory): + text = "Dies ist ein Beispieltext." + + class Meta: + model = ExplanationBlock + + +class UserConfirmationBlockFactory(wagtail_factories.StructBlockFactory): + text = "Ja, ich habe Motorfahrzeugversicherungspolice von jemandem aus meiner Familie oder meinem Freundeskreis erhalten." + + class Meta: + 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 = TaskContentStreamBlockFactory() + + 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 = 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 + + +class AssignmentListPageFactory(wagtail_factories.PageFactory): + title = "Aufträge" + + class Meta: + model = AssignmentListPage 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/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() 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..fd779e9b 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) @@ -156,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 0eb08b49..92facbab 100644 --- a/server/vbv_lernwelt/course/management/commands/create_uk_course.py +++ b/server/vbv_lernwelt/course/management/commands/create_uk_course.py @@ -5,10 +5,12 @@ 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 from vbv_lernwelt.learnpath.tests.learning_path_factories import ( + AssignmentBlockFactory, AttendanceDayBlockFactory, CircleFactory, FeedbackBlockFactory, @@ -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/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 6abf0e9d..b2903b07 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.AssignmentListPage", ] course = models.OneToOneField("course.Course", on_delete=models.PROTECT) @@ -185,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", 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() 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..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,4 +1,4 @@ -# Generated by Django 3.2.13 on 2023-04-03 16:05 +# Generated by Django 3.2.13 on 2023-04-04 08:28 import wagtail.blocks import wagtail.fields @@ -104,8 +104,13 @@ class Migration(migrations.Migration): wagtail.blocks.StructBlock( [ ("description", wagtail.blocks.TextBlock()), - ("url", wagtail.blocks.TextBlock()), - ("text", wagtail.blocks.RichTextBlock(required=False)), + ( + "assignment", + wagtail.blocks.PageChooserBlock( + help_text="Choose the corresponding assignment.", + required=True, + ), + ), ] ), ), diff --git a/server/vbv_lernwelt/learnpath/models_learning_unit_content.py b/server/vbv_lernwelt/learnpath/models_learning_unit_content.py index ce7351a8..0fb69bd9 100644 --- a/server/vbv_lernwelt/learnpath/models_learning_unit_content.py +++ b/server/vbv_lernwelt/learnpath/models_learning_unit_content.py @@ -1,10 +1,12 @@ from wagtail import blocks +from wagtail.blocks import PageChooserBlock class AssignmentBlock(blocks.StructBlock): description = blocks.TextBlock() - url = blocks.TextBlock() - text = blocks.RichTextBlock(required=False) + assignment = PageChooserBlock( + required=True, help_text="Choose the corresponding assignment." + ) class Meta: icon = "media"