diff --git a/client/src/gql/graphql.ts b/client/src/gql/graphql.ts index 09584176..a23ed2ab 100644 --- a/client/src/gql/graphql.ts +++ b/client/src/gql/graphql.ts @@ -45,6 +45,10 @@ export type Scalars = { export type AssignmentAssignmentAssignmentTypeChoices = /** CASEWORK */ | 'CASEWORK' + /** CONDITION_ACCEPTANCE */ + | 'CONDITION_ACCEPTANCE' + /** EDONIQ_TEST */ + | 'EDONIQ_TEST' /** PREP_ASSIGNMENT */ | 'PREP_ASSIGNMENT' /** REFLECTION */ @@ -94,7 +98,9 @@ export type AssignmentCompletionStatus = export type AssignmentObjectType = CoursePageInterface & { __typename?: 'AssignmentObjectType'; assignment_type: AssignmentAssignmentAssignmentTypeChoices; + circle?: Maybe; content_type?: Maybe; + course?: Maybe; /** Zeitaufwand als Text */ effort_required: Scalars['String']['output']; /** Beschreibung der Bewertung */ @@ -106,7 +112,9 @@ export type AssignmentObjectType = CoursePageInterface & { id?: Maybe; /** Erläuterung der Ausgangslage */ intro_text: Scalars['String']['output']; + learning_content?: Maybe; live?: Maybe; + max_points?: Maybe; performance_objectives?: Maybe; slug?: Maybe; tasks?: Maybe; @@ -140,7 +148,9 @@ export type AttendanceUserType = { export type CircleObjectType = CoursePageInterface & { __typename?: 'CircleObjectType'; + circle?: Maybe; content_type?: Maybe; + course?: Maybe; description: Scalars['String']['output']; frontend_url?: Maybe; goals: Scalars['String']['output']; @@ -152,6 +162,20 @@ export type CircleObjectType = CoursePageInterface & { translation_key?: Maybe; }; +export type CompetenceCertificateObjectType = CoursePageInterface & { + __typename?: 'CompetenceCertificateObjectType'; + assignments?: Maybe>>; + circle?: Maybe; + content_type?: Maybe; + course?: Maybe; + frontend_url?: Maybe; + id?: Maybe; + live?: Maybe; + slug?: Maybe; + title?: Maybe; + translation_key?: Maybe; +}; + /** An enumeration. */ export type CoreUserLanguageChoices = /** Deutsch */ @@ -171,7 +195,9 @@ export type CourseObjectType = { }; export type CoursePageInterface = { + circle?: Maybe; content_type?: Maybe; + course?: Maybe; frontend_url?: Maybe; id?: Maybe; live?: Maybe; @@ -211,9 +237,11 @@ export type FeedbackResponse = Node & { export type LearningContentAssignmentObjectType = LearningContentInterface & { __typename?: 'LearningContentAssignmentObjectType'; assignment_type: LearnpathLearningContentAssignmentAssignmentTypeChoices; + circle?: Maybe; content?: Maybe; content_assignment: AssignmentObjectType; content_type?: Maybe; + course?: Maybe; description?: Maybe; frontend_url?: Maybe; id?: Maybe; @@ -226,8 +254,10 @@ export type LearningContentAssignmentObjectType = LearningContentInterface & { export type LearningContentAttendanceCourseObjectType = LearningContentInterface & { __typename?: 'LearningContentAttendanceCourseObjectType'; + circle?: Maybe; content?: Maybe; content_type?: Maybe; + course?: Maybe; description?: Maybe; frontend_url?: Maybe; id?: Maybe; @@ -240,8 +270,10 @@ export type LearningContentAttendanceCourseObjectType = LearningContentInterface export type LearningContentDocumentListObjectType = LearningContentInterface & { __typename?: 'LearningContentDocumentListObjectType'; + circle?: Maybe; content?: Maybe; content_type?: Maybe; + course?: Maybe; description?: Maybe; frontend_url?: Maybe; id?: Maybe; @@ -254,8 +286,10 @@ export type LearningContentDocumentListObjectType = LearningContentInterface & { export type LearningContentFeedbackObjectType = LearningContentInterface & { __typename?: 'LearningContentFeedbackObjectType'; + circle?: Maybe; content?: Maybe; content_type?: Maybe; + course?: Maybe; description?: Maybe; frontend_url?: Maybe; id?: Maybe; @@ -267,8 +301,10 @@ export type LearningContentFeedbackObjectType = LearningContentInterface & { }; export type LearningContentInterface = { + circle?: Maybe; content?: Maybe; content_type?: Maybe; + course?: Maybe; description?: Maybe; frontend_url?: Maybe; id?: Maybe; @@ -281,8 +317,10 @@ export type LearningContentInterface = { export type LearningContentLearningModuleObjectType = LearningContentInterface & { __typename?: 'LearningContentLearningModuleObjectType'; + circle?: Maybe; content?: Maybe; content_type?: Maybe; + course?: Maybe; description?: Maybe; frontend_url?: Maybe; id?: Maybe; @@ -295,8 +333,10 @@ export type LearningContentLearningModuleObjectType = LearningContentInterface & export type LearningContentMediaLibraryObjectType = LearningContentInterface & { __typename?: 'LearningContentMediaLibraryObjectType'; + circle?: Maybe; content?: Maybe; content_type?: Maybe; + course?: Maybe; description?: Maybe; frontend_url?: Maybe; id?: Maybe; @@ -309,8 +349,10 @@ export type LearningContentMediaLibraryObjectType = LearningContentInterface & { export type LearningContentPlaceholderObjectType = LearningContentInterface & { __typename?: 'LearningContentPlaceholderObjectType'; + circle?: Maybe; content?: Maybe; content_type?: Maybe; + course?: Maybe; description?: Maybe; frontend_url?: Maybe; id?: Maybe; @@ -323,8 +365,10 @@ export type LearningContentPlaceholderObjectType = LearningContentInterface & { export type LearningContentRichTextObjectType = LearningContentInterface & { __typename?: 'LearningContentRichTextObjectType'; + circle?: Maybe; content?: Maybe; content_type?: Maybe; + course?: Maybe; description?: Maybe; frontend_url?: Maybe; id?: Maybe; @@ -337,8 +381,11 @@ export type LearningContentRichTextObjectType = LearningContentInterface & { export type LearningContentTestObjectType = LearningContentInterface & { __typename?: 'LearningContentTestObjectType'; + circle?: Maybe; content?: Maybe; + content_assignment?: Maybe; content_type?: Maybe; + course?: Maybe; description?: Maybe; frontend_url?: Maybe; id?: Maybe; @@ -351,8 +398,10 @@ export type LearningContentTestObjectType = LearningContentInterface & { export type LearningContentVideoObjectType = LearningContentInterface & { __typename?: 'LearningContentVideoObjectType'; + circle?: Maybe; content?: Maybe; content_type?: Maybe; + course?: Maybe; description?: Maybe; frontend_url?: Maybe; id?: Maybe; @@ -365,7 +414,9 @@ export type LearningContentVideoObjectType = LearningContentInterface & { export type LearningPathObjectType = CoursePageInterface & { __typename?: 'LearningPathObjectType'; + circle?: Maybe; content_type?: Maybe; + course?: Maybe; depth: Scalars['Int']['output']; draft_title: Scalars['String']['output']; expire_at?: Maybe; @@ -399,7 +450,9 @@ export type LearningPathObjectType = CoursePageInterface & { export type LearningSequenceObjectType = CoursePageInterface & { __typename?: 'LearningSequenceObjectType'; + circle?: Maybe; content_type?: Maybe; + course?: Maybe; frontend_url?: Maybe; icon: Scalars['String']['output']; id?: Maybe; @@ -412,7 +465,9 @@ export type LearningSequenceObjectType = CoursePageInterface & { export type LearningUnitObjectType = CoursePageInterface & { __typename?: 'LearningUnitObjectType'; + circle?: Maybe; content_type?: Maybe; + course?: Maybe; frontend_url?: Maybe; id?: Maybe; learning_contents?: Maybe>>; @@ -426,6 +481,10 @@ export type LearningUnitObjectType = CoursePageInterface & { export type LearnpathLearningContentAssignmentAssignmentTypeChoices = /** CASEWORK */ | 'CASEWORK' + /** CONDITION_ACCEPTANCE */ + | 'CONDITION_ACCEPTANCE' + /** EDONIQ_TEST */ + | 'EDONIQ_TEST' /** PREP_ASSIGNMENT */ | 'PREP_ASSIGNMENT' /** REFLECTION */ @@ -472,6 +531,7 @@ export type Query = { assignment?: Maybe; assignment_completion?: Maybe; circle?: Maybe; + competence_certificate?: Maybe; course?: Maybe; course_session_attendance_course?: Maybe; learning_content_assignment?: Maybe; @@ -508,6 +568,11 @@ export type QueryCircleArgs = { }; +export type QueryCompetenceCertificateArgs = { + id?: InputMaybe; +}; + + export type QueryCourseArgs = { id?: InputMaybe; }; @@ -541,8 +606,10 @@ export type SendFeedbackPayload = { export type TopicObjectType = CoursePageInterface & { __typename?: 'TopicObjectType'; + circle?: Maybe; circles?: Maybe>>; content_type?: Maybe; + course?: Maybe; frontend_url?: Maybe; id?: Maybe; is_visible: Scalars['Boolean']['output']; diff --git a/client/src/gql/schema.graphql b/client/src/gql/schema.graphql index 47a08ee2..cc80187a 100644 --- a/client/src/gql/schema.graphql +++ b/client/src/gql/schema.graphql @@ -13,6 +13,7 @@ type Query { learning_content_document_list: LearningContentDocumentListObjectType course_session_attendance_course(id: ID!, assignment_user_id: ID): CourseSessionAttendanceCourseType course(id: Int): CourseObjectType + competence_certificate(id: ID): CompetenceCertificateObjectType assignment(id: ID, slug: String): AssignmentObjectType assignment_completion(assignment_id: ID!, course_session_id: ID!, learning_content_page_id: ID, assignment_user_id: UUID): AssignmentCompletionObjectType } @@ -27,6 +28,8 @@ type CircleObjectType implements CoursePageInterface { live: Boolean translation_key: String frontend_url: String + circle: CircleObjectType + course: CourseObjectType learning_sequences: [LearningSequenceObjectType] } @@ -38,42 +41,16 @@ interface CoursePageInterface { live: Boolean translation_key: String frontend_url: String + circle: CircleObjectType + course: CourseObjectType } -type LearningSequenceObjectType implements CoursePageInterface { - icon: String! - id: ID - title: String - slug: String - content_type: String - live: Boolean - translation_key: String - frontend_url: String - learning_units: [LearningUnitObjectType] -} - -type LearningUnitObjectType implements CoursePageInterface { - id: ID - title: String - slug: String - content_type: String - live: Boolean - translation_key: String - frontend_url: String - learning_contents: [LearningContentInterface] -} - -interface LearningContentInterface { - id: ID - title: String - slug: String - content_type: String - live: Boolean - translation_key: String - frontend_url: String - minutes: Int - description: String - content: String +type CourseObjectType { + id: ID! + title: String! + category_name: String! + slug: String! + learning_path: LearningPathObjectType } type LearningPathObjectType implements CoursePageInterface { @@ -115,6 +92,8 @@ type LearningPathObjectType implements CoursePageInterface { search_description: String! latest_revision_created_at: DateTime frontend_url: String + circle: CircleObjectType + course: CourseObjectType topics: [TopicObjectType] } @@ -165,9 +144,53 @@ type TopicObjectType implements CoursePageInterface { live: Boolean translation_key: String frontend_url: String + circle: CircleObjectType + course: CourseObjectType circles: [CircleObjectType] } +type LearningSequenceObjectType implements CoursePageInterface { + icon: String! + id: ID + title: String + slug: String + content_type: String + live: Boolean + translation_key: String + frontend_url: String + circle: CircleObjectType + course: CourseObjectType + learning_units: [LearningUnitObjectType] +} + +type LearningUnitObjectType implements CoursePageInterface { + id: ID + title: String + slug: String + content_type: String + live: Boolean + translation_key: String + frontend_url: String + circle: CircleObjectType + course: CourseObjectType + learning_contents: [LearningContentInterface] +} + +interface LearningContentInterface { + id: ID + title: String + slug: String + content_type: String + live: Boolean + translation_key: String + frontend_url: String + circle: CircleObjectType + course: CourseObjectType + minutes: Int + description: String + content: String +} + type LearningContentMediaLibraryObjectType implements LearningContentInterface { id: ID title: String @@ -176,6 +199,8 @@ type LearningContentMediaLibraryObjectType implements LearningContentInterface { live: Boolean translation_key: String frontend_url: String + circle: CircleObjectType + course: CourseObjectType minutes: Int description: String content: String @@ -191,6 +216,8 @@ type LearningContentAssignmentObjectType implements LearningContentInterface { live: Boolean translation_key: String frontend_url: String + circle: CircleObjectType + course: CourseObjectType minutes: Int description: String content: String @@ -217,9 +244,13 @@ type AssignmentObjectType implements CoursePageInterface { live: Boolean translation_key: String frontend_url: String + circle: CircleObjectType + course: CourseObjectType tasks: JSONStreamField evaluation_tasks: JSONStreamField performance_objectives: JSONStreamField + max_points: Int + learning_content: LearningContentInterface } """An enumeration.""" @@ -232,6 +263,12 @@ enum AssignmentAssignmentAssignmentTypeChoices { """REFLECTION""" REFLECTION + + """CONDITION_ACCEPTANCE""" + CONDITION_ACCEPTANCE + + """EDONIQ_TEST""" + EDONIQ_TEST } scalar JSONStreamField @@ -246,6 +283,12 @@ enum LearnpathLearningContentAssignmentAssignmentTypeChoices { """REFLECTION""" REFLECTION + + """CONDITION_ACCEPTANCE""" + CONDITION_ACCEPTANCE + + """EDONIQ_TEST""" + EDONIQ_TEST } type LearningContentAttendanceCourseObjectType implements LearningContentInterface { @@ -256,6 +299,8 @@ type LearningContentAttendanceCourseObjectType implements LearningContentInterfa live: Boolean translation_key: String frontend_url: String + circle: CircleObjectType + course: CourseObjectType minutes: Int description: String content: String @@ -269,6 +314,8 @@ type LearningContentFeedbackObjectType implements LearningContentInterface { live: Boolean translation_key: String frontend_url: String + circle: CircleObjectType + course: CourseObjectType minutes: Int description: String content: String @@ -282,6 +329,8 @@ type LearningContentLearningModuleObjectType implements LearningContentInterface live: Boolean translation_key: String frontend_url: String + circle: CircleObjectType + course: CourseObjectType minutes: Int description: String content: String @@ -295,6 +344,8 @@ type LearningContentPlaceholderObjectType implements LearningContentInterface { live: Boolean translation_key: String frontend_url: String + circle: CircleObjectType + course: CourseObjectType minutes: Int description: String content: String @@ -308,12 +359,15 @@ type LearningContentRichTextObjectType implements LearningContentInterface { live: Boolean translation_key: String frontend_url: String + circle: CircleObjectType + course: CourseObjectType minutes: Int description: String content: String } type LearningContentTestObjectType implements LearningContentInterface { + content_assignment: AssignmentObjectType id: ID title: String slug: String @@ -321,6 +375,8 @@ type LearningContentTestObjectType implements LearningContentInterface { live: Boolean translation_key: String frontend_url: String + circle: CircleObjectType + course: CourseObjectType minutes: Int description: String content: String @@ -334,6 +390,8 @@ type LearningContentVideoObjectType implements LearningContentInterface { live: Boolean translation_key: String frontend_url: String + circle: CircleObjectType + course: CourseObjectType minutes: Int description: String content: String @@ -347,6 +405,8 @@ type LearningContentDocumentListObjectType implements LearningContentInterface { live: Boolean translation_key: String frontend_url: String + circle: CircleObjectType + course: CourseObjectType minutes: Int description: String content: String @@ -378,12 +438,17 @@ enum AttendanceUserStatus { ABSENT } -type CourseObjectType { - id: ID! - title: String! - category_name: String! - slug: String! - learning_path: LearningPathObjectType +type CompetenceCertificateObjectType implements CoursePageInterface { + id: ID + title: String + slug: String + content_type: String + live: Boolean + translation_key: String + frontend_url: String + circle: CircleObjectType + course: CourseObjectType + assignments: [AssignmentObjectType] } type AssignmentCompletionObjectType { @@ -492,4 +557,4 @@ enum AssignmentCompletionStatus { SUBMITTED EVALUATION_IN_PROGRESS EVALUATION_SUBMITTED -} +} \ No newline at end of file diff --git a/client/src/gql/typenames.ts b/client/src/gql/typenames.ts index fb87f08b..51e70750 100644 --- a/client/src/gql/typenames.ts +++ b/client/src/gql/typenames.ts @@ -10,6 +10,7 @@ export const AttendanceUserStatus = "AttendanceUserStatus"; export const AttendanceUserType = "AttendanceUserType"; export const Boolean = "Boolean"; export const CircleObjectType = "CircleObjectType"; +export const CompetenceCertificateObjectType = "CompetenceCertificateObjectType"; export const CoreUserLanguageChoices = "CoreUserLanguageChoices"; export const CourseObjectType = "CourseObjectType"; export const CoursePageInterface = "CoursePageInterface"; diff --git a/scripts/competenceCertificate.graphql b/scripts/competenceCertificate.graphql new file mode 100644 index 00000000..40e842b2 --- /dev/null +++ b/scripts/competenceCertificate.graphql @@ -0,0 +1,15 @@ +{ + competence_certificate(id:1) { + title, + assignments { + id + title + max_points + learning_content { + title + frontend_url + content_type + } + } + } +} \ No newline at end of file diff --git a/scripts/learningPath.graphql b/scripts/learningPath.graphql new file mode 100644 index 00000000..0130bf1f --- /dev/null +++ b/scripts/learningPath.graphql @@ -0,0 +1,30 @@ +{ + learning_path(slug: "test-lehrgang-lp") { + id + title + content_type + topics { + id + title + content_type + circles { + id + title + content_type + learning_sequences { + id + title + icon + learning_units { + id + title + learning_contents { + id + title + } + } + } + } + } + } +} diff --git a/server/vbv_lernwelt/assignment/creators/create_assignments.py b/server/vbv_lernwelt/assignment/creators/create_assignments.py index 861d8dca..8985926b 100644 --- a/server/vbv_lernwelt/assignment/creators/create_assignments.py +++ b/server/vbv_lernwelt/assignment/creators/create_assignments.py @@ -25,7 +25,7 @@ from wagtail.blocks.list_block import ListBlock, ListValue from wagtail.rich_text import RichText -def create_uk_fahrzeug_casework(course_id=COURSE_UK): +def create_uk_fahrzeug_casework(course_id=COURSE_UK, competence_certificate=None): assignment_list_page = ( CoursePage.objects.get(course_id=course_id) .get_children() @@ -36,6 +36,7 @@ def create_uk_fahrzeug_casework(course_id=COURSE_UK): assignment = AssignmentFactory( parent=assignment_list_page, title="Überprüfen einer Motorfahrzeugs-Versicherungspolice", + competence_certificate=competence_certificate, effort_required="ca. 5 Stunden", intro_text=replace_whitespace( """ diff --git a/server/vbv_lernwelt/assignment/graphql/types.py b/server/vbv_lernwelt/assignment/graphql/types.py index 3ba46191..b998cb27 100644 --- a/server/vbv_lernwelt/assignment/graphql/types.py +++ b/server/vbv_lernwelt/assignment/graphql/types.py @@ -5,12 +5,15 @@ from graphene_django import DjangoObjectType from vbv_lernwelt.assignment.models import Assignment, AssignmentCompletion from vbv_lernwelt.core.graphql.types import JSONStreamField from vbv_lernwelt.course.graphql.interfaces import CoursePageInterface +from vbv_lernwelt.learnpath.graphql.types import LearningContentInterface class AssignmentObjectType(DjangoObjectType): tasks = JSONStreamField() evaluation_tasks = JSONStreamField() performance_objectives = JSONStreamField() + max_points = graphene.Int() + learning_content = graphene.Field(LearningContentInterface) class Meta: model = Assignment @@ -21,8 +24,15 @@ class AssignmentObjectType(DjangoObjectType): "effort_required", "evaluation_description", "evaluation_document_url", + "learning_content", ) + def resolve_max_points(self, info): + return self.get_max_points() + + def resolve_learning_content(self, info): + return self.find_attached_learning_content() + class AssignmentCompletionObjectType(DjangoObjectType): completion_data = GenericScalar() diff --git a/server/vbv_lernwelt/assignment/migrations/0007_auto_20230901_1112.py b/server/vbv_lernwelt/assignment/migrations/0007_auto_20230901_1112.py new file mode 100644 index 00000000..e32156f7 --- /dev/null +++ b/server/vbv_lernwelt/assignment/migrations/0007_auto_20230901_1112.py @@ -0,0 +1,25 @@ +# Generated by Django 3.2.20 on 2023-09-01 09:12 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('competence', '0003_competencecertificate_competencecertificatelist_competencenavipage'), + ('assignment', '0006_auto_20230823_1127'), + ] + + operations = [ + migrations.AddField( + model_name='assignment', + name='competence_certificate', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='competence.competencecertificate'), + ), + migrations.AlterField( + model_name='assignment', + name='assignment_type', + field=models.CharField(choices=[('CASEWORK', 'CASEWORK'), ('PREP_ASSIGNMENT', 'PREP_ASSIGNMENT'), ('REFLECTION', 'REFLECTION'), ('CONDITION_ACCEPTANCE', 'CONDITION_ACCEPTANCE'), ('EDONIQ_TEST', 'EDONIQ_TEST')], default='CASEWORK', max_length=50), + ), + ] diff --git a/server/vbv_lernwelt/assignment/models.py b/server/vbv_lernwelt/assignment/models.py index 74c25df6..059eedc5 100644 --- a/server/vbv_lernwelt/assignment/models.py +++ b/server/vbv_lernwelt/assignment/models.py @@ -5,7 +5,7 @@ from django.db import models from django.db.models import UniqueConstraint from slugify import slugify from wagtail import blocks -from wagtail.admin.panels import FieldPanel +from wagtail.admin.panels import FieldPanel, PageChooserPanel from wagtail.fields import RichTextField, StreamField from wagtail.models import Page @@ -113,6 +113,7 @@ class AssignmentType(Enum): PREP_ASSIGNMENT = "PREP_ASSIGNMENT" # Vorbereitungsauftrag REFLECTION = "REFLECTION" # Reflexion CONDITION_ACCEPTANCE = "CONDITION_ACCEPTANCE" # Bedingungsannahme + EDONIQ_TEST = "EDONIQ_TEST" # EdonIQ Test class Assignment(CourseBasePage): @@ -132,6 +133,13 @@ class Assignment(CourseBasePage): default=AssignmentType.CASEWORK.value, ) + competence_certificate = models.ForeignKey( + "competence.CompetenceCertificate", + null=True, + blank=True, + on_delete=models.SET_NULL, + ) + intro_text = RichTextField( help_text="Erläuterung der Ausgangslage", features=DEFAULT_RICH_TEXT_FEATURES_WITH_HEADER, @@ -180,6 +188,7 @@ class Assignment(CourseBasePage): content_panels = Page.content_panels + [ FieldPanel("assignment_type"), + PageChooserPanel("competence_certificate", "competence.CompetenceCertificate"), FieldPanel("intro_text"), FieldPanel("effort_required"), FieldPanel("performance_objectives"), @@ -239,6 +248,23 @@ class Assignment(CourseBasePage): def get_input_tasks(self): return self.filter_user_subtasks() + self.get_evaluation_tasks() + def get_max_points(self): + return sum( + [task["value"].get("max_points", 0) for task in self.get_evaluation_tasks()] + ) + + def find_attached_learning_content(self): + """ + Returns the first learning content page attached to this assignment + """ + page = self.learningcontentassignment_set.first() + if page: + return page.specific + page = self.learningcontentedoniqtest_set.first() + if page: + return page.specific + return None + class AssignmentCompletionStatus(Enum): IN_PROGRESS = "IN_PROGRESS" diff --git a/server/vbv_lernwelt/competence/factories.py b/server/vbv_lernwelt/competence/factories.py index 9ff245bc..ce9f05f8 100644 --- a/server/vbv_lernwelt/competence/factories.py +++ b/server/vbv_lernwelt/competence/factories.py @@ -4,9 +4,33 @@ from vbv_lernwelt.competence.models import ( CompetencePage, CompetenceProfilePage, PerformanceCriteria, + CompetenceCertificate, + CompetenceNaviPage, + CompetenceCertificateList, ) +class CompetenceNaviPageFactory(wagtail_factories.PageFactory): + title = "KompetenzNavi" + + class Meta: + model = CompetenceNaviPage + + +class CompetenceCertificateListFactory(wagtail_factories.PageFactory): + title = "Kompetenznachweise" + + class Meta: + model = CompetenceCertificateList + + +class CompetenceCertificateFactory(wagtail_factories.PageFactory): + title = "Kompetenznachweis" + + class Meta: + model = CompetenceCertificate + + class CompetenceProfilePageFactory(wagtail_factories.PageFactory): title = "KompetenzNavi" diff --git a/server/vbv_lernwelt/competence/graphql/__init__.py b/server/vbv_lernwelt/competence/graphql/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/server/vbv_lernwelt/competence/graphql/queries.py b/server/vbv_lernwelt/competence/graphql/queries.py new file mode 100644 index 00000000..182cf6f1 --- /dev/null +++ b/server/vbv_lernwelt/competence/graphql/queries.py @@ -0,0 +1,14 @@ +import graphene + +from vbv_lernwelt.competence.graphql.types import CompetenceCertificateObjectType +from vbv_lernwelt.competence.models import CompetenceCertificate + + +class CompetenceCertificateQuery(object): + competence_certificate = graphene.Field( + CompetenceCertificateObjectType, + id=graphene.ID(), + ) + + def resolve_competence_certificate(root, info, id): + return CompetenceCertificate.objects.get(id=id) diff --git a/server/vbv_lernwelt/competence/graphql/types.py b/server/vbv_lernwelt/competence/graphql/types.py new file mode 100644 index 00000000..1b46c9aa --- /dev/null +++ b/server/vbv_lernwelt/competence/graphql/types.py @@ -0,0 +1,19 @@ +from graphene import List +from graphene_django import DjangoObjectType + +from vbv_lernwelt.assignment.graphql.types import AssignmentObjectType +from vbv_lernwelt.competence.models import CompetenceCertificate +from vbv_lernwelt.course.graphql.interfaces import CoursePageInterface + + +class CompetenceCertificateObjectType(DjangoObjectType): + assignments = List(AssignmentObjectType) + + class Meta: + model = CompetenceCertificate + interfaces = (CoursePageInterface,) + fields = ["assignments"] + + # resolver for the assignments + def resolve_assignments(self, info): + return self.assignment_set.all() diff --git a/server/vbv_lernwelt/competence/migrations/0003_competencecertificate_competencecertificatelist_competencenavipage.py b/server/vbv_lernwelt/competence/migrations/0003_competencecertificate_competencecertificatelist_competencenavipage.py new file mode 100644 index 00000000..ee1a1898 --- /dev/null +++ b/server/vbv_lernwelt/competence/migrations/0003_competencecertificate_competencecertificatelist_competencenavipage.py @@ -0,0 +1,45 @@ +# Generated by Django 3.2.20 on 2023-09-01 09:12 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('wagtailcore', '0089_log_entry_data_json_null_to_object'), + ('competence', '0002_performancecriteria_learning_unit'), + ] + + operations = [ + migrations.CreateModel( + name='CompetenceCertificate', + 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='CompetenceCertificateList', + 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='CompetenceNaviPage', + 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/competence/models.py b/server/vbv_lernwelt/competence/models.py index 45b4488b..de2328f0 100644 --- a/server/vbv_lernwelt/competence/models.py +++ b/server/vbv_lernwelt/competence/models.py @@ -9,6 +9,38 @@ from vbv_lernwelt.core.model_utils import find_available_slug from vbv_lernwelt.course.models import CourseBasePage +class CompetenceNaviPage(CourseBasePage): + parent_page_types = ["course.CoursePage"] + subpage_types = ["competence.CompetenceCertificateList"] + + def get_frontend_url(self): + return f"/course/{self.slug.replace('-competencenavi', '')}/competence" + + def save(self, clean=True, user=None, log_action=False, **kwargs): + self.slug = find_available_slug( + slugify(f"{self.get_parent().slug}-competencenavi", allow_unicode=True), + ignore_page_id=self.id, + ) + super(CompetenceNaviPage, self).save(clean, user, log_action, **kwargs) + + +class CompetenceCertificateList(CourseBasePage): + """Kompetenznachweise für einen Lehrgang""" + + parent_page_types = ["competence.CompetenceNaviPage"] + subpage_types = ["competence.CompetenceCertificate"] + + +class CompetenceCertificate(CourseBasePage): + """einzelner Kompetenznachweis""" + + parent_page_types = ["competence.CompetenceCertificateList"] + + def __str__(self): + course = self.get_course() + return f"{course.title} - {self.title}" + + class CompetenceProfilePage(CourseBasePage): serialize_field_names = [ "course", @@ -35,6 +67,8 @@ class CompetenceProfilePage(CourseBasePage): class CompetencePage(CourseBasePage): + """Handlungskompetenz""" + serialize_field_names = [ "competence_id", "children", diff --git a/server/vbv_lernwelt/core/schema.py b/server/vbv_lernwelt/core/schema.py index d389be78..11d9a045 100644 --- a/server/vbv_lernwelt/core/schema.py +++ b/server/vbv_lernwelt/core/schema.py @@ -2,6 +2,7 @@ import graphene from vbv_lernwelt.assignment.graphql.mutations import AssignmentMutation from vbv_lernwelt.assignment.graphql.queries import AssignmentQuery +from vbv_lernwelt.competence.graphql.queries import CompetenceCertificateQuery from vbv_lernwelt.course.graphql.queries import CourseQuery from vbv_lernwelt.course_session.graphql.mutations import CourseSessionMutation from vbv_lernwelt.course_session.graphql.queries import CourseSessionQuery @@ -10,7 +11,12 @@ from vbv_lernwelt.learnpath.graphql.queries import CircleQuery class Query( - AssignmentQuery, CourseQuery, CourseSessionQuery, CircleQuery, graphene.ObjectType + AssignmentQuery, + CompetenceCertificateQuery, + CourseQuery, + CourseSessionQuery, + CircleQuery, + graphene.ObjectType, ): pass diff --git a/server/vbv_lernwelt/course/creators/test_course.py b/server/vbv_lernwelt/course/creators/test_course.py index 264965bf..c25e9fd2 100644 --- a/server/vbv_lernwelt/course/creators/test_course.py +++ b/server/vbv_lernwelt/course/creators/test_course.py @@ -11,13 +11,24 @@ from vbv_lernwelt.assignment.creators.create_assignments import ( create_uk_fahrzeug_prep_assignment, create_uk_reflection, ) -from vbv_lernwelt.assignment.models import Assignment, AssignmentCompletionStatus +from vbv_lernwelt.assignment.models import ( + Assignment, + AssignmentCompletionStatus, + AssignmentListPage, +) from vbv_lernwelt.assignment.services import update_assignment_completion -from vbv_lernwelt.assignment.tests.assignment_factories import AssignmentListPageFactory +from vbv_lernwelt.assignment.tests.assignment_factories import ( + AssignmentListPageFactory, + AssignmentFactory, + EvaluationTaskBlockFactory, +) from vbv_lernwelt.competence.factories import ( CompetencePageFactory, CompetenceProfilePageFactory, PerformanceCriteriaFactory, + CompetenceNaviPageFactory, + CompetenceCertificateListFactory, + CompetenceCertificateFactory, ) from vbv_lernwelt.competence.models import CompetencePage from vbv_lernwelt.core.constants import ( @@ -74,15 +85,21 @@ def create_test_course(include_uk=True, include_vv=True, with_sessions=False): create_test_competence_profile() if include_uk: + competence_certificate = create_test_competence_navi() # assignments create assignments parent page course_page = CoursePage.objects.get(course_id=COURSE_TEST_ID) _assignment_list_page = AssignmentListPageFactory( parent=course_page, ) - create_uk_fahrzeug_casework(course_id=COURSE_TEST_ID) + create_uk_fahrzeug_casework( + course_id=COURSE_TEST_ID, competence_certificate=competence_certificate + ) create_uk_fahrzeug_prep_assignment(course_id=COURSE_TEST_ID) create_uk_condition_acceptance(course_id=COURSE_TEST_ID) create_uk_reflection(course_id=COURSE_TEST_ID) + create_test_assignment_edoniq( + course_id=COURSE_TEST_ID, competence_certificate=competence_certificate + ) create_test_learning_path(include_uk=include_uk, include_vv=include_vv) create_test_media_library() @@ -316,6 +333,9 @@ damit du erfolgreich mit deinem Lernpfad (durch-)starten kannst. description=RichText( "

Folgender Test mit Wissens- und Verständnisfragen ist Teil des Kompetenznachweises. Der Test kann nur einmal durchgeführt werden und ist notenrelevant.

" ), + content_assignment=Assignment.objects.get( + slug__startswith="test-lehrgang-assignment-edoniq-wissens-und-verständisfragen-circle-fahrzeug-demo" + ), checkbox_text="Hiermit bestätige ich, dass ich die Anweisungen verstanden und die Redlichkeitserklärung akzeptiert habe.", test_url="https://exam.vbv-afa.ch/e-tutor/v4/user/course/pre_course_object?aid=1689096897473,2147466097", extended_time_test_url="https://exam2.vbv-afa.ch/e-tutor/v4/user/course/pre_course_object?aid=1689096897473,2147466097", @@ -488,6 +508,27 @@ def create_test_competence_profile(): ) +def create_test_competence_navi(): + course_page = CoursePage.objects.get(course_id=COURSE_TEST_ID) + + competence_navi_page = CompetenceNaviPageFactory( + title="KompetenzNavi", + parent=course_page, + ) + + competence_certificate_list = CompetenceCertificateListFactory( + title="Kompetenznachweise", + parent=competence_navi_page, + ) + + competence_certificate = CompetenceCertificateFactory( + title="Kompetenznachweis 1", + parent=competence_certificate_list, + ) + + return competence_certificate + + def create_test_media_library(): course = Course.objects.get(id=COURSE_TEST_ID) course_page = CoursePage.objects.get(course_id=COURSE_TEST_ID) @@ -564,3 +605,40 @@ def create_test_media_library(): f"
  • Mit Risiken im Strassenverkehr umgehen
  • Versicherungsschutz
  • Vertragsarten
  • Zusammenfassung
" ), ) + + +def create_test_assignment_edoniq( + course_id=COURSE_TEST_ID, competence_certificate=None +): + assignment_list_page = ( + CoursePage.objects.get(course_id=course_id) + .get_children() + .exact_type(AssignmentListPage) + .first() + ) + + assignment = AssignmentFactory( + parent=assignment_list_page, + title="Edoniq Wissens- und Verständisfragen - Circle Fahrzeug (Demo)", + competence_certificate=competence_certificate, + effort_required="ca. 5 Stunden", + intro_text="Edoniq Test", + performance_objectives=[], + evaluation_document_url="", + evaluation_description="", + ) + + assignment.evaluation_tasks = [] + assignment.evaluation_tasks.append( + ( + "task", + EvaluationTaskBlockFactory( + title="Maximale Punktzahl vom Edoniq Test", + max_points=24, + ), + ), + ) + + assignment.save() + + return assignment diff --git a/server/vbv_lernwelt/course/graphql/interfaces.py b/server/vbv_lernwelt/course/graphql/interfaces.py index 10dfc250..d71a1f12 100644 --- a/server/vbv_lernwelt/course/graphql/interfaces.py +++ b/server/vbv_lernwelt/course/graphql/interfaces.py @@ -1,6 +1,7 @@ import graphene from vbv_lernwelt.core.utils import get_django_content_type +from vbv_lernwelt.learnpath.models import Circle class CoursePageInterface(graphene.Interface): @@ -11,9 +12,20 @@ class CoursePageInterface(graphene.Interface): live = graphene.Boolean() translation_key = graphene.String() frontend_url = graphene.String() + circle = graphene.Field("vbv_lernwelt.learnpath.graphql.types.CircleObjectType") + course = graphene.Field("vbv_lernwelt.course.graphql.types.CourseObjectType") def resolve_frontend_url(self, info): return self.get_frontend_url() def resolve_content_type(self, info): return get_django_content_type(self) + + def resolve_circle(self, info): + circle = self.get_ancestors().exact_type(Circle).first() + if circle: + return circle.specific + return None + + def resolve_course(self, info): + return self.get_course() diff --git a/server/vbv_lernwelt/learnpath/graphql/types.py b/server/vbv_lernwelt/learnpath/graphql/types.py index 0434efd8..c055b4d9 100644 --- a/server/vbv_lernwelt/learnpath/graphql/types.py +++ b/server/vbv_lernwelt/learnpath/graphql/types.py @@ -106,7 +106,9 @@ class LearningContentTestObjectType(DjangoObjectType): class Meta: model = LearningContentEdoniqTest interfaces = (LearningContentInterface,) - fields = [] + fields = [ + "content_assignment", + ] class LearningContentRichTextObjectType(DjangoObjectType): diff --git a/server/vbv_lernwelt/learnpath/migrations/0004_auto_20230901_1112.py b/server/vbv_lernwelt/learnpath/migrations/0004_auto_20230901_1112.py new file mode 100644 index 00000000..b5b6782e --- /dev/null +++ b/server/vbv_lernwelt/learnpath/migrations/0004_auto_20230901_1112.py @@ -0,0 +1,25 @@ +# Generated by Django 3.2.20 on 2023-09-01 09:12 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('assignment', '0007_auto_20230901_1112'), + ('learnpath', '0003_auto_20230810_0817'), + ] + + operations = [ + migrations.AddField( + model_name='learningcontentedoniqtest', + name='content_assignment', + field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to='assignment.assignment'), + ), + migrations.AlterField( + model_name='learningcontentassignment', + name='assignment_type', + field=models.CharField(choices=[('CASEWORK', 'CASEWORK'), ('PREP_ASSIGNMENT', 'PREP_ASSIGNMENT'), ('REFLECTION', 'REFLECTION'), ('CONDITION_ACCEPTANCE', 'CONDITION_ACCEPTANCE'), ('EDONIQ_TEST', 'EDONIQ_TEST')], default='CASEWORK', max_length=50), + ), + ] diff --git a/server/vbv_lernwelt/learnpath/models.py b/server/vbv_lernwelt/learnpath/models.py index a7a61585..a9ea847f 100644 --- a/server/vbv_lernwelt/learnpath/models.py +++ b/server/vbv_lernwelt/learnpath/models.py @@ -337,8 +337,16 @@ class LearningContentEdoniqTest(LearningContent): FieldPanel("checkbox_text", classname="Text"), FieldPanel("test_url", classname="Text"), FieldPanel("extended_time_test_url", classname="Text"), + PageChooserPanel("content_assignment", "assignment.Assignment"), ] + content_assignment = models.ForeignKey( + "assignment.Assignment", + # `null=True` is only set because of existing data... + null=True, + on_delete=models.SET_NULL, + ) + @property def has_extended_time_test(self): return bool(self.extended_time_test_url) diff --git a/server/vbv_lernwelt/notify/migrations/0004_alter_notification_notification_trigger.py b/server/vbv_lernwelt/notify/migrations/0004_alter_notification_notification_trigger.py new file mode 100644 index 00000000..deb22d78 --- /dev/null +++ b/server/vbv_lernwelt/notify/migrations/0004_alter_notification_notification_trigger.py @@ -0,0 +1,18 @@ +# Generated by Django 3.2.20 on 2023-09-01 09:12 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('notify', '0003_truncate_notifications'), + ] + + operations = [ + migrations.AlterField( + model_name='notification', + name='notification_trigger', + field=models.CharField(choices=[('ATTENDANCE_COURSE_REMINDER', 'Attendance Course Reminder'), ('CASEWORK_SUBMITTED', 'Casework Submitted'), ('CASEWORK_EVALUATED', 'Casework Evaluated'), ('NEW_FEEDBACK', 'New Feedback')], default='', max_length=255), + ), + ]