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 #center>
<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>
</template>
@ -34,7 +34,7 @@ import ItRow from "@/components/ui/ItRow.vue";
import { itGet } from "@/fetchHelpers";
import { onMounted, ref, watch } from "vue";
import type { Circle } from "@/services/circle";
import type { OldCircle } from "@/services/oldCircle";
interface FeedbackSummary {
circle_id: string;
@ -42,12 +42,12 @@ interface FeedbackSummary {
}
interface FeedbackDisplaySummary extends FeedbackSummary {
circle: Circle;
circle: OldCircle;
}
function makeSummary(
feedbackData: FeedbackSummary[],
circles: Circle[],
circles: OldCircle[],
selectedCircles: string[]
) {
const summary: FeedbackDisplaySummary[] = circles
@ -64,7 +64,7 @@ function makeSummary(
const props = defineProps<{
selctedCircles: string[];
circles: Circle[];
circles: OldCircle[];
courseSessionId: string;
url: string;
}>();

View File

@ -1,7 +1,17 @@
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 { 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 log from "loglevel";
import type { ComputedRef } from "vue";
@ -150,3 +160,71 @@ export function useLearningPathQuery(courseSlug: string) {
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 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 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,
};
@ -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.
*/
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.
*/

File diff suppressed because one or more lines are too long

View File

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

View File

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

View File

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

View File

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

View File

@ -147,7 +147,9 @@ const getIconName = (lc: LearningContent) => {
</div>
<div class="flex flex-col">
<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>
<AssignmentSubmissionProgress

View File

@ -1,6 +1,5 @@
<script setup lang="ts">
import { showIcon } from "@/pages/learningPath/circlePage/learningSequenceUtils";
import { useCircleStore } from "@/stores/circle";
import type { DefaultArcObject } from "d3";
import * as d3 from "d3";
import pick from "lodash/pick";
@ -9,26 +8,23 @@ import { computed, onMounted } from "vue";
// @ts-ignore
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) {
if (circleStore.circle) {
return circleStore.circle.someFinishedInLearningSequence(
learningSequence.translation_key
);
}
return false;
}
const props = defineProps<{
circle: CircleType;
}>();
function allFinished(learningSequence: LearningSequence) {
if (circleStore.circle) {
return circleStore.circle.allFinishedInLearningSequence(
learningSequence.translation_key
);
return allFinishedInLearningSequence(learningSequence);
}
return false;
function someFinished(learningSequence: LearningSequence) {
return someFinishedInLearningSequence(learningSequence);
}
onMounted(async () => {
@ -48,14 +44,14 @@ interface CirclePie extends d3.PieArcDatum<number> {
}
const pieData = computed(() => {
const circle = circleStore.circle;
const circle = props.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 angles = pieGenerator(pieWeights);
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
angle.startAngle += Math.PI;

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,6 +1,6 @@
<script setup lang="ts">
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 { computed } from "vue";
@ -10,7 +10,7 @@ const props = defineProps<{
const topics = computed(() => props.learningPath?.topics ?? []);
const isCurrentCircle = (circle: Circle) =>
const isCurrentCircle = (circle: OldCircle) =>
props.learningPath?.nextLearningContent?.parentCircle === circle;
</script>

View File

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

View File

@ -2,9 +2,9 @@ import type {
CircleSectorData,
CircleSectorProgress,
} 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) => {
let progress: CircleSectorProgress = "none";
if (circle.allFinishedInLearningSequence(ls.translation_key)) {

View File

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

View File

@ -1,268 +1,70 @@
import type { LearningPath } from "@/services/learningPath";
import type {
CircleChild,
CircleGoal,
CircleJobSituation,
CourseCompletion,
LearningContent,
LearningContentInterface,
CircleType,
CourseCompletionStatus,
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";
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: 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;
export function circleFlatChildren(circle: CircleType) {
return [
...circleFlatLearningContents(circle),
...circleFlatLearningUnits(circle).flatMap((lu) => {
return lu.performance_criteria;
}),
});
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...");
}
export function circleFlatLearningContents(circle: CircleType) {
return circleFlatLearningUnits(circle).flatMap((lu) => {
return lu.learning_contents;
});
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;
export function circleFlatLearningUnits(circle: CircleType) {
return circle.learning_sequences.flatMap((ls) => {
return ls.learning_units;
});
learningSequence.minutes += learningUnit.minutes;
});
});
return result;
}
export class Circle implements WagtailCircle {
readonly content_type = "learnpath.Circle";
readonly learningSequences: LearningSequence[];
export function learningSequenceFlatChildren(ls: LearningSequence) {
return [
...ls.learning_units.flatMap((lu) => {
return lu.learning_contents;
}),
...ls.learning_units.flatMap((lu) => {
return lu.performance_criteria;
}),
];
}
nextCircle?: Circle;
previousCircle?: Circle;
export function someFinishedInLearningSequence(ls: LearningSequence) {
return learningSequenceFlatChildren(ls).some((lc) => {
return lc.completion_status === "SUCCESS";
});
}
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
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";
}
public static fromJson(
wagtailCircle: WagtailCircle,
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"
if (
learningUnit.performance_criteria.every(
(q) => q.completion_status === "FAIL" || q.completion_status === "SUCCESS"
)
))
);
}
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 "FAIL";
}
}
return "UNKNOWN";
}

View File

@ -1,6 +1,6 @@
import orderBy from "lodash/orderBy";
import { Circle } from "@/services/circle";
import { OldCircle } from "@/services/oldCircle";
import { useCourseSessionsStore } from "@/stores/courseSessions";
import { useLearningPathStore } from "@/stores/learningPath";
import type {
@ -12,6 +12,8 @@ import type {
WagtailLearningPath,
} from "@/types";
// FIXME: remove
export interface ContinueData {
url: string;
has_no_progress: boolean;
@ -35,7 +37,7 @@ function getLastCompleted(courseSlug: string, completionData: CourseCompletion[]
export class LearningPath implements WagtailLearningPath {
readonly content_type = "learnpath.LearningPath";
public topics: Topic[];
public circles: Circle[];
public circles: OldCircle[];
public nextLearningContent?: LearningContentInterface;
public static fromJson(
@ -81,7 +83,7 @@ export class LearningPath implements WagtailLearningPath {
topic = Object.assign(page, { circles: [] });
}
if (page.content_type === "learnpath.Circle") {
const circle = Circle.fromJson(page, this);
const circle = OldCircle.fromJson(page, this);
if (completionData && completionData.length > 0) {
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 { useLearningPathStore } from "@/stores/learningPath";
import { useUserStore } from "@/stores/user";
@ -14,7 +14,7 @@ import { defineStore } from "pinia";
import type { RouteLocationNormalized } from "vue-router";
export type CircleStoreState = {
circle: Circle | undefined;
circle: OldCircle | undefined;
page: "INDEX" | "OVERVIEW";
};
@ -42,7 +42,7 @@ export const useCircleStore = defineStore({
courseSlug: string,
circleSlug: string,
userId: string | undefined = undefined
): Promise<Circle> {
): Promise<OldCircle> {
if (!userId) {
const userStore = useUserStore();
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(
currentLearningContent: LearningContentInterface,
returnRoute?: RouteLocationNormalized

View File

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

View File

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

View File

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

View File

@ -6,6 +6,7 @@ from vbv_lernwelt.assignment.graphql.types import AssignmentObjectType
from vbv_lernwelt.competence.models import (
CompetenceCertificate,
CompetenceCertificateList,
PerformanceCriteria,
)
from vbv_lernwelt.course.graphql.interfaces import CoursePageInterface
@ -34,3 +35,10 @@ class CompetenceCertificateListObjectType(DjangoObjectType):
def resolve_competence_certificates(self, info):
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"),
]
def get_frontend_url(self):
return ""
def save(self, clean=True, user=None, log_action=False, **kwargs):
profile_parent = (
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.models import User
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.models import FeedbackResponse
from vbv_lernwelt.learnpath.models import Circle

View File

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