Load CompletionData

This commit is contained in:
Daniel Egger 2023-10-12 14:32:26 +02:00
parent 627e4f6873
commit 8621d4af07
30 changed files with 696 additions and 458 deletions

View File

@ -11,7 +11,7 @@
</template> </template>
<template #center> <template #center>
<div class="flex w-full justify-between"> <div class="flex w-full justify-between">
<div>Circle: {{ feedbacks.circle.title }}</div> <div>{{ $t("a.Circle") }}: {{ feedbacks.circle.title }}</div>
<div>{{ $t("feedback.sentByUsers", { count: feedbacks.count }) }}</div> <div>{{ $t("feedback.sentByUsers", { count: feedbacks.count }) }}</div>
</div> </div>
</template> </template>
@ -34,7 +34,7 @@ import ItRow from "@/components/ui/ItRow.vue";
import { itGet } from "@/fetchHelpers"; import { itGet } from "@/fetchHelpers";
import { onMounted, ref, watch } from "vue"; import { onMounted, ref, watch } from "vue";
import type { Circle } from "@/services/circle"; import type { OldCircle } from "@/services/oldCircle";
interface FeedbackSummary { interface FeedbackSummary {
circle_id: string; circle_id: string;
@ -42,12 +42,12 @@ interface FeedbackSummary {
} }
interface FeedbackDisplaySummary extends FeedbackSummary { interface FeedbackDisplaySummary extends FeedbackSummary {
circle: Circle; circle: OldCircle;
} }
function makeSummary( function makeSummary(
feedbackData: FeedbackSummary[], feedbackData: FeedbackSummary[],
circles: Circle[], circles: OldCircle[],
selectedCircles: string[] selectedCircles: string[]
) { ) {
const summary: FeedbackDisplaySummary[] = circles const summary: FeedbackDisplaySummary[] = circles
@ -64,7 +64,7 @@ function makeSummary(
const props = defineProps<{ const props = defineProps<{
selctedCircles: string[]; selctedCircles: string[];
circles: Circle[]; circles: OldCircle[];
courseSessionId: string; courseSessionId: string;
url: string; url: string;
}>(); }>();

View File

@ -1,7 +1,17 @@
import { COURSE_SESSION_DETAIL_QUERY, LEARNING_PATH_QUERY } from "@/graphql/queries"; import { COURSE_SESSION_DETAIL_QUERY, LEARNING_PATH_QUERY } from "@/graphql/queries";
import { circleFlatChildren } from "@/services/circle";
import { useCompletionStore } from "@/stores/completion";
import { useCourseSessionsStore } from "@/stores/courseSessions"; import { useCourseSessionsStore } from "@/stores/courseSessions";
import { useUserStore } from "@/stores/user"; import { useUserStore } from "@/stores/user";
import type { CourseSession, CourseSessionDetail, LearningPathType } from "@/types"; import type {
CourseCompletion,
CourseCompletionStatus,
CourseSession,
CourseSessionDetail,
LearningContentWithCompletion,
LearningPathType,
LearningUnitPerformanceCriteria,
} from "@/types";
import { useQuery } from "@urql/vue"; import { useQuery } from "@urql/vue";
import log from "loglevel"; import log from "loglevel";
import type { ComputedRef } from "vue"; import type { ComputedRef } from "vue";
@ -150,3 +160,71 @@ export function useLearningPathQuery(courseSlug: string) {
return { ...queryResult, learningPath, circles, findCircle }; return { ...queryResult, learningPath, circles, findCircle };
} }
export function useLearningPathWithCompletion(
courseSlug?: string,
courseSessionId?: string,
userId?: string
) {
if (!courseSlug) {
courseSlug = useCurrentCourseSession().value.course.slug;
}
if (!userId) {
userId = useUserStore().id;
}
if (!courseSessionId) {
courseSessionId = useCurrentCourseSession().value.id;
}
const lpQueryResult = useLearningPathQuery(courseSlug);
const completionStore = useCompletionStore();
function updateCompletionData() {
if (userId && courseSessionId) {
completionStore
.loadCourseSessionCompletionData(courseSessionId, userId)
.then(_parseCompletionData);
}
}
watchEffect(() => {
if (lpQueryResult.data.value) {
// load completion data when learning path data is loaded
updateCompletionData();
}
});
function _parseCompletionData(completionData: CourseCompletion[]) {
if (lpQueryResult.circles.value) {
lpQueryResult.circles.value.forEach((circle) => {
circleFlatChildren(circle).forEach((lc) => {
const pageIndex = completionData.findIndex((e) => {
return e.page_id === lc.id;
});
if (pageIndex >= 0) {
lc.completion_status = completionData[pageIndex].completion_status;
} else {
lc.completion_status = "UNKNOWN";
}
});
});
}
}
async function markCompletion(
page: LearningContentWithCompletion | LearningUnitPerformanceCriteria,
completion_status: CourseCompletionStatus = "SUCCESS"
) {
if (userId && courseSessionId) {
page.completion_status = completion_status;
const completionData = await completionStore.markPage(
page,
userId,
courseSessionId
);
_parseCompletionData(completionData);
}
}
return { ...lpQueryResult, updateCompletionData, markCompletion };
}

View File

@ -20,7 +20,7 @@ const documents = {
"\n query assignmentCompletionQuery(\n $assignmentId: ID!\n $courseSessionId: ID!\n $learningContentId: ID\n $assignmentUserId: UUID\n ) {\n assignment(id: $assignmentId) {\n assignment_type\n needs_expert_evaluation\n max_points\n content_type\n effort_required\n evaluation_description\n evaluation_document_url\n evaluation_tasks\n id\n intro_text\n performance_objectives\n slug\n tasks\n title\n translation_key\n competence_certificate {\n ...CoursePageFields\n }\n }\n assignment_completion(\n assignment_id: $assignmentId\n course_session_id: $courseSessionId\n assignment_user_id: $assignmentUserId\n learning_content_page_id: $learningContentId\n ) {\n id\n completion_status\n submitted_at\n evaluation_submitted_at\n evaluation_user {\n id\n }\n assignment_user {\n id\n }\n evaluation_points\n evaluation_max_points\n evaluation_passed\n edoniq_extended_time_flag\n completion_data\n task_completion_data\n }\n }\n": types.AssignmentCompletionQueryDocument, "\n query assignmentCompletionQuery(\n $assignmentId: ID!\n $courseSessionId: ID!\n $learningContentId: ID\n $assignmentUserId: UUID\n ) {\n assignment(id: $assignmentId) {\n assignment_type\n needs_expert_evaluation\n max_points\n content_type\n effort_required\n evaluation_description\n evaluation_document_url\n evaluation_tasks\n id\n intro_text\n performance_objectives\n slug\n tasks\n title\n translation_key\n competence_certificate {\n ...CoursePageFields\n }\n }\n assignment_completion(\n assignment_id: $assignmentId\n course_session_id: $courseSessionId\n assignment_user_id: $assignmentUserId\n learning_content_page_id: $learningContentId\n ) {\n id\n completion_status\n submitted_at\n evaluation_submitted_at\n evaluation_user {\n id\n }\n assignment_user {\n id\n }\n evaluation_points\n evaluation_max_points\n evaluation_passed\n edoniq_extended_time_flag\n completion_data\n task_completion_data\n }\n }\n": types.AssignmentCompletionQueryDocument,
"\n query competenceCertificateQuery($courseSlug: String!, $courseSessionId: ID!) {\n competence_certificate_list(course_slug: $courseSlug) {\n ...CoursePageFields\n competence_certificates {\n ...CoursePageFields\n assignments {\n ...CoursePageFields\n assignment_type\n max_points\n completion(course_session_id: $courseSessionId) {\n id\n completion_status\n submitted_at\n evaluation_points\n evaluation_max_points\n evaluation_passed\n }\n learning_content {\n ...CoursePageFields\n circle {\n ...CoursePageFields\n }\n }\n }\n }\n }\n }\n": types.CompetenceCertificateQueryDocument, "\n query competenceCertificateQuery($courseSlug: String!, $courseSessionId: ID!) {\n competence_certificate_list(course_slug: $courseSlug) {\n ...CoursePageFields\n competence_certificates {\n ...CoursePageFields\n assignments {\n ...CoursePageFields\n assignment_type\n max_points\n completion(course_session_id: $courseSessionId) {\n id\n completion_status\n submitted_at\n evaluation_points\n evaluation_max_points\n evaluation_passed\n }\n learning_content {\n ...CoursePageFields\n circle {\n ...CoursePageFields\n }\n }\n }\n }\n }\n }\n": types.CompetenceCertificateQueryDocument,
"\n query courseSessionDetail($courseSessionId: ID!) {\n course_session(id: $courseSessionId) {\n id\n title\n course {\n id\n title\n slug\n }\n users {\n id\n user_id\n first_name\n last_name\n email\n avatar_url\n role\n circles {\n id\n title\n slug\n }\n }\n attendance_courses {\n id\n location\n trainer\n due_date {\n id\n start\n end\n }\n learning_content_id\n learning_content {\n id\n title\n circle {\n id\n title\n slug\n }\n }\n }\n assignments {\n id\n submission_deadline {\n id\n start\n }\n evaluation_deadline {\n id\n start\n }\n learning_content {\n id\n title\n content_assignment {\n id\n title\n assignment_type\n }\n }\n }\n edoniq_tests {\n id\n deadline {\n id\n start\n end\n }\n learning_content {\n id\n title\n content_assignment {\n id\n title\n assignment_type\n }\n }\n }\n }\n }\n": types.CourseSessionDetailDocument, "\n query courseSessionDetail($courseSessionId: ID!) {\n course_session(id: $courseSessionId) {\n id\n title\n course {\n id\n title\n slug\n }\n users {\n id\n user_id\n first_name\n last_name\n email\n avatar_url\n role\n circles {\n id\n title\n slug\n }\n }\n attendance_courses {\n id\n location\n trainer\n due_date {\n id\n start\n end\n }\n learning_content_id\n learning_content {\n id\n title\n circle {\n id\n title\n slug\n }\n }\n }\n assignments {\n id\n submission_deadline {\n id\n start\n }\n evaluation_deadline {\n id\n start\n }\n learning_content {\n id\n title\n content_assignment {\n id\n title\n assignment_type\n }\n }\n }\n edoniq_tests {\n id\n deadline {\n id\n start\n end\n }\n learning_content {\n id\n title\n content_assignment {\n id\n title\n assignment_type\n }\n }\n }\n }\n }\n": types.CourseSessionDetailDocument,
"\n query learningPathQuery($slug: String!) {\n learning_path(slug: $slug) {\n ...CoursePageFields\n topics {\n is_visible\n ...CoursePageFields\n circles {\n ...CoursePageFields\n learning_sequences {\n icon\n ...CoursePageFields\n learning_units {\n evaluate_url\n ...CoursePageFields\n learning_contents {\n ...CoursePageFields\n ... on LearningContentAssignmentObjectType {\n assignment_type\n content_assignment {\n id\n }\n competence_certificate {\n ...CoursePageFields\n }\n }\n ... on LearningContentEdoniqTestObjectType {\n checkbox_text\n competence_certificate {\n ...CoursePageFields\n }\n }\n }\n }\n }\n }\n }\n }\n }\n": types.LearningPathQueryDocument, "\n query learningPathQuery($slug: String!) {\n learning_path(slug: $slug) {\n ...CoursePageFields\n topics {\n is_visible\n ...CoursePageFields\n circles {\n description\n goals\n ...CoursePageFields\n learning_sequences {\n icon\n ...CoursePageFields\n learning_units {\n evaluate_url\n ...CoursePageFields\n performance_criteria {\n ...CoursePageFields\n }\n learning_contents {\n can_user_self_toggle_course_completion\n ...CoursePageFields\n ... on LearningContentAssignmentObjectType {\n assignment_type\n content_assignment {\n id\n }\n competence_certificate {\n ...CoursePageFields\n }\n }\n ... on LearningContentEdoniqTestObjectType {\n checkbox_text\n competence_certificate {\n ...CoursePageFields\n }\n }\n }\n }\n }\n }\n }\n }\n }\n": types.LearningPathQueryDocument,
"\n mutation SendFeedbackMutation(\n $courseSessionId: ID!\n $learningContentId: ID!\n $data: GenericScalar!\n $submitted: Boolean\n ) {\n send_feedback(\n course_session_id: $courseSessionId\n learning_content_page_id: $learningContentId\n data: $data\n submitted: $submitted\n ) {\n feedback_response {\n id\n data\n submitted\n }\n errors {\n field\n messages\n }\n }\n }\n": types.SendFeedbackMutationDocument, "\n mutation SendFeedbackMutation(\n $courseSessionId: ID!\n $learningContentId: ID!\n $data: GenericScalar!\n $submitted: Boolean\n ) {\n send_feedback(\n course_session_id: $courseSessionId\n learning_content_page_id: $learningContentId\n data: $data\n submitted: $submitted\n ) {\n feedback_response {\n id\n data\n submitted\n }\n errors {\n field\n messages\n }\n }\n }\n": types.SendFeedbackMutationDocument,
}; };
@ -69,7 +69,7 @@ export function graphql(source: "\n query courseSessionDetail($courseSessionId:
/** /**
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients. * The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
*/ */
export function graphql(source: "\n query learningPathQuery($slug: String!) {\n learning_path(slug: $slug) {\n ...CoursePageFields\n topics {\n is_visible\n ...CoursePageFields\n circles {\n ...CoursePageFields\n learning_sequences {\n icon\n ...CoursePageFields\n learning_units {\n evaluate_url\n ...CoursePageFields\n learning_contents {\n ...CoursePageFields\n ... on LearningContentAssignmentObjectType {\n assignment_type\n content_assignment {\n id\n }\n competence_certificate {\n ...CoursePageFields\n }\n }\n ... on LearningContentEdoniqTestObjectType {\n checkbox_text\n competence_certificate {\n ...CoursePageFields\n }\n }\n }\n }\n }\n }\n }\n }\n }\n"): (typeof documents)["\n query learningPathQuery($slug: String!) {\n learning_path(slug: $slug) {\n ...CoursePageFields\n topics {\n is_visible\n ...CoursePageFields\n circles {\n ...CoursePageFields\n learning_sequences {\n icon\n ...CoursePageFields\n learning_units {\n evaluate_url\n ...CoursePageFields\n learning_contents {\n ...CoursePageFields\n ... on LearningContentAssignmentObjectType {\n assignment_type\n content_assignment {\n id\n }\n competence_certificate {\n ...CoursePageFields\n }\n }\n ... on LearningContentEdoniqTestObjectType {\n checkbox_text\n competence_certificate {\n ...CoursePageFields\n }\n }\n }\n }\n }\n }\n }\n }\n }\n"]; export function graphql(source: "\n query learningPathQuery($slug: String!) {\n learning_path(slug: $slug) {\n ...CoursePageFields\n topics {\n is_visible\n ...CoursePageFields\n circles {\n description\n goals\n ...CoursePageFields\n learning_sequences {\n icon\n ...CoursePageFields\n learning_units {\n evaluate_url\n ...CoursePageFields\n performance_criteria {\n ...CoursePageFields\n }\n learning_contents {\n can_user_self_toggle_course_completion\n ...CoursePageFields\n ... on LearningContentAssignmentObjectType {\n assignment_type\n content_assignment {\n id\n }\n competence_certificate {\n ...CoursePageFields\n }\n }\n ... on LearningContentEdoniqTestObjectType {\n checkbox_text\n competence_certificate {\n ...CoursePageFields\n }\n }\n }\n }\n }\n }\n }\n }\n }\n"): (typeof documents)["\n query learningPathQuery($slug: String!) {\n learning_path(slug: $slug) {\n ...CoursePageFields\n topics {\n is_visible\n ...CoursePageFields\n circles {\n description\n goals\n ...CoursePageFields\n learning_sequences {\n icon\n ...CoursePageFields\n learning_units {\n evaluate_url\n ...CoursePageFields\n performance_criteria {\n ...CoursePageFields\n }\n learning_contents {\n can_user_self_toggle_course_completion\n ...CoursePageFields\n ... on LearningContentAssignmentObjectType {\n assignment_type\n content_assignment {\n id\n }\n competence_certificate {\n ...CoursePageFields\n }\n }\n ... on LearningContentEdoniqTestObjectType {\n checkbox_text\n competence_certificate {\n ...CoursePageFields\n }\n }\n }\n }\n }\n }\n }\n }\n }\n"];
/** /**
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients. * The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
*/ */

File diff suppressed because one or more lines are too long

View File

@ -93,6 +93,7 @@ type LearningUnitObjectType implements CoursePageInterface {
circle: CircleObjectType circle: CircleObjectType
course: CourseObjectType course: CourseObjectType
learning_contents: [LearningContentInterface!]! learning_contents: [LearningContentInterface!]!
performance_criteria: [PerformanceCriteriaObjectType!]!
evaluate_url: String! evaluate_url: String!
} }
@ -107,8 +108,22 @@ interface LearningContentInterface {
circle: CircleObjectType! circle: CircleObjectType!
course: CourseObjectType course: CourseObjectType
minutes: Int minutes: Int
description: String description: String!
content: String content_url: String!
can_user_self_toggle_course_completion: Boolean!
}
type PerformanceCriteriaObjectType implements CoursePageInterface {
competence_id: String!
id: ID!
title: String!
slug: String!
content_type: String!
live: Boolean!
translation_key: String!
frontend_url: String!
circle: CircleObjectType
course: CourseObjectType
} }
type TopicObjectType implements CoursePageInterface { type TopicObjectType implements CoursePageInterface {
@ -136,8 +151,9 @@ type LearningContentMediaLibraryObjectType implements CoursePageInterface & Lear
circle: CircleObjectType! circle: CircleObjectType!
course: CourseObjectType course: CourseObjectType
minutes: Int minutes: Int
description: String description: String!
content: String content_url: String!
can_user_self_toggle_course_completion: Boolean!
} }
type LearningContentAssignmentObjectType implements CoursePageInterface & LearningContentInterface { type LearningContentAssignmentObjectType implements CoursePageInterface & LearningContentInterface {
@ -153,9 +169,10 @@ type LearningContentAssignmentObjectType implements CoursePageInterface & Learni
circle: CircleObjectType! circle: CircleObjectType!
course: CourseObjectType course: CourseObjectType
minutes: Int minutes: Int
description: String description: String!
content: String content_url: String!
competence_certificate: CompetenceCertificateObjectType! can_user_self_toggle_course_completion: Boolean!
competence_certificate: CompetenceCertificateObjectType
} }
type AssignmentObjectType implements CoursePageInterface { type AssignmentObjectType implements CoursePageInterface {
@ -330,8 +347,9 @@ type LearningContentAttendanceCourseObjectType implements CoursePageInterface &
circle: CircleObjectType! circle: CircleObjectType!
course: CourseObjectType course: CourseObjectType
minutes: Int minutes: Int
description: String description: String!
content: String content_url: String!
can_user_self_toggle_course_completion: Boolean!
} }
type DueDateObjectType { type DueDateObjectType {
@ -411,9 +429,10 @@ type LearningContentEdoniqTestObjectType implements CoursePageInterface & Learni
circle: CircleObjectType! circle: CircleObjectType!
course: CourseObjectType course: CourseObjectType
minutes: Int minutes: Int
description: String description: String!
content: String content_url: String!
competence_certificate: CompetenceCertificateObjectType! can_user_self_toggle_course_completion: Boolean!
competence_certificate: CompetenceCertificateObjectType
} }
type CourseSessionUserObjectsType { type CourseSessionUserObjectsType {
@ -504,8 +523,9 @@ type LearningContentFeedbackObjectType implements CoursePageInterface & Learning
circle: CircleObjectType! circle: CircleObjectType!
course: CourseObjectType course: CourseObjectType
minutes: Int minutes: Int
description: String description: String!
content: String content_url: String!
can_user_self_toggle_course_completion: Boolean!
} }
type LearningContentLearningModuleObjectType implements CoursePageInterface & LearningContentInterface { type LearningContentLearningModuleObjectType implements CoursePageInterface & LearningContentInterface {
@ -519,8 +539,9 @@ type LearningContentLearningModuleObjectType implements CoursePageInterface & Le
circle: CircleObjectType! circle: CircleObjectType!
course: CourseObjectType course: CourseObjectType
minutes: Int minutes: Int
description: String description: String!
content: String content_url: String!
can_user_self_toggle_course_completion: Boolean!
} }
type LearningContentPlaceholderObjectType implements CoursePageInterface & LearningContentInterface { type LearningContentPlaceholderObjectType implements CoursePageInterface & LearningContentInterface {
@ -534,8 +555,9 @@ type LearningContentPlaceholderObjectType implements CoursePageInterface & Learn
circle: CircleObjectType! circle: CircleObjectType!
course: CourseObjectType course: CourseObjectType
minutes: Int minutes: Int
description: String description: String!
content: String content_url: String!
can_user_self_toggle_course_completion: Boolean!
} }
type LearningContentRichTextObjectType implements CoursePageInterface & LearningContentInterface { type LearningContentRichTextObjectType implements CoursePageInterface & LearningContentInterface {
@ -549,8 +571,9 @@ type LearningContentRichTextObjectType implements CoursePageInterface & Learning
circle: CircleObjectType! circle: CircleObjectType!
course: CourseObjectType course: CourseObjectType
minutes: Int minutes: Int
description: String description: String!
content: String content_url: String!
can_user_self_toggle_course_completion: Boolean!
} }
type LearningContentVideoObjectType implements CoursePageInterface & LearningContentInterface { type LearningContentVideoObjectType implements CoursePageInterface & LearningContentInterface {
@ -564,8 +587,9 @@ type LearningContentVideoObjectType implements CoursePageInterface & LearningCon
circle: CircleObjectType! circle: CircleObjectType!
course: CourseObjectType course: CourseObjectType
minutes: Int minutes: Int
description: String description: String!
content: String content_url: String!
can_user_self_toggle_course_completion: Boolean!
} }
type LearningContentDocumentListObjectType implements CoursePageInterface & LearningContentInterface { type LearningContentDocumentListObjectType implements CoursePageInterface & LearningContentInterface {
@ -579,8 +603,9 @@ type LearningContentDocumentListObjectType implements CoursePageInterface & Lear
circle: CircleObjectType! circle: CircleObjectType!
course: CourseObjectType course: CourseObjectType
minutes: Int minutes: Int
description: String description: String!
content: String content_url: String!
can_user_self_toggle_course_completion: Boolean!
} }
type CompetenceCertificateListObjectType implements CoursePageInterface { type CompetenceCertificateListObjectType implements CoursePageInterface {

View File

@ -49,6 +49,7 @@ export const LearningSequenceObjectType = "LearningSequenceObjectType";
export const LearningUnitObjectType = "LearningUnitObjectType"; export const LearningUnitObjectType = "LearningUnitObjectType";
export const LearnpathLearningContentAssignmentAssignmentTypeChoices = "LearnpathLearningContentAssignmentAssignmentTypeChoices"; export const LearnpathLearningContentAssignmentAssignmentTypeChoices = "LearnpathLearningContentAssignmentAssignmentTypeChoices";
export const Mutation = "Mutation"; export const Mutation = "Mutation";
export const PerformanceCriteriaObjectType = "PerformanceCriteriaObjectType";
export const Query = "Query"; export const Query = "Query";
export const SendFeedbackMutation = "SendFeedbackMutation"; export const SendFeedbackMutation = "SendFeedbackMutation";
export const String = "String"; export const String = "String";

View File

@ -199,6 +199,8 @@ export const LEARNING_PATH_QUERY = graphql(`
is_visible is_visible
...CoursePageFields ...CoursePageFields
circles { circles {
description
goals
...CoursePageFields ...CoursePageFields
learning_sequences { learning_sequences {
icon icon
@ -206,7 +208,11 @@ export const LEARNING_PATH_QUERY = graphql(`
learning_units { learning_units {
evaluate_url evaluate_url
...CoursePageFields ...CoursePageFields
performance_criteria {
...CoursePageFields
}
learning_contents { learning_contents {
can_user_self_toggle_course_completion
...CoursePageFields ...CoursePageFields
... on LearningContentAssignmentObjectType { ... on LearningContentAssignmentObjectType {
assignment_type assignment_type

View File

@ -54,7 +54,7 @@ onMounted(async () => {
{{ learningContentAssignment.title }} {{ learningContentAssignment.title }}
</h2> </h2>
<div class="pt-1 underline"> <div class="pt-1 underline">
Circle «{{ learningContentAssignment.parentCircle.title }}» {{ $t("a.Circle") }} «{{ learningContentAssignment.parentCircle.title }}»
</div> </div>
<div v-if="assignmentDetail"> <div v-if="assignmentDetail">
<span v-if="assignmentDetail.submission_deadline?.start"> <span v-if="assignmentDetail.submission_deadline?.start">

View File

@ -147,7 +147,9 @@ const getIconName = (lc: LearningContent) => {
</div> </div>
<div class="flex flex-col"> <div class="flex flex-col">
<h3 class="text-bold flex items-center gap-2">{{ submittable.title }}</h3> <h3 class="text-bold flex items-center gap-2">{{ submittable.title }}</h3>
<p class="text-gray-800">Circle «{{ submittable.circleName }}»</p> <p class="text-gray-800">
{{ $t("a.Circle") }} «{{ submittable.circleName }}»
</p>
</div> </div>
</div> </div>
<AssignmentSubmissionProgress <AssignmentSubmissionProgress

View File

@ -1,6 +1,5 @@
<script setup lang="ts"> <script setup lang="ts">
import { showIcon } from "@/pages/learningPath/circlePage/learningSequenceUtils"; import { showIcon } from "@/pages/learningPath/circlePage/learningSequenceUtils";
import { useCircleStore } from "@/stores/circle";
import type { DefaultArcObject } from "d3"; import type { DefaultArcObject } from "d3";
import * as d3 from "d3"; import * as d3 from "d3";
import pick from "lodash/pick"; import pick from "lodash/pick";
@ -9,26 +8,23 @@ import { computed, onMounted } from "vue";
// @ts-ignore // @ts-ignore
import colors from "@/colors.json"; import colors from "@/colors.json";
import type { LearningSequence } from "@/types";
const circleStore = useCircleStore(); import type { CircleType, LearningSequence } from "@/types";
import {
allFinishedInLearningSequence,
someFinishedInLearningSequence,
} from "@/services/circle";
function someFinished(learningSequence: LearningSequence) { const props = defineProps<{
if (circleStore.circle) { circle: CircleType;
return circleStore.circle.someFinishedInLearningSequence( }>();
learningSequence.translation_key
);
}
return false;
}
function allFinished(learningSequence: LearningSequence) { function allFinished(learningSequence: LearningSequence) {
if (circleStore.circle) { return allFinishedInLearningSequence(learningSequence);
return circleStore.circle.allFinishedInLearningSequence( }
learningSequence.translation_key
); function someFinished(learningSequence: LearningSequence) {
} return someFinishedInLearningSequence(learningSequence);
return false;
} }
onMounted(async () => { onMounted(async () => {
@ -48,14 +44,14 @@ interface CirclePie extends d3.PieArcDatum<number> {
} }
const pieData = computed(() => { const pieData = computed(() => {
const circle = circleStore.circle; const circle = props.circle;
if (circle) { if (circle) {
const pieWeights = new Array(Math.max(circle.learningSequences.length, 1)).fill(1); const pieWeights = new Array(Math.max(circle.learning_sequences.length, 1)).fill(1);
const pieGenerator = d3.pie(); const pieGenerator = d3.pie();
const angles = pieGenerator(pieWeights); const angles = pieGenerator(pieWeights);
let result = angles.map((angle) => { let result = angles.map((angle) => {
const thisLearningSequence = circle.learningSequences[angle.index]; const thisLearningSequence = circle.learning_sequences[angle.index];
// Rotate the cirlce by PI (180 degrees) normally 0 = 12'o clock, now start at 6 o clock // Rotate the cirlce by PI (180 degrees) normally 0 = 12'o clock, now start at 6 o clock
angle.startAngle += Math.PI; angle.startAngle += Math.PI;

View File

@ -1,9 +1,9 @@
<script setup lang="ts"> <script setup lang="ts">
import ItFullScreenModal from "@/components/ui/ItFullScreenModal.vue"; import ItFullScreenModal from "@/components/ui/ItFullScreenModal.vue";
import type { Circle } from "@/services/circle"; import type { CircleType } from "@/types";
defineProps<{ defineProps<{
circle: Circle | undefined; circle: CircleType;
show: boolean; show: boolean;
}>(); }>();

View File

@ -6,7 +6,10 @@ import { useRoute } from "vue-router";
import CircleDiagram from "./CircleDiagram.vue"; import CircleDiagram from "./CircleDiagram.vue";
import CircleOverview from "./CircleOverview.vue"; import CircleOverview from "./CircleOverview.vue";
import DocumentSection from "./DocumentSection.vue"; import DocumentSection from "./DocumentSection.vue";
import { useCourseSessionDetailQuery, useLearningPathQuery } from "@/composables"; import {
useCourseSessionDetailQuery,
useLearningPathWithCompletion,
} from "@/composables";
import { stringifyParse } from "@/utils/utils"; import { stringifyParse } from "@/utils/utils";
import { useCircleStore } from "@/stores/circle"; import { useCircleStore } from "@/stores/circle";
import LearningSequence from "@/pages/learningPath/circlePage/LearningSequence.vue"; import LearningSequence from "@/pages/learningPath/circlePage/LearningSequence.vue";
@ -29,6 +32,12 @@ const props = withDefaults(defineProps<Props>(), {
log.debug("CirclePage created", stringifyParse(props)); log.debug("CirclePage created", stringifyParse(props));
const lpQueryResult = useLearningPathWithCompletion(props.courseSlug);
const circle = computed(() => {
return lpQueryResult.findCircle(props.circleSlug);
});
const circleExperts = computed(() => { const circleExperts = computed(() => {
if (circle.value) { if (circle.value) {
return courseSessionDetailResult.filterCircleExperts(circle.value.slug); return courseSessionDetailResult.filterCircleExperts(circle.value.slug);
@ -51,12 +60,6 @@ const showDuration = computed(() => {
return false; return false;
}); });
const lpQueryResult = useLearningPathQuery(props.courseSlug);
const circle = computed(() => {
return lpQueryResult.findCircle(props.circleSlug);
});
watch( watch(
() => circle.value, () => circle.value,
() => { () => {
@ -102,7 +105,7 @@ watch(
<div v-if="circle"> <div v-if="circle">
<Teleport to="body"> <Teleport to="body">
<CircleOverview <CircleOverview
:circle="circleStore.circle" :circle="circle"
:show="circleStore.page === 'OVERVIEW'" :show="circleStore.page === 'OVERVIEW'"
@closemodal="circleStore.page = 'INDEX'" @closemodal="circleStore.page = 'INDEX'"
/> />
@ -151,7 +154,7 @@ watch(
</div> </div>
<div class="mt-8 w-full"> <div class="mt-8 w-full">
<CircleDiagram></CircleDiagram> <CircleDiagram v-if="circle" :circle="circle"></CircleDiagram>
</div> </div>
<div v-if="!props.readonly" class="mt-4 border-t-2 lg:hidden"> <div v-if="!props.readonly" class="mt-4 border-t-2 lg:hidden">
<div <div
@ -221,6 +224,7 @@ watch(
:key="learningSequence.id" :key="learningSequence.id"
> >
<LearningSequence <LearningSequence
:course-slug="props.courseSlug"
:learning-sequence="learningSequence" :learning-sequence="learningSequence"
:readonly="props.readonly" :readonly="props.readonly"
></LearningSequence> ></LearningSequence>

View File

@ -3,19 +3,28 @@ import LearningContentBadge from "@/pages/learningPath/LearningContentTypeBadge.
import { showIcon } from "@/pages/learningPath/circlePage/learningSequenceUtils"; import { showIcon } from "@/pages/learningPath/circlePage/learningSequenceUtils";
import { useCircleStore } from "@/stores/circle"; import { useCircleStore } from "@/stores/circle";
import type { import type {
CourseCompletionStatus,
LearningContent, LearningContent,
LearningContentAssignment, LearningContentAssignment,
LearningContentEdoniqTest, LearningContentEdoniqTest,
LearningContentWithCompletion,
LearningSequence, LearningSequence,
} from "@/types"; } from "@/types";
import { computed } from "vue"; import { computed } from "vue";
import { humanizeDuration } from "../../../utils/humanizeDuration";
import { import {
itCheckboxDefaultIconCheckedTailwindClass, itCheckboxDefaultIconCheckedTailwindClass,
itCheckboxDefaultIconUncheckedTailwindClass, itCheckboxDefaultIconUncheckedTailwindClass,
} from "@/constants"; } from "@/constants";
import ItCheckbox from "@/components/ui/ItCheckbox.vue";
import {
allFinishedInLearningSequence,
calcSelfEvaluationStatus,
someFinishedInLearningSequence,
} from "@/services/circle";
import { useLearningPathWithCompletion } from "@/composables";
type Props = { type Props = {
courseSlug: string;
learningSequence: LearningSequence; learningSequence: LearningSequence;
readonly?: boolean; readonly?: boolean;
}; };
@ -26,30 +35,28 @@ const props = withDefaults(defineProps<Props>(), {
const circleStore = useCircleStore(); const circleStore = useCircleStore();
function toggleCompleted(learningContent: LearningContent) { const lpQueryResult = useLearningPathWithCompletion(props.courseSlug);
// let completionStatus: CourseCompletionStatus = "SUCCESS";
// if (learningContent.completion_status === "SUCCESS") { function toggleCompleted(learningContent: LearningContentWithCompletion) {
// completionStatus = "FAIL"; let completionStatus: CourseCompletionStatus = "SUCCESS";
// } if (learningContent.completion_status === "SUCCESS") {
// circleStore.markCompletion(learningContent, completionStatus); completionStatus = "FAIL";
}
lpQueryResult.markCompletion(learningContent, completionStatus);
} }
const someFinished = computed(() => { const someFinished = computed(() => {
// if (props.learningSequence && circleStore.circle) { if (props.learningSequence) {
// return circleStore.circle.someFinishedInLearningSequence( return someFinishedInLearningSequence(props.learningSequence);
// props.learningSequence.translation_key }
// );
// }
return false; return false;
}); });
const allFinished = computed(() => { const allFinished = computed(() => {
// if (props.learningSequence && circleStore.circle) { if (props.learningSequence) {
// return circleStore.circle.allFinishedInLearningSequence( return allFinishedInLearningSequence(props.learningSequence);
// props.learningSequence.translation_key }
// );
// }
return false; return false;
}); });
@ -78,13 +85,13 @@ const continueTranslationKeyTuple = computed(() => {
const learningSequenceBorderClass = computed(() => { const learningSequenceBorderClass = computed(() => {
let result: string[] = []; let result: string[] = [];
if (props.learningSequence && circleStore.circle) { if (props.learningSequence) {
if (allFinished.value) { if (allFinished.value) {
result = ["border-l-4", "border-l-green-500"]; result = ["border-l-4", "border-l-green-500"];
} else if (someFinished.value) { } else if (someFinished.value) {
result = ["border-l-4", "border-l-sky-500"]; result = ["border-l-4", "border-l-sky-500"];
} else { } else {
result = ["border-l-gray-500"]; result = ["border-l", "border-l-gray-500"];
} }
} }
@ -125,9 +132,9 @@ function checkboxIconUncheckedTailwindClass(lc: LearningContent) {
<h3 class="text-large font-semibold"> <h3 class="text-large font-semibold">
{{ learningSequence.title }} {{ learningSequence.title }}
</h3> </h3>
<div v-if="learningSequence.minutes > 0"> <!-- <div v-if="learningSequence.minutes > 0">-->
{{ humanizeDuration(learningSequence.minutes) }} <!-- {{ humanizeDuration(learningSequence.minutes) }}-->
</div> <!-- </div>-->
</div> </div>
<ol class="border bg-white px-4 lg:px-6" :class="learningSequenceBorderClass"> <ol class="border bg-white px-4 lg:px-6" :class="learningSequenceBorderClass">
@ -144,9 +151,9 @@ function checkboxIconUncheckedTailwindClass(lc: LearningContent) {
<div class="font-semibold"> <div class="font-semibold">
{{ learningUnit.title }} {{ learningUnit.title }}
</div> </div>
<div v-if="learningUnit.minutes > 0" class="whitespace-nowrap"> <!-- <div v-if="learningUnit.minutes > 0" class="whitespace-nowrap">-->
{{ humanizeDuration(learningUnit.minutes) }} <!-- {{ humanizeDuration(learningUnit.minutes) }}-->
</div> <!-- </div>-->
</div> </div>
<ol> <ol>
<li <li
@ -254,31 +261,31 @@ function checkboxIconUncheckedTailwindClass(lc: LearningContent) {
</li> </li>
</ol> </ol>
<!-- <div--> <div
<!-- v-if="learningUnit.children.length"--> v-if="learningUnit.performance_criteria.length"
<!-- :class="{ 'cursor-pointer': !props.readonly }"--> :class="{ 'cursor-pointer': !props.readonly }"
<!-- :data-cy="`${learningUnit.slug}`"--> :data-cy="`${learningUnit.slug}`"
<!-- @click="!props.readonly && circleStore.openSelfEvaluation(learningUnit)"--> @click="!props.readonly && circleStore.openSelfEvaluation(learningUnit)"
<!-- >--> >
<!-- <div--> <div
<!-- v-if="circleStore.calcSelfEvaluationStatus(learningUnit) === 'SUCCESS'"--> v-if="calcSelfEvaluationStatus(learningUnit) === 'SUCCESS'"
<!-- class="self-evaluation-success flex items-center gap-4 pb-6"--> class="self-evaluation-success flex items-center gap-4 pb-6"
<!-- >--> >
<!-- <it-icon-smiley-happy class="mr-4 h-8 w-8 flex-none" data-cy="success" />--> <it-icon-smiley-happy class="mr-4 h-8 w-8 flex-none" data-cy="success" />
<!-- <div>{{ $t("selfEvaluation.selfEvaluationYes") }}</div>--> <div>{{ $t("selfEvaluation.selfEvaluationYes") }}</div>
<!-- </div>--> </div>
<!-- <div--> <div
<!-- v-else-if="circleStore.calcSelfEvaluationStatus(learningUnit) === 'FAIL'"--> v-else-if="calcSelfEvaluationStatus(learningUnit) === 'FAIL'"
<!-- class="self-evaluation-fail flex items-center gap-4 pb-6"--> class="self-evaluation-fail flex items-center gap-4 pb-6"
<!-- >--> >
<!-- <it-icon-smiley-thinking class="mr-4 h-8 w-8 flex-none" data-cy="fail" />--> <it-icon-smiley-thinking class="mr-4 h-8 w-8 flex-none" data-cy="fail" />
<!-- <div>{{ $t("selfEvaluation.selfEvaluationNo") }}</div>--> <div>{{ $t("selfEvaluation.selfEvaluationNo") }}</div>
<!-- </div>--> </div>
<!-- <div v-else class="self-evaluation-unknown flex items-center gap-4 pb-6">--> <div v-else class="self-evaluation-unknown flex items-center gap-4 pb-6">
<!-- <it-icon-smiley-neutral class="mr-4 h-8 w-8 flex-none" data-cy="unknown" />--> <it-icon-smiley-neutral class="mr-4 h-8 w-8 flex-none" data-cy="unknown" />
<!-- <div>{{ $t("a.Selbsteinschätzung") }}</div>--> <div>{{ $t("a.Selbsteinschätzung") }}</div>
<!-- </div>--> </div>
<!-- </div>--> </div>
<hr v-if="!learningUnit.last" class="-mx-4 text-gray-500" /> <hr v-if="!learningUnit.last" class="-mx-4 text-gray-500" />
</li> </li>

View File

@ -2,14 +2,14 @@
import LearningPathCircle from "@/pages/learningPath/learningPathPage/LearningPathCircle.vue"; import LearningPathCircle from "@/pages/learningPath/learningPathPage/LearningPathCircle.vue";
import LearningPathContinueButton from "@/pages/learningPath/learningPathPage/LearningPathContinueButton.vue"; import LearningPathContinueButton from "@/pages/learningPath/learningPathPage/LearningPathContinueButton.vue";
import { calculateCircleSectorData } from "@/pages/learningPath/learningPathPage/utils"; import { calculateCircleSectorData } from "@/pages/learningPath/learningPathPage/utils";
import type { Circle } from "@/services/circle"; import type { OldCircle } from "@/services/oldCircle";
import type { LearningPath } from "@/services/learningPath"; import type { LearningPath } from "@/services/learningPath";
import type { Topic } from "@/types"; import type { Topic } from "@/types";
import { onMounted, ref } from "vue"; import { onMounted, ref } from "vue";
const props = defineProps<{ const props = defineProps<{
learningPath: LearningPath | undefined; learningPath: LearningPath | undefined;
circle: Circle; circle: OldCircle;
topic: Topic; topic: Topic;
isFirstCircle: boolean; isFirstCircle: boolean;
isLastCircle: boolean; isLastCircle: boolean;

View File

@ -2,13 +2,13 @@
import LearningPathCircle from "@/pages/learningPath/learningPathPage/LearningPathCircle.vue"; import LearningPathCircle from "@/pages/learningPath/learningPathPage/LearningPathCircle.vue";
import LearningPathContinueButton from "@/pages/learningPath/learningPathPage/LearningPathContinueButton.vue"; import LearningPathContinueButton from "@/pages/learningPath/learningPathPage/LearningPathContinueButton.vue";
import { calculateCircleSectorData } from "@/pages/learningPath/learningPathPage/utils"; import { calculateCircleSectorData } from "@/pages/learningPath/learningPathPage/utils";
import type { Circle } from "@/services/circle"; import type { OldCircle } from "@/services/oldCircle";
import type { LearningPath } from "@/services/learningPath"; import type { LearningPath } from "@/services/learningPath";
import { onMounted, ref } from "vue"; import { onMounted, ref } from "vue";
const props = defineProps<{ const props = defineProps<{
learningPath: LearningPath | undefined; learningPath: LearningPath | undefined;
circle: Circle; circle: OldCircle;
isCurrentCircle: boolean; isCurrentCircle: boolean;
}>(); }>();

View File

@ -1,6 +1,6 @@
<script setup lang="ts"> <script setup lang="ts">
import LearningPathCircleListTile from "@/pages/learningPath/learningPathPage/LearningPathCircleListTile.vue"; import LearningPathCircleListTile from "@/pages/learningPath/learningPathPage/LearningPathCircleListTile.vue";
import type { Circle } from "@/services/circle"; import type { OldCircle } from "@/services/oldCircle";
import type { LearningPath } from "@/services/learningPath"; import type { LearningPath } from "@/services/learningPath";
import { computed } from "vue"; import { computed } from "vue";
@ -10,7 +10,7 @@ const props = defineProps<{
const topics = computed(() => props.learningPath?.topics ?? []); const topics = computed(() => props.learningPath?.topics ?? []);
const isCurrentCircle = (circle: Circle) => const isCurrentCircle = (circle: OldCircle) =>
props.learningPath?.nextLearningContent?.parentCircle === circle; props.learningPath?.nextLearningContent?.parentCircle === circle;
</script> </script>

View File

@ -1,7 +1,7 @@
<script setup lang="ts"> <script setup lang="ts">
import LearningPathCircleColumn from "@/pages/learningPath/learningPathPage/LearningPathCircleColumn.vue"; import LearningPathCircleColumn from "@/pages/learningPath/learningPathPage/LearningPathCircleColumn.vue";
import LearningPathScrollButton from "@/pages/learningPath/learningPathPage/LearningPathScrollButton.vue"; import LearningPathScrollButton from "@/pages/learningPath/learningPathPage/LearningPathScrollButton.vue";
import type { Circle } from "@/services/circle"; import type { OldCircle } from "@/services/oldCircle";
import type { LearningPath } from "@/services/learningPath"; import type { LearningPath } from "@/services/learningPath";
import { useScroll } from "@vueuse/core"; import { useScroll } from "@vueuse/core";
import { ref } from "vue"; import { ref } from "vue";
@ -25,7 +25,7 @@ const isLastCircle = (topicIndex: number, circleIndex: number, numCircles: numbe
topicIndex === (props.learningPath?.topics ?? []).length - 1 && topicIndex === (props.learningPath?.topics ?? []).length - 1 &&
circleIndex === numCircles - 1; circleIndex === numCircles - 1;
const isCurrentCircle = (circle: Circle) => const isCurrentCircle = (circle: OldCircle) =>
props.learningPath?.nextLearningContent?.parentCircle === circle; props.learningPath?.nextLearningContent?.parentCircle === circle;
const scrollRight = () => scrollLearnPathDiagram(scrollIncrement); const scrollRight = () => scrollLearnPathDiagram(scrollIncrement);

View File

@ -2,9 +2,9 @@ import type {
CircleSectorData, CircleSectorData,
CircleSectorProgress, CircleSectorProgress,
} from "@/pages/learningPath/learningPathPage/LearningPathCircle.vue"; } from "@/pages/learningPath/learningPathPage/LearningPathCircle.vue";
import type { Circle } from "@/services/circle"; import type { OldCircle } from "@/services/oldCircle";
export function calculateCircleSectorData(circle: Circle): CircleSectorData[] { export function calculateCircleSectorData(circle: OldCircle): CircleSectorData[] {
return circle.learningSequences.map((ls) => { return circle.learningSequences.map((ls) => {
let progress: CircleSectorProgress = "none"; let progress: CircleSectorProgress = "none";
if (circle.allFinishedInLearningSequence(ls.translation_key)) { if (circle.allFinishedInLearningSequence(ls.translation_key)) {

View File

@ -1,6 +1,6 @@
import type { WagtailCircle } from "@/types"; import type { WagtailCircle } from "@/types";
import { describe, it } from "vitest"; import { describe, it } from "vitest";
import { Circle } from "../circle"; import { OldCircle } from "../oldCircle";
import data from "./learning_path_json.json"; import data from "./learning_path_json.json";
describe("Circle.parseJson", () => { describe("Circle.parseJson", () => {
@ -8,7 +8,7 @@ describe("Circle.parseJson", () => {
const cirleData = data.children.find( const cirleData = data.children.find(
(c) => c.slug === "test-lehrgang-lp-circle-fahrzeug" (c) => c.slug === "test-lehrgang-lp-circle-fahrzeug"
) as unknown as WagtailCircle; ) as unknown as WagtailCircle;
const circle = Circle.fromJson(cirleData, undefined); const circle = OldCircle.fromJson(cirleData, undefined);
expect(circle.learningSequences.length).toBe(3); expect(circle.learningSequences.length).toBe(3);
expect(circle.flatLearningContents.length).toBe(9); expect(circle.flatLearningContents.length).toBe(9);
}); });

View File

@ -1,268 +1,70 @@
import type { LearningPath } from "@/services/learningPath";
import type { import type {
CircleChild, CircleType,
CircleGoal, CourseCompletionStatus,
CircleJobSituation,
CourseCompletion,
LearningContent,
LearningContentInterface,
LearningSequence, LearningSequence,
LearningUnit, LearningUnit,
LearningUnitPerformanceCriteria,
WagtailCircle,
} from "@/types"; } from "@/types";
import groupBy from "lodash/groupBy";
import partition from "lodash/partition";
import values from "lodash/values";
import log from "loglevel";
function isLearningContentType(object: any): object is LearningContent { export function circleFlatChildren(circle: CircleType) {
return ( return [
object?.content_type === "learnpath.LearningContentAssignment" || ...circleFlatLearningContents(circle),
object?.content_type === "learnpath.LearningContentAttendanceCourse" || ...circleFlatLearningUnits(circle).flatMap((lu) => {
object?.content_type === "learnpath.LearningContentDocumentList" || return lu.performance_criteria;
object?.content_type === "learnpath.LearningContentFeedback" ||
object?.content_type === "learnpath.LearningContentLearningModule" ||
object?.content_type === "learnpath.LearningContentMediaLibrary" ||
object?.content_type === "learnpath.LearningContentPlaceholder" ||
object?.content_type === "learnpath.LearningContentRichText" ||
object?.content_type === "learnpath.LearningContentEdoniqTest" ||
object?.content_type === "learnpath.LearningContentVideo"
);
}
export function parseLearningSequences(
circle: Circle,
children: CircleChild[]
): LearningSequence[] {
let learningSequence: LearningSequence | undefined;
let learningUnit: LearningUnit | undefined;
let learningContent: LearningContent | undefined;
let previousLearningContent: LearningContent | undefined;
const result: LearningSequence[] = [];
children.forEach((child) => {
if (child.content_type === "learnpath.LearningSequence") {
if (learningSequence) {
if (learningUnit) {
learningUnit.last = true;
}
}
learningSequence = Object.assign(child, { learningUnits: [] });
result.push(learningSequence);
} else if (child.content_type === "learnpath.LearningUnit") {
if (!learningSequence) {
throw new Error("LearningUnit found before LearningSequence");
}
learningUnit = Object.assign(child, {
learningContents: [],
parentLearningSequence: learningSequence,
parentCircle: circle,
children: child.children.map((c) => {
c.parentLearningUnit = learningUnit;
c.parentLearningSequence = learningSequence;
return c;
}), }),
}); ];
learningSequence.learningUnits.push(learningUnit);
} else if (isLearningContentType(child)) {
if (!learningUnit) {
throw new Error(`LearningContent found before LearningUnit ${child.slug}`);
}
previousLearningContent = learningContent;
learningContent = Object.assign(child, {
parentCircle: circle,
parentLearningSequence: learningSequence,
parentLearningUnit: learningUnit,
previousLearningContent: previousLearningContent,
});
if (previousLearningContent) {
previousLearningContent.nextLearningContent = learningContent;
}
learningUnit.learningContents.push(child);
} else {
log.error("Unknown CircleChild found...", child);
throw new Error("Unknown CircleChild found...");
}
});
if (learningUnit) {
learningUnit.last = true;
} else {
throw new Error(
"Finished with LearningContent but there is no LearningSequence and LearningUnit"
);
}
// sum minutes
result.forEach((learningSequence) => {
learningSequence.minutes = 0;
learningSequence.learningUnits.forEach((learningUnit) => {
learningUnit.minutes = 0;
learningUnit.learningContents.forEach((learningContent) => {
learningUnit.minutes += learningContent.minutes;
});
learningSequence.minutes += learningUnit.minutes;
});
});
return result;
} }
export class Circle implements WagtailCircle { export function circleFlatLearningContents(circle: CircleType) {
readonly content_type = "learnpath.Circle"; return circleFlatLearningUnits(circle).flatMap((lu) => {
readonly learningSequences: LearningSequence[]; return lu.learning_contents;
});
}
nextCircle?: Circle; export function circleFlatLearningUnits(circle: CircleType) {
previousCircle?: Circle; return circle.learning_sequences.flatMap((ls) => {
return ls.learning_units;
});
}
constructor( export function learningSequenceFlatChildren(ls: LearningSequence) {
public readonly id: string, return [
public readonly slug: string, ...ls.learning_units.flatMap((lu) => {
public readonly title: string, return lu.learning_contents;
public readonly translation_key: string, }),
public readonly frontend_url: string, ...ls.learning_units.flatMap((lu) => {
public readonly description: string, return lu.performance_criteria;
public readonly children: CircleChild[], }),
public readonly goal_description: string, ];
public readonly goals: CircleGoal[], }
public readonly job_situation_description: string,
public readonly job_situations: CircleJobSituation[], export function someFinishedInLearningSequence(ls: LearningSequence) {
public readonly parentLearningPath?: LearningPath return learningSequenceFlatChildren(ls).some((lc) => {
return lc.completion_status === "SUCCESS";
});
}
export function allFinishedInLearningSequence(ls: LearningSequence) {
return learningSequenceFlatChildren(ls).every((lc) => {
return lc.completion_status === "SUCCESS";
});
}
export function calcSelfEvaluationStatus(
learningUnit: LearningUnit
): CourseCompletionStatus {
if (learningUnit.performance_criteria.length > 0) {
if (
learningUnit.performance_criteria.every((q) => q.completion_status === "SUCCESS")
) { ) {
this.learningSequences = parseLearningSequences(this, this.children); return "SUCCESS";
} }
if (
public static fromJson( learningUnit.performance_criteria.every(
wagtailCircle: WagtailCircle, (q) => q.completion_status === "FAIL" || q.completion_status === "SUCCESS"
learningPath?: LearningPath
): Circle {
// TODO add error checking when the data does not conform to the schema
return new Circle(
wagtailCircle.id,
wagtailCircle.slug,
wagtailCircle.title,
wagtailCircle.translation_key,
wagtailCircle.frontend_url,
wagtailCircle.description,
wagtailCircle.children,
wagtailCircle.goal_description,
wagtailCircle.goals,
wagtailCircle.job_situation_description,
wagtailCircle.job_situations,
learningPath
);
}
public get flatChildren(): (
| LearningContentInterface
| LearningUnitPerformanceCriteria
)[] {
const result: (LearningContentInterface | LearningUnitPerformanceCriteria)[] = [];
this.learningSequences.forEach((learningSequence) => {
learningSequence.learningUnits.forEach((learningUnit) => {
learningUnit.children.forEach((performanceCriteria) => {
result.push(performanceCriteria);
});
learningUnit.learningContents.forEach((learningContent) => {
result.push(learningContent);
});
});
});
return result;
}
public get flatLearningContents(): LearningContent[] {
const result: LearningContent[] = [];
this.learningSequences.forEach((learningSequence) => {
learningSequence.learningUnits.forEach((learningUnit) => {
learningUnit.learningContents.forEach((learningContent) => {
result.push(learningContent);
});
});
});
return result;
}
public get flatLearningUnits(): LearningUnit[] {
const result: LearningUnit[] = [];
this.learningSequences.forEach((learningSequence) => {
learningSequence.learningUnits.forEach((learningUnit) => {
result.push(learningUnit);
});
});
return result;
}
public someFinishedInLearningSequence(translationKey: string): boolean {
if (translationKey) {
return (
this.flatChildren.filter((lc) => {
return (
lc.completion_status === "SUCCESS" &&
lc.parentLearningSequence?.translation_key === translationKey
);
}).length > 0
);
}
return false;
}
public allFinishedInLearningSequence(translationKey: string): boolean {
if (translationKey) {
const [performanceCriteria, learningContents] = partition(
this.flatChildren.filter(
(lc) => lc.parentLearningSequence?.translation_key === translationKey
),
function (child) {
return child.content_type === "competence.PerformanceCriteria";
}
);
const groupedPerformanceCriteria = values(
groupBy(performanceCriteria, (pc) => pc.parentLearningUnit?.id)
);
return (
learningContents.every((lc) => lc.completion_status === "SUCCESS") &&
(groupedPerformanceCriteria.length === 0 ||
groupedPerformanceCriteria.every((group) =>
group.every(
(pc) =>
pc.completion_status === "SUCCESS" || pc.completion_status === "FAIL"
) )
)) ) {
); return "FAIL";
}
return false;
}
public isComplete(): boolean {
return this.learningSequences.every((ls) =>
this.allFinishedInLearningSequence(ls.translation_key)
);
}
public parseCompletionData(completionData: CourseCompletion[]) {
this.flatChildren.forEach((page) => {
const pageIndex = completionData.findIndex((e) => {
return e.page_id === page.id;
});
if (pageIndex >= 0) {
page.completion_status = completionData[pageIndex].completion_status;
} else {
page.completion_status = "UNKNOWN";
}
});
if (this.parentLearningPath) {
this.parentLearningPath.calcNextLearningContent(completionData);
} }
} }
return "UNKNOWN";
} }

View File

@ -1,6 +1,6 @@
import orderBy from "lodash/orderBy"; import orderBy from "lodash/orderBy";
import { Circle } from "@/services/circle"; import { OldCircle } from "@/services/oldCircle";
import { useCourseSessionsStore } from "@/stores/courseSessions"; import { useCourseSessionsStore } from "@/stores/courseSessions";
import { useLearningPathStore } from "@/stores/learningPath"; import { useLearningPathStore } from "@/stores/learningPath";
import type { import type {
@ -12,6 +12,8 @@ import type {
WagtailLearningPath, WagtailLearningPath,
} from "@/types"; } from "@/types";
// FIXME: remove
export interface ContinueData { export interface ContinueData {
url: string; url: string;
has_no_progress: boolean; has_no_progress: boolean;
@ -35,7 +37,7 @@ function getLastCompleted(courseSlug: string, completionData: CourseCompletion[]
export class LearningPath implements WagtailLearningPath { export class LearningPath implements WagtailLearningPath {
readonly content_type = "learnpath.LearningPath"; readonly content_type = "learnpath.LearningPath";
public topics: Topic[]; public topics: Topic[];
public circles: Circle[]; public circles: OldCircle[];
public nextLearningContent?: LearningContentInterface; public nextLearningContent?: LearningContentInterface;
public static fromJson( public static fromJson(
@ -81,7 +83,7 @@ export class LearningPath implements WagtailLearningPath {
topic = Object.assign(page, { circles: [] }); topic = Object.assign(page, { circles: [] });
} }
if (page.content_type === "learnpath.Circle") { if (page.content_type === "learnpath.Circle") {
const circle = Circle.fromJson(page, this); const circle = OldCircle.fromJson(page, this);
if (completionData && completionData.length > 0) { if (completionData && completionData.length > 0) {
circle.parseCompletionData(completionData); circle.parseCompletionData(completionData);
} }

View File

@ -0,0 +1,264 @@
import type { LearningPath } from "@/services/learningPath";
import type {
CircleChild,
CircleGoal,
CircleJobSituation,
CourseCompletion,
LearningContent,
LearningContentInterface,
LearningSequence,
LearningUnit,
LearningUnitPerformanceCriteria,
WagtailCircle,
} from "@/types";
import groupBy from "lodash/groupBy";
import partition from "lodash/partition";
import values from "lodash/values";
import log from "loglevel";
// FIXME: remove
function isLearningContentType(object: any): object is LearningContent {
return (
object?.content_type === "learnpath.LearningContentAssignment" ||
object?.content_type === "learnpath.LearningContentAttendanceCourse" ||
object?.content_type === "learnpath.LearningContentDocumentList" ||
object?.content_type === "learnpath.LearningContentFeedback" ||
object?.content_type === "learnpath.LearningContentLearningModule" ||
object?.content_type === "learnpath.LearningContentMediaLibrary" ||
object?.content_type === "learnpath.LearningContentPlaceholder" ||
object?.content_type === "learnpath.LearningContentRichText" ||
object?.content_type === "learnpath.LearningContentEdoniqTest" ||
object?.content_type === "learnpath.LearningContentVideo"
);
}
export function parseLearningSequences(
circle: OldCircle,
children: CircleChild[]
): LearningSequence[] {
let learningSequence: LearningSequence | undefined;
let learningUnit: LearningUnit | undefined;
let learningContent: LearningContent | undefined;
let previousLearningContent: LearningContent | undefined;
const result: LearningSequence[] = [];
children.forEach((child) => {
if (child.content_type === "learnpath.LearningSequence") {
if (learningSequence) {
if (learningUnit) {
learningUnit.last = true;
}
}
learningSequence = Object.assign(child, { learningUnits: [] });
result.push(learningSequence);
} else if (child.content_type === "learnpath.LearningUnit") {
if (!learningSequence) {
throw new Error("LearningUnit found before LearningSequence");
}
learningUnit = Object.assign(child, {
learningContents: [],
parentLearningSequence: learningSequence,
parentCircle: circle,
children: child.children.map((c) => {
c.parentLearningUnit = learningUnit;
c.parentLearningSequence = learningSequence;
return c;
}),
});
learningSequence.learningUnits.push(learningUnit);
} else if (isLearningContentType(child)) {
if (!learningUnit) {
throw new Error(`LearningContent found before LearningUnit ${child.slug}`);
}
previousLearningContent = learningContent;
learningContent = Object.assign(child, {
parentCircle: circle,
parentLearningSequence: learningSequence,
parentLearningUnit: learningUnit,
previousLearningContent: previousLearningContent,
});
if (previousLearningContent) {
previousLearningContent.nextLearningContent = learningContent;
}
learningUnit.learningContents.push(child);
} else {
log.error("Unknown CircleChild found...", child);
throw new Error("Unknown CircleChild found...");
}
});
if (learningUnit) {
learningUnit.last = true;
} else {
throw new Error(
"Finished with LearningContent but there is no LearningSequence and LearningUnit"
);
}
// sum minutes
result.forEach((learningSequence) => {
learningSequence.minutes = 0;
learningSequence.learningUnits.forEach((learningUnit) => {
learningUnit.minutes = 0;
learningUnit.learningContents.forEach((learningContent) => {
learningUnit.minutes += learningContent.minutes;
});
learningSequence.minutes += learningUnit.minutes;
});
});
return result;
}
export class OldCircle implements WagtailCircle {
readonly content_type = "learnpath.Circle";
readonly learningSequences: LearningSequence[];
nextCircle?: OldCircle;
previousCircle?: OldCircle;
constructor(
public readonly id: string,
public readonly slug: string,
public readonly title: string,
public readonly translation_key: string,
public readonly frontend_url: string,
public readonly description: string,
public readonly children: CircleChild[],
public readonly goal_description: string,
public readonly goals: CircleGoal[],
public readonly job_situation_description: string,
public readonly job_situations: CircleJobSituation[],
public readonly parentLearningPath?: LearningPath
) {
this.learningSequences = parseLearningSequences(this, this.children);
}
public static fromJson(
wagtailCircle: WagtailCircle,
learningPath?: LearningPath
): OldCircle {
// TODO add error checking when the data does not conform to the schema
return new OldCircle(
wagtailCircle.id,
wagtailCircle.slug,
wagtailCircle.title,
wagtailCircle.translation_key,
wagtailCircle.frontend_url,
wagtailCircle.description,
wagtailCircle.children,
wagtailCircle.goal_description,
wagtailCircle.goals,
wagtailCircle.job_situation_description,
wagtailCircle.job_situations,
learningPath
);
}
public get flatChildren(): (
| LearningContentInterface
| LearningUnitPerformanceCriteria
)[] {
const result: (LearningContentInterface | LearningUnitPerformanceCriteria)[] = [];
this.learningSequences.forEach((learningSequence) => {
learningSequence.learningUnits.forEach((learningUnit) => {
learningUnit.children.forEach((performanceCriteria) => {
result.push(performanceCriteria);
});
learningUnit.learningContents.forEach((learningContent) => {
result.push(learningContent);
});
});
});
return result;
}
public get flatLearningContents(): LearningContent[] {
const result: LearningContent[] = [];
this.learningSequences.forEach((learningSequence) => {
learningSequence.learningUnits.forEach((learningUnit) => {
learningUnit.learningContents.forEach((learningContent) => {
result.push(learningContent);
});
});
});
return result;
}
public get flatLearningUnits(): LearningUnit[] {
const result: LearningUnit[] = [];
this.learningSequences.forEach((learningSequence) => {
learningSequence.learningUnits.forEach((learningUnit) => {
result.push(learningUnit);
});
});
return result;
}
public someFinishedInLearningSequence(translationKey: string): boolean {
if (translationKey) {
return (
this.flatChildren.filter((lc) => {
return (
lc.completion_status === "SUCCESS" &&
lc.parentLearningSequence?.translation_key === translationKey
);
}).length > 0
);
}
return false;
}
public allFinishedInLearningSequence(translationKey: string): boolean {
if (translationKey) {
const [performanceCriteria, learningContents] = partition(
this.flatChildren.filter(
(lc) => lc.parentLearningSequence?.translation_key === translationKey
),
function (child) {
return child.content_type === "competence.PerformanceCriteria";
}
);
const groupedPerformanceCriteria = values(
groupBy(performanceCriteria, (pc) => pc.parentLearningUnit?.id)
);
return (
learningContents.every((lc) => lc.completion_status === "SUCCESS") &&
(groupedPerformanceCriteria.length === 0 ||
groupedPerformanceCriteria.every((group) =>
group.every(
(pc) =>
pc.completion_status === "SUCCESS" || pc.completion_status === "FAIL"
)
))
);
}
return false;
}
public parseCompletionData(completionData: CourseCompletion[]) {
this.flatChildren.forEach((page) => {
const pageIndex = completionData.findIndex((e) => {
return e.page_id === page.id;
});
if (pageIndex >= 0) {
page.completion_status = completionData[pageIndex].completion_status;
} else {
page.completion_status = "UNKNOWN";
}
});
if (this.parentLearningPath) {
this.parentLearningPath.calcNextLearningContent(completionData);
}
}
}

View File

@ -1,4 +1,4 @@
import type { Circle } from "@/services/circle"; import type { OldCircle } from "@/services/oldCircle";
import { useCompletionStore } from "@/stores/completion"; import { useCompletionStore } from "@/stores/completion";
import { useLearningPathStore } from "@/stores/learningPath"; import { useLearningPathStore } from "@/stores/learningPath";
import { useUserStore } from "@/stores/user"; import { useUserStore } from "@/stores/user";
@ -14,7 +14,7 @@ import { defineStore } from "pinia";
import type { RouteLocationNormalized } from "vue-router"; import type { RouteLocationNormalized } from "vue-router";
export type CircleStoreState = { export type CircleStoreState = {
circle: Circle | undefined; circle: OldCircle | undefined;
page: "INDEX" | "OVERVIEW"; page: "INDEX" | "OVERVIEW";
}; };
@ -42,7 +42,7 @@ export const useCircleStore = defineStore({
courseSlug: string, courseSlug: string,
circleSlug: string, circleSlug: string,
userId: string | undefined = undefined userId: string | undefined = undefined
): Promise<Circle> { ): Promise<OldCircle> {
if (!userId) { if (!userId) {
const userStore = useUserStore(); const userStore = useUserStore();
userId = userStore.id; userId = userStore.id;
@ -158,21 +158,6 @@ export const useCircleStore = defineStore({
}); });
} }
}, },
calcSelfEvaluationStatus(learningUnit: LearningUnit): CourseCompletionStatus {
if (learningUnit.children.length > 0) {
if (learningUnit.children.every((q) => q.completion_status === "SUCCESS")) {
return "SUCCESS";
}
if (
learningUnit.children.every(
(q) => q.completion_status === "FAIL" || q.completion_status === "SUCCESS"
)
) {
return "FAIL";
}
}
return "UNKNOWN";
},
continueFromLearningContent( continueFromLearningContent(
currentLearningContent: LearningContentInterface, currentLearningContent: LearningContentInterface,
returnRoute?: RouteLocationNormalized returnRoute?: RouteLocationNormalized

View File

@ -1,7 +1,11 @@
import { bustItGetCache, itGetCached, itPost } from "@/fetchHelpers"; import { bustItGetCache, itGetCached, itPost } from "@/fetchHelpers";
import { useCourseSessionsStore } from "@/stores/courseSessions"; import { useCourseSessionsStore } from "@/stores/courseSessions";
import { useUserStore } from "@/stores/user"; import { useUserStore } from "@/stores/user";
import type { BaseCourseWagtailPage, CourseCompletion } from "@/types"; import type {
CourseCompletion,
LearningContentWithCompletion,
LearningUnitPerformanceCriteria,
} from "@/types";
import { defineStore } from "pinia"; import { defineStore } from "pinia";
export const useCompletionStore = defineStore({ export const useCompletionStore = defineStore({
@ -29,7 +33,7 @@ export const useCompletionStore = defineStore({
return userCompletionData || []; return userCompletionData || [];
}, },
async markPage( async markPage(
page: BaseCourseWagtailPage, page: LearningContentWithCompletion | LearningUnitPerformanceCriteria,
userId: string | undefined = undefined, userId: string | undefined = undefined,
courseSessionId: string | undefined = undefined courseSessionId: string | undefined = undefined
) { ) {

View File

@ -10,6 +10,7 @@ import log from "loglevel";
import { defineStore } from "pinia"; import { defineStore } from "pinia";
import { computed, reactive } from "vue"; import { computed, reactive } from "vue";
// FIXME: remove
export type LearningPathStoreState = { export type LearningPathStoreState = {
learningPaths: Map<string, LearningPath>; learningPaths: Map<string, LearningPath>;
page: "INDEX" | "OVERVIEW"; page: "INDEX" | "OVERVIEW";

View File

@ -18,6 +18,7 @@ import type {
LearningPathObjectType, LearningPathObjectType,
LearningSequenceObjectType, LearningSequenceObjectType,
LearningUnitObjectType, LearningUnitObjectType,
PerformanceCriteriaObjectType,
TopicObjectType, TopicObjectType,
} from "@/gql/graphql"; } from "@/gql/graphql";
import type { Component } from "vue"; import type { Component } from "vue";
@ -26,6 +27,11 @@ export type LoginMethod = "local" | "sso";
export type CourseCompletionStatus = "UNKNOWN" | "FAIL" | "SUCCESS"; export type CourseCompletionStatus = "UNKNOWN" | "FAIL" | "SUCCESS";
export type Completable = {
completion_status?: CourseCompletionStatus;
completion_status_updated_at?: string;
};
export interface BaseCourseWagtailPage { export interface BaseCourseWagtailPage {
readonly id: string; readonly id: string;
readonly title: string; readonly title: string;
@ -33,8 +39,6 @@ export interface BaseCourseWagtailPage {
readonly content_type: string; readonly content_type: string;
readonly translation_key: string; readonly translation_key: string;
readonly frontend_url: string; readonly frontend_url: string;
completion_status?: CourseCompletionStatus;
completion_status_updated_at?: string;
} }
export interface CircleLight { export interface CircleLight {
@ -97,14 +101,17 @@ export type LearningContent =
| LearningContentRichText | LearningContentRichText
| LearningContentVideo; | LearningContentVideo;
export type LearningContentWithCompletion = LearningContent & Completable;
export type LearningContentContentType = LearningContent["content_type"]; export type LearningContentContentType = LearningContent["content_type"];
export type LearningUnit = Omit< export type LearningUnit = Omit<
LearningUnitObjectType, LearningUnitObjectType,
"content_type" | "learning_contents" "content_type" | "learning_contents" | "performance_criteria"
> & { > & {
content_type: "learnpath.LearningUnit"; content_type: "learnpath.LearningUnit";
learning_contents: LearningContent[]; learning_contents: LearningContentWithCompletion[];
performance_criteria: LearningUnitPerformanceCriteria[];
}; };
export type LearningSequence = Omit< export type LearningSequence = Omit<
@ -136,12 +143,13 @@ export type LearningPathType = Omit<
topics: TopicType[]; topics: TopicType[];
}; };
export interface LearningUnitPerformanceCriteria extends BaseCourseWagtailPage { export type LearningUnitPerformanceCriteria = Omit<
PerformanceCriteriaObjectType,
"content_type"
> &
Completable & {
readonly content_type: "competence.PerformanceCriteria"; readonly content_type: "competence.PerformanceCriteria";
readonly competence_id: string; };
parentLearningSequence?: LearningSequence;
parentLearningUnit?: LearningUnit;
}
export interface CourseCompletion { export interface CourseCompletion {
readonly id: string; readonly id: string;

View File

@ -6,6 +6,7 @@ from vbv_lernwelt.assignment.graphql.types import AssignmentObjectType
from vbv_lernwelt.competence.models import ( from vbv_lernwelt.competence.models import (
CompetenceCertificate, CompetenceCertificate,
CompetenceCertificateList, CompetenceCertificateList,
PerformanceCriteria,
) )
from vbv_lernwelt.course.graphql.interfaces import CoursePageInterface from vbv_lernwelt.course.graphql.interfaces import CoursePageInterface
@ -34,3 +35,10 @@ class CompetenceCertificateListObjectType(DjangoObjectType):
def resolve_competence_certificates(self, info): def resolve_competence_certificates(self, info):
return CompetenceCertificate.objects.child_of(self) return CompetenceCertificate.objects.child_of(self)
class PerformanceCriteriaObjectType(DjangoObjectType):
class Meta:
model = PerformanceCriteria
interfaces = (CoursePageInterface,)
fields = ["competence_id"]

View File

@ -141,6 +141,9 @@ class PerformanceCriteria(CourseBasePage):
FieldPanel("learning_unit"), FieldPanel("learning_unit"),
] ]
def get_frontend_url(self):
return ""
def save(self, clean=True, user=None, log_action=False, **kwargs): def save(self, clean=True, user=None, log_action=False, **kwargs):
profile_parent = ( profile_parent = (
self.get_ancestors().exact_type(ActionCompetenceListPage).last() self.get_ancestors().exact_type(ActionCompetenceListPage).last()

View File

@ -3,7 +3,7 @@ from rest_framework.test import APITestCase
from vbv_lernwelt.core.create_default_users import create_default_users from vbv_lernwelt.core.create_default_users import create_default_users
from vbv_lernwelt.core.models import User from vbv_lernwelt.core.models import User
from vbv_lernwelt.course.creators.test_course import create_test_course from vbv_lernwelt.course.creators.test_course import create_test_course
from vbv_lernwelt.course.models import CourseSession, CourseSessionUser from vbv_lernwelt.course.models import CourseSession
from vbv_lernwelt.feedback.factories import FeedbackResponseFactory from vbv_lernwelt.feedback.factories import FeedbackResponseFactory
from vbv_lernwelt.feedback.models import FeedbackResponse from vbv_lernwelt.feedback.models import FeedbackResponse
from vbv_lernwelt.learnpath.models import Circle from vbv_lernwelt.learnpath.models import Circle

View File

@ -27,8 +27,9 @@ logger = structlog.get_logger(__name__)
class LearningContentInterface(CoursePageInterface): class LearningContentInterface(CoursePageInterface):
minutes = graphene.Int() minutes = graphene.Int()
description = graphene.String() description = graphene.String(required=True)
content = graphene.String() content_url = graphene.String(required=True)
can_user_self_toggle_course_completion = graphene.Boolean(required=True)
circle = graphene.Field( circle = graphene.Field(
"vbv_lernwelt.learnpath.graphql.types.CircleObjectType", required=True "vbv_lernwelt.learnpath.graphql.types.CircleObjectType", required=True
) )
@ -186,6 +187,12 @@ class LearningUnitObjectType(DjangoObjectType):
learning_contents = graphene.List( learning_contents = graphene.List(
graphene.NonNull(LearningContentInterface), required=True graphene.NonNull(LearningContentInterface), required=True
) )
performance_criteria = graphene.List(
graphene.NonNull(
"vbv_lernwelt.competence.graphql.types.PerformanceCriteriaObjectType"
),
required=True,
)
evaluate_url = graphene.String(required=True) evaluate_url = graphene.String(required=True)
class Meta: class Meta:
@ -197,6 +204,10 @@ class LearningUnitObjectType(DjangoObjectType):
def resolve_evaluate_url(root: LearningUnit, info, **kwargs): def resolve_evaluate_url(root: LearningUnit, info, **kwargs):
return root.get_evaluate_url() return root.get_evaluate_url()
@staticmethod
def resolve_performance_criteria(root: LearningUnit, info, **kwargs):
return root.performancecriteria_set.all()
@staticmethod @staticmethod
def resolve_learning_contents(root: LearningUnit, info, **kwargs): def resolve_learning_contents(root: LearningUnit, info, **kwargs):
siblings = None siblings = None