Merged in feature/VBV-465-learningpath-graphql-client-composable (pull request #220)

Feature/VBV-465 learningpath graphql client composable
This commit is contained in:
Daniel Egger 2023-10-15 20:40:54 +00:00
commit 7cbe15ce50
95 changed files with 2554 additions and 3502 deletions

2
.gitignore vendored
View File

@ -284,4 +284,4 @@ git-crypt-encrypted-files-check.txt
/server/vbv_lernwelt/static/storybook
/server/vbv_lernwelt/templates/vue/index.html
/server/vbv_lernwelt/media
/client/src/gql/minifiedSchema.json
/client/src/gql/dist/minifiedSchema.json

View File

@ -77,6 +77,7 @@ js-tests: &js-tests
- cd client
- pwd
- npm install
- npm run codegen
- npm test
js-linting: &js-linting

View File

@ -11,7 +11,13 @@ const config: CodegenConfig = {
"./src/gql/": {
preset: "client",
config: {
// avoidOptionals: true,
useTypeImports: true,
scalars: {
ID: "string",
UUID: "string",
DateTime: "string",
},
},
plugins: [],
},

View File

@ -8,4 +8,4 @@ const schema = readFileSync("./src/gql/schema.graphql", "utf-8");
const minifiedSchema = minifyIntrospectionQuery(getIntrospectedSchema(schema));
// Write the minified schema to a new file
writeFileSync("./src/gql/minifiedSchema.json", JSON.stringify(minifiedSchema));
writeFileSync("./src/gql/dist/minifiedSchema.json", JSON.stringify(minifiedSchema));

View File

@ -16,7 +16,8 @@
"storybook": "storybook dev -p 6006",
"tailwind": "tailwindcss -i tailwind.css -o ../server/vbv_lernwelt/static/css/tailwind.css --watch",
"test": "vitest run",
"typecheck": "npm run codegen && vue-tsc --noEmit -p tsconfig.app.json --composite false"
"typecheck": "npm run codegen && vue-tsc --noEmit -p tsconfig.app.json --composite false",
"typecheck-only": "vue-tsc --noEmit -p tsconfig.app.json --composite false"
},
"dependencies": {
"@headlessui/tailwindcss": "^0.1.3",

View File

@ -3,7 +3,7 @@ import dayjs from "dayjs";
import LocalizedFormat from "dayjs/plugin/localizedFormat";
import i18next from "i18next";
export const formatDueDate = (start: string, end?: string) => {
export const formatDueDate = (start: string, end?: string | null) => {
dayjs.extend(LocalizedFormat);
const startDayjs = dayjs(start);
const startDateString = getDateString(startDayjs);

View File

@ -1,101 +0,0 @@
<template>
<div class="mb-4 bg-white px-6 py-5">
<h3 class="heading-3 mb-4 flex items-center gap-2">
<it-icon-feedback-large class="h-16 w-16"></it-icon-feedback-large>
<div>{{ $t("general.feedback_other") }}</div>
</h3>
<ol v-if="feedbackSummary.length > 0">
<ItRow v-for="feedbacks in feedbackSummary" :key="feedbacks.circle_id">
<template #firstRow>
<span class="text-bold">{{ $t("feedback.circleFeedback") }}</span>
</template>
<template #center>
<div class="flex w-full justify-between">
<div>Circle: {{ feedbacks.circle.title }}</div>
<div>{{ $t("feedback.sentByUsers", { count: feedbacks.count }) }}</div>
</div>
</template>
<template #link>
<router-link
:to="`${url}/cockpit/feedback/${feedbacks.circle_id}`"
class="w-full text-right underline"
>
{{ $t("feedback.showDetails") }}
</router-link>
</template>
</ItRow>
</ol>
<p v-else>{{ $t("feedback.noFeedbacks") }}</p>
</div>
</template>
<script setup lang="ts">
import ItRow from "@/components/ui/ItRow.vue";
import { itGet } from "@/fetchHelpers";
import { onMounted, ref, watch } from "vue";
import type { Circle } from "@/services/circle";
interface FeedbackSummary {
circle_id: string;
count: number;
}
interface FeedbackDisplaySummary extends FeedbackSummary {
circle: Circle;
}
function makeSummary(
feedbackData: FeedbackSummary[],
circles: Circle[],
selectedCircles: string[]
) {
const summary: FeedbackDisplaySummary[] = circles
.filter((circle) => selectedCircles.includes(circle.translation_key))
.reduce((acc: FeedbackDisplaySummary[], circle) => {
const circleFeedbacks = feedbackData
.filter((data) => data.circle_id === circle.id)
.map((data) => Object.assign({}, data, { circle }));
return acc.concat(circleFeedbacks);
}, []);
return summary;
}
const props = defineProps<{
selctedCircles: string[];
circles: Circle[];
courseSessionId: string;
url: string;
}>();
const feedbackSummary = ref<FeedbackDisplaySummary[]>([]);
let feedbackData: FeedbackSummary[] = [];
onMounted(async () => {
feedbackData = await itGet(`/api/core/feedback/${props.courseSessionId}/summary`);
feedbackSummary.value = makeSummary(
feedbackData,
props.circles,
props.selctedCircles
);
});
watch(
() => props,
() => {
if (
feedbackData.length > 0 &&
props.circles.length > 0 &&
props.selctedCircles.length > 0
) {
feedbackSummary.value = makeSummary(
feedbackData,
props.circles,
props.selctedCircles
);
}
},
{ deep: true }
);
</script>

View File

@ -1,30 +1,38 @@
<script setup lang="ts">
import LearningPathCircle from "@/pages/learningPath/learningPathPage/LearningPathCircle.vue";
import { calculateCircleSectorData } from "@/pages/learningPath/learningPathPage/utils";
import type { LearningPath } from "@/services/learningPath";
import { computed } from "vue";
import { useCourseDataWithCompletion } from "@/composables";
export type DiagramType = "horizontal" | "horizontalSmall" | "singleSmall";
export interface Props {
diagramType?: DiagramType;
learningPath: LearningPath;
// set to undefined (default) to show all circles
showCircleSlugs?: string[];
courseSlug: string;
courseSessionId: string;
userId?: string;
diagramType?: DiagramType;
}
const props = withDefaults(defineProps<Props>(), {
diagramType: "horizontal",
showCircleSlugs: undefined,
userId: undefined,
});
const lpQueryResult = useCourseDataWithCompletion(
props.courseSlug,
props.userId,
props.courseSessionId
);
const circles = computed(() => {
if (props.showCircleSlugs?.length) {
return props.learningPath.circles.filter(
return (lpQueryResult.circles.value ?? []).filter(
(c) => props.showCircleSlugs?.includes(c.slug) ?? true
);
}
return props.learningPath.circles;
return lpQueryResult.circles.value ?? [];
});
const wrapperClasses = computed(() => {

View File

@ -1,33 +0,0 @@
<script setup lang="ts">
import { useLearningPathStore } from "@/stores/learningPath";
import * as log from "loglevel";
import LearningPathDiagram from "@/components/learningPath/LearningPathDiagram.vue";
import type { LearningPath } from "@/services/learningPath";
import { ref } from "vue";
log.debug("LearningPathDiagramSmall created");
const props = defineProps<{
courseSlug: string;
}>();
const learningPathData = ref<LearningPath | undefined>(undefined);
const learningPathStore = useLearningPathStore();
learningPathStore
.loadLearningPath(props.courseSlug + "-lp", undefined, false, false)
.then((data) => {
learningPathData.value = data;
});
</script>
<template>
<LearningPathDiagram
v-if="learningPathData"
:learning-path="learningPathData"
diagram-type="horizontalSmall"
></LearningPathDiagram>
</template>
<style scoped></style>

View File

@ -1,9 +1,27 @@
import { graphqlClient } from "@/graphql/client";
import { COURSE_QUERY, COURSE_SESSION_DETAIL_QUERY } from "@/graphql/queries";
import {
circleFlatChildren,
circleFlatLearningContents,
circleFlatLearningUnits,
} from "@/services/circle";
import { useCompletionStore } from "@/stores/completion";
import { useCourseSessionsStore } from "@/stores/courseSessions";
import type { CourseSession, CourseSessionDetail } from "@/types";
import { useQuery } from "@urql/vue";
import { COURSE_SESSION_DETAIL_QUERY } from "@/graphql/queries";
import { useUserStore } from "@/stores/user";
import type {
ActionCompetence,
Course,
CourseCompletion,
CourseCompletionStatus,
CourseSession,
CourseSessionDetail,
LearningContentWithCompletion,
LearningPathType,
LearningUnitPerformanceCriteria,
PerformanceCriteria,
} from "@/types";
import { useQuery } from "@urql/vue";
import orderBy from "lodash/orderBy";
import log from "loglevel";
import type { ComputedRef } from "vue";
import { computed, ref, watchEffect } from "vue";
@ -56,19 +74,19 @@ export function useCourseSessionDetailQuery(courSessionId?: string) {
function findAssignment(learningContentId: string) {
return (courseSessionDetail.value?.assignments ?? []).find((a) => {
return a.learning_content.id === learningContentId;
return a.learning_content?.id === learningContentId;
});
}
function findEdoniqTest(learningContentId: string) {
return (courseSessionDetail.value?.edoniq_tests ?? []).find((e) => {
return e.learning_content.id === learningContentId;
return e.learning_content?.id === learningContentId;
});
}
function findAttendanceCourse(learningContentId: string) {
return (courseSessionDetail.value?.attendance_courses ?? []).find((e) => {
return e.learning_content.id === learningContentId;
return e.learning_content?.id === learningContentId;
});
}
@ -123,3 +141,273 @@ export function useCourseSessionDetailQuery(courSessionId?: string) {
filterCircleExperts,
};
}
export function flatCircles(learningPath: LearningPathType) {
return learningPath.topics.flatMap((t) => t.circles);
}
export function useCourseData(courseSlug: string) {
const learningPath = ref<LearningPathType | undefined>(undefined);
const actionCompetences = ref<ActionCompetence[]>([]);
const course = ref<Course | undefined>(undefined);
// urql.useQuery is not meant to be used programmatically, so we use graphqlClient.query instead
const resultPromise = graphqlClient
.query(COURSE_QUERY, { slug: `${courseSlug}` })
.toPromise();
resultPromise.then((result) => {
if (result.error) {
log.error(result.error);
}
course.value = result.data?.course as Course;
actionCompetences.value = result.data?.course
?.action_competences as ActionCompetence[];
learningPath.value = result.data?.course?.learning_path as LearningPathType;
// attach circle information to learning contents
if (learningPath.value) {
flatCircles(learningPath.value).forEach((circle) => {
circle.learning_sequences.forEach((ls, lsIndex) => {
const circleData = {
id: circle.id,
slug: circle.slug,
title: circle.title,
};
return ls.learning_units.forEach((lu, luIndex) => {
lu.circle = Object.assign({}, circleData);
lu.learning_contents.forEach((lc, lcIndex) => {
lc.circle = Object.assign({}, circleData);
lc.continueUrl = ls.frontend_url || circle.frontend_url;
lc.firstInCircle = lcIndex === 0 && luIndex === 0 && lsIndex === 0;
lc.parentLearningUnit = {
id: lu.id,
slug: lu.slug,
title: lu.title,
};
});
lu.performance_criteria.forEach((luPc) => {
luPc.circle = Object.assign({}, circleData);
const pc = findPerformanceCriterion(luPc.id);
if (pc) {
pc.circle = Object.assign({}, circleData);
}
});
});
});
});
}
});
const circles = computed(() => {
if (learningPath.value) {
return flatCircles(learningPath.value);
}
return undefined;
});
function findCircle(idOrSlug: string) {
return (circles.value ?? []).find((c) => {
return c.id === idOrSlug || c.slug.endsWith(idOrSlug);
});
}
function findPerformanceCriterion(id: string) {
return (actionCompetences.value ?? [])
.flatMap((ac) => {
return ac.performance_criteria;
})
.find((pc) => {
return pc.id === id;
}) as PerformanceCriteria | undefined;
}
function findLearningContent(
learningContentIdOrSlug: string,
circleIdOrSlug?: string
) {
let filteredCircles = circles.value ?? [];
if (circleIdOrSlug) {
filteredCircles = filteredCircles.filter((c) => {
return c.id === circleIdOrSlug || c.slug.endsWith(circleIdOrSlug);
});
}
return filteredCircles
.flatMap((c) => {
return circleFlatLearningContents(c);
})
.find((lc) => {
return (
lc.id === learningContentIdOrSlug || lc.slug.endsWith(learningContentIdOrSlug)
);
});
}
function findLearningUnit(learningUnitIdOrSlug: string, circleIdOrSlug?: string) {
let filteredCircles = circles.value ?? [];
if (circleIdOrSlug) {
filteredCircles = filteredCircles.filter((c) => {
return c.id === circleIdOrSlug || c.slug.endsWith(circleIdOrSlug);
});
}
return filteredCircles
.flatMap((c) => {
return circleFlatLearningUnits(c);
})
.find((lu) => {
return lu.id === learningUnitIdOrSlug || lu.slug.endsWith(learningUnitIdOrSlug);
});
}
const flatPerformanceCriteria = computed(() => {
return (actionCompetences.value ?? []).flatMap((ac) => {
return ac.performance_criteria;
}) as PerformanceCriteria[];
});
return {
resultPromise,
course,
learningPath,
actionCompetences,
circles,
findCircle,
findLearningContent,
findLearningUnit,
flatPerformanceCriteria,
};
}
export function useCourseDataWithCompletion(
courseSlug?: string,
userId?: string,
courseSessionId?: string
) {
if (!courseSlug) {
courseSlug = useCurrentCourseSession().value.course.slug;
}
if (!userId) {
userId = useUserStore().id;
}
if (!courseSessionId) {
courseSessionId = useCurrentCourseSession().value.id;
}
const courseResult = useCourseData(courseSlug);
const completionStore = useCompletionStore();
const nextLearningContent = ref<LearningContentWithCompletion | undefined>(undefined);
const loaded = ref(false);
function updateCompletionData() {
if (userId && courseSessionId) {
return completionStore.loadCourseSessionCompletionData(courseSessionId, userId);
}
return Promise.resolve([]);
}
function _parseCompletionData(completionData: CourseCompletion[]) {
if (courseResult.circles.value) {
courseResult.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";
}
});
});
}
if (courseResult.actionCompetences.value) {
courseResult.actionCompetences.value.forEach((ac) => {
ac.performance_criteria.forEach((pc) => {
const pageIndex = completionData.findIndex((e) => {
return e.page_id === pc.id;
});
if (pageIndex >= 0) {
pc.completion_status = completionData[pageIndex].completion_status;
} else {
pc.completion_status = "UNKNOWN";
}
});
});
}
calcNextLearningContent(completionData);
}
function calcNextLearningContent(completionData: CourseCompletion[]) {
const flatLearningContents = (courseResult.circles.value ?? []).flatMap((c) => {
return circleFlatLearningContents(c);
});
const lastCompleted = findLastCompletedLearningContent(completionData);
if (lastCompleted) {
const lastCompletedIndex = flatLearningContents.findIndex((lc) => {
return lc.id === lastCompleted.id;
});
if (flatLearningContents[lastCompletedIndex + 1]) {
nextLearningContent.value = flatLearningContents[lastCompletedIndex + 1];
} else {
nextLearningContent.value = undefined;
}
} else {
nextLearningContent.value = flatLearningContents[0];
}
}
function findLastCompletedLearningContent(completionData: CourseCompletion[]) {
const latestCompletion = orderBy(completionData ?? [], ["updated_at"], "desc").find(
(c: CourseCompletion) => {
return (
c.completion_status === "SUCCESS" &&
c.page_type.startsWith("learnpath.LearningContent")
);
}
);
if (latestCompletion) {
return courseResult.findLearningContent(latestCompletion.page_id);
}
}
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);
}
}
async function _start() {
return Promise.all([courseResult.resultPromise, updateCompletionData()]).then(
// eslint-disable-next-line @typescript-eslint/no-unused-vars
([_queryResults, completionData]) => {
_parseCompletionData(completionData);
loaded.value = true;
}
);
}
const resultPromise = _start();
return {
...courseResult,
loaded,
resultPromise,
markCompletion,
nextLearningContent,
};
}

View File

@ -1,9 +1,3 @@
import type { CourseCompletionStatus } from "@/types";
export const COMPLETION_SUCCESS: CourseCompletionStatus = "SUCCESS";
export const COMPLETION_FAILURE: CourseCompletionStatus = "FAIL";
export const COMPLETION_UNKNOWN: CourseCompletionStatus = "UNKNOWN";
export const itCheckboxDefaultIconCheckedTailwindClass =
"bg-[url(/static/icons/icon-checkbox-checked.svg)] hover:bg-[url(/static/icons/icon-checkbox-checked-hover.svg)]";

0
client/src/gql/dist/.gitkeep vendored Normal file
View File

View File

@ -18,9 +18,9 @@ const documents = {
"\n fragment CoursePageFields on CoursePageInterface {\n title\n id\n slug\n content_type\n frontend_url\n }\n": types.CoursePageFieldsFragmentDoc,
"\n query attendanceCheckQuery($courseSessionId: ID!) {\n course_session_attendance_course(id: $courseSessionId) {\n id\n attendance_user_list {\n user_id\n status\n }\n }\n }\n": types.AttendanceCheckQueryDocument,
"\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 courseQuery($courseId: ID!) {\n course(id: $courseId) {\n id\n slug\n title\n category_name\n learning_path {\n id\n }\n }\n }\n": types.CourseQueryDocument,
"\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 title\n id\n slug\n content_type\n frontend_url\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 id\n title\n slug\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 courseQuery($slug: String!) {\n course(slug: $slug) {\n id\n title\n slug\n category_name\n action_competences {\n competence_id\n ...CoursePageFields\n performance_criteria {\n competence_id\n learning_unit {\n id\n slug\n evaluate_url\n }\n ...CoursePageFields\n }\n }\n learning_path {\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 content_url\n minutes\n description\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 has_extended_time_test\n content_assignment {\n id\n }\n competence_certificate {\n ...CoursePageFields\n }\n }\n ... on LearningContentRichTextObjectType {\n text\n }\n }\n }\n }\n }\n }\n }\n }\n }\n": types.CourseQueryDocument,
"\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,
};
@ -61,15 +61,15 @@ export function graphql(source: "\n query assignmentCompletionQuery(\n $assi
/**
* 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 courseQuery($courseId: ID!) {\n course(id: $courseId) {\n id\n slug\n title\n category_name\n learning_path {\n id\n }\n }\n }\n"): (typeof documents)["\n query courseQuery($courseId: ID!) {\n course(id: $courseId) {\n id\n slug\n title\n category_name\n learning_path {\n id\n }\n }\n }\n"];
/**
* 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 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 title\n id\n slug\n content_type\n frontend_url\n circle {\n ...CoursePageFields\n }\n }\n }\n }\n }\n }\n"): (typeof documents)["\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 title\n id\n slug\n content_type\n frontend_url\n circle {\n ...CoursePageFields\n }\n }\n }\n }\n }\n }\n"];
export function graphql(source: "\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 id\n title\n slug\n }\n }\n }\n }\n }\n }\n"): (typeof documents)["\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 id\n title\n slug\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.
*/
export function graphql(source: "\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"): (typeof documents)["\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"];
/**
* 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 courseQuery($slug: String!) {\n course(slug: $slug) {\n id\n title\n slug\n category_name\n action_competences {\n competence_id\n ...CoursePageFields\n performance_criteria {\n competence_id\n learning_unit {\n id\n slug\n evaluate_url\n }\n ...CoursePageFields\n }\n }\n learning_path {\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 content_url\n minutes\n description\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 has_extended_time_test\n content_assignment {\n id\n }\n competence_certificate {\n ...CoursePageFields\n }\n }\n ... on LearningContentRichTextObjectType {\n text\n }\n }\n }\n }\n }\n }\n }\n }\n }\n"): (typeof documents)["\n query courseQuery($slug: String!) {\n course(slug: $slug) {\n id\n title\n slug\n category_name\n action_competences {\n competence_id\n ...CoursePageFields\n performance_criteria {\n competence_id\n learning_unit {\n id\n slug\n evaluate_url\n }\n ...CoursePageFields\n }\n }\n learning_path {\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 content_url\n minutes\n description\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 has_extended_time_test\n content_assignment {\n id\n }\n competence_certificate {\n ...CoursePageFields\n }\n }\n ... on LearningContentRichTextObjectType {\n text\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

File diff suppressed because it is too large Load Diff

View File

@ -1,3 +1,4 @@
export const ActionCompetenceObjectType = "ActionCompetenceObjectType";
export const AssignmentAssignmentAssignmentTypeChoices = "AssignmentAssignmentAssignmentTypeChoices";
export const AssignmentAssignmentCompletionCompletionStatusChoices = "AssignmentAssignmentCompletionCompletionStatusChoices";
export const AssignmentCompletionMutation = "AssignmentCompletionMutation";
@ -9,11 +10,12 @@ export const AttendanceUserInputType = "AttendanceUserInputType";
export const AttendanceUserObjectType = "AttendanceUserObjectType";
export const AttendanceUserStatus = "AttendanceUserStatus";
export const Boolean = "Boolean";
export const CircleDocumentObjectType = "CircleDocumentObjectType";
export const CircleLightObjectType = "CircleLightObjectType";
export const CircleObjectType = "CircleObjectType";
export const CompetenceCertificateListObjectType = "CompetenceCertificateListObjectType";
export const CompetenceCertificateObjectType = "CompetenceCertificateObjectType";
export const CoreUserLanguageChoices = "CoreUserLanguageChoices";
export const CourseCourseSessionUserRoleChoices = "CourseCourseSessionUserRoleChoices";
export const CourseObjectType = "CourseObjectType";
export const CoursePageInterface = "CoursePageInterface";
export const CourseSessionAssignmentObjectType = "CourseSessionAssignmentObjectType";
@ -49,9 +51,10 @@ 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";
export const TopicObjectType = "TopicObjectType";
export const UUID = "UUID";
export const UserType = "UserType";
export const UserObjectType = "UserObjectType";

View File

@ -2,7 +2,7 @@
// @ts-ignore
import { cacheExchange } from "@urql/exchange-graphcache";
import { Client, fetchExchange } from "@urql/vue";
import schema from "../gql/minifiedSchema.json";
import schema from "../gql/dist/minifiedSchema.json";
import {
AssignmentCompletionMutation,
AssignmentCompletionObjectType,

View File

@ -76,20 +76,6 @@ export const ASSIGNMENT_COMPLETION_QUERY = graphql(`
}
`);
export const COURSE_QUERY = graphql(`
query courseQuery($courseId: ID!) {
course(id: $courseId) {
id
slug
title
category_name
learning_path {
id
}
}
}
`);
export const COMPETENCE_NAVI_CERTIFICATE_QUERY = graphql(`
query competenceCertificateQuery($courseSlug: String!, $courseSessionId: ID!) {
competence_certificate_list(course_slug: $courseSlug) {
@ -109,13 +95,11 @@ export const COMPETENCE_NAVI_CERTIFICATE_QUERY = graphql(`
evaluation_passed
}
learning_content {
title
id
slug
content_type
frontend_url
...CoursePageFields
circle {
...CoursePageFields
id
title
slug
}
}
}
@ -208,3 +192,79 @@ export const COURSE_SESSION_DETAIL_QUERY = graphql(`
}
}
`);
export const COURSE_QUERY = graphql(`
query courseQuery($slug: String!) {
course(slug: $slug) {
id
title
slug
category_name
action_competences {
competence_id
...CoursePageFields
performance_criteria {
competence_id
learning_unit {
id
slug
evaluate_url
}
...CoursePageFields
}
}
learning_path {
...CoursePageFields
topics {
is_visible
...CoursePageFields
circles {
description
goals
...CoursePageFields
learning_sequences {
icon
...CoursePageFields
learning_units {
evaluate_url
...CoursePageFields
performance_criteria {
...CoursePageFields
}
learning_contents {
can_user_self_toggle_course_completion
content_url
minutes
description
...CoursePageFields
... on LearningContentAssignmentObjectType {
assignment_type
content_assignment {
id
}
competence_certificate {
...CoursePageFields
}
}
... on LearningContentEdoniqTestObjectType {
checkbox_text
has_extended_time_test
content_assignment {
id
}
competence_certificate {
...CoursePageFields
}
}
... on LearningContentRichTextObjectType {
text
}
}
}
}
}
}
}
}
}
`);

View File

@ -1,17 +1,16 @@
<script setup lang="ts">
import { computed, onMounted, ref, watch } from "vue";
import { useCourseSessionsStore } from "@/stores/courseSessions";
import { useLearningPathStore } from "@/stores/learningPath";
import { useTranslation } from "i18next-vue";
import ItDropdownSelect from "@/components/ui/ItDropdownSelect.vue";
import type { DueDate } from "@/types";
import DueDatesList from "@/components/dueDates/DueDatesList.vue";
import { useCourseData } from "@/composables";
const { t } = useTranslation();
const UNFILTERED = Number.MAX_SAFE_INTEGER.toString();
const courseSessionsStore = useCourseSessionsStore();
const learningPathStore = useLearningPathStore();
type Item = {
id: string;
@ -61,16 +60,15 @@ const circles = ref<Item[]>([initialItemCircle]);
const selectedCircle = ref<Item>(circles.value[0]);
async function loadCircleValues() {
const data = await learningPathStore.loadLearningPath(
`${selectedCourse.value.slug}-lp`,
undefined,
false,
false
);
if (data) {
if (selectedCourse.value) {
const learningPathQuery = useCourseData(selectedCourse.value.slug);
await learningPathQuery.resultPromise;
circles.value = [
initialItemCircle,
...data.circles.map((circle) => ({ id: circle.id, name: circle.title })),
...(learningPathQuery.circles.value ?? []).map((circle) => ({
id: circle.id,
name: circle.title,
})),
];
} else {
circles.value = [initialItemCircle];

View File

@ -1,12 +1,12 @@
<script setup lang="ts">
import DueDatesList from "@/components/dueDates/DueDatesList.vue";
import LearningPathDiagramSmall from "@/components/learningPath/LearningPathDiagramSmall.vue";
import { useCourseSessionsStore } from "@/stores/courseSessions";
import { useUserStore } from "@/stores/user";
import type { CourseSession } from "@/types";
import log from "loglevel";
import { computed, onMounted } from "vue";
import { getCockpitUrl, getLearningPathUrl } from "@/utils/utils";
import LearningPathDiagram from "@/components/learningPath/LearningPathDiagram.vue";
log.debug("DashboardPage created");
@ -47,10 +47,12 @@ const getNextStepLink = (courseSession: CourseSession) => {
<div class="bg-white p-6 md:h-full">
<h3 class="mb-4">{{ courseSession.course.title }}</h3>
<div>
<LearningPathDiagramSmall
<LearningPathDiagram
class="mb-4"
:course-slug="courseSession.course.slug"
></LearningPathDiagramSmall>
:course-session-id="courseSession.id"
diagram-type="horizontalSmall"
></LearningPathDiagram>
</div>
<div>
<router-link

View File

@ -1,11 +1,11 @@
<script setup lang="ts">
import { useCourseSessionDetailQuery } from "@/composables";
import {
useCourseDataWithCompletion,
useCourseSessionDetailQuery,
} from "@/composables";
import { useCockpitStore } from "@/stores/cockpit";
import { useCompetenceStore } from "@/stores/competence";
import { useLearningPathStore } from "@/stores/learningPath";
import { useUserStore } from "@/stores/user";
import * as log from "loglevel";
import { onMounted } from "vue";
import { onMounted, ref } from "vue";
log.debug("CockpitParentPage created");
@ -14,33 +14,28 @@ const props = defineProps<{
}>();
const cockpitStore = useCockpitStore();
const competenceStore = useCompetenceStore();
const learningPathStore = useLearningPathStore();
const courseSessionDetailResult = useCourseSessionDetailQuery();
const loaded = ref(false);
onMounted(async () => {
log.debug("CockpitParentPage mounted", props.courseSlug);
try {
await courseSessionDetailResult.waitForData();
const members = courseSessionDetailResult.filterMembers();
members.forEach((csu) => {
competenceStore.loadCompetenceProfilePage(
props.courseSlug + "-competencenavi-competences",
csu.user_id
);
learningPathStore.loadLearningPath(props.courseSlug + "-lp", csu.user_id);
});
await learningPathStore.loadLearningPath(
props.courseSlug + "-lp",
useUserStore().id
);
await cockpitStore.loadCircles(
props.courseSlug,
courseSessionDetailResult.findCurrentUser()
);
// workaround so that the completion data is loaded before display
const userDataPromises = courseSessionDetailResult.filterMembers().map((m) => {
const completionData = useCourseDataWithCompletion(props.courseSlug, m.id);
return completionData.resultPromise;
});
await Promise.all(userDataPromises);
loaded.value = true;
} catch (error) {
log.error(error);
}
@ -50,7 +45,9 @@ onMounted(async () => {
<template>
<div class="bg-gray-200">
<main>
<router-view></router-view>
<div v-if="loaded">
<router-view></router-view>
</div>
</main>
</div>
</template>

View File

@ -1,12 +1,13 @@
<script setup lang="ts">
import { useCompetenceStore } from "@/stores/competence";
import { useLearningPathStore } from "@/stores/learningPath";
import * as log from "loglevel";
import { computed, onMounted } from "vue";
import CompetenceDetail from "@/pages/competence/ActionCompetenceDetail.vue";
import LearningPathPathView from "@/pages/learningPath/learningPathPage/LearningPathPathView.vue";
import { useCourseSessionDetailQuery } from "@/composables";
import {
useCourseSessionDetailQuery,
useCourseDataWithCompletion,
} from "@/composables";
const props = defineProps<{
userId: string;
@ -15,15 +16,19 @@ const props = defineProps<{
log.debug("CockpitUserProfilePage created", props.userId);
const competenceStore = useCompetenceStore();
const learningPathStore = useLearningPathStore();
const courseCompletionData = useCourseDataWithCompletion(
props.courseSlug,
props.userId
);
onMounted(async () => {
log.debug("CockpitUserProfilePage mounted");
});
const lpQueryResult = useCourseDataWithCompletion(props.courseSlug, props.userId);
const learningPath = computed(() => {
return learningPathStore.learningPathForUser(props.courseSlug, props.userId);
return lpQueryResult.learningPath.value;
});
const { findUser } = useCourseSessionDetailQuery();
@ -66,6 +71,7 @@ function setActiveClasses(isActive: boolean) {
:use-mobile-layout="false"
:hide-buttons="true"
:learning-path="learningPath"
:next-learning-content="undefined"
:override-circle-url-base="`/course/${props.courseSlug}/cockpit/profile/${props.userId}`"
></LearningPathPathView>
</div>
@ -84,12 +90,9 @@ function setActiveClasses(isActive: boolean) {
</li>
</ul>
<div>
<ul
v-if="competenceStore.competenceProfilePage(user.user_id)"
class="bg-white px-8"
>
<ul class="bg-white px-8">
<li
v-for="competence in competenceStore.competences(user.user_id)"
v-for="competence in courseCompletionData.actionCompetences.value ?? []"
:key="competence.id"
class="border-b border-gray-500 p-8 last:border-0"
>

View File

@ -65,7 +65,7 @@ const assignmentDetail = computed(() => {
});
const dueDate = computed(() =>
dayjs(assignmentDetail.value?.evaluation_deadline.start)
dayjs(assignmentDetail.value?.evaluation_deadline?.start)
);
const inEvaluationTask = computed(

View File

@ -81,7 +81,7 @@ const courseSessionDetailResult = useCourseSessionDetailQuery();
const evaluationUser = computed(() => {
if (props.assignmentCompletion.evaluation_user) {
return courseSessionDetailResult.findUser(
props.assignmentCompletion.evaluation_user
props.assignmentCompletion.evaluation_user?.id
);
}
return undefined;
@ -113,7 +113,12 @@ const evaluationUser = computed(() => {
</div>
</section>
<div v-if="!props.assignmentCompletion.evaluation_passed">
<div
v-if="
props.assignmentCompletion.completion_status === 'EVALUATION_SUBMITTED' &&
!props.assignmentCompletion.evaluation_passed
"
>
<span class="my-2 rounded-md bg-error-red-200 px-2.5 py-0.5">
{{ $t("a.Nicht Bestanden") }}
</span>

View File

@ -13,16 +13,14 @@ import { computed, onMounted, reactive } from "vue";
import AssignmentSubmissionProgress from "@/pages/cockpit/cockpitPage/AssignmentSubmissionProgress.vue";
import { useCourseSessionDetailQuery } from "@/composables";
import { formatDueDate } from "../../../components/dueDates/dueDatesUtils";
import { stringifyParse } from "@/utils/utils";
const props = defineProps<{
courseSession: CourseSession;
learningContentAssignment: LearningContentAssignment;
}>();
log.debug(
"AssignmentDetails created",
props.learningContentAssignment.content_assignment_id
);
log.debug("AssignmentDetails created", stringifyParse(props));
const courseSessionDetailResult = useCourseSessionDetailQuery();
@ -39,7 +37,7 @@ const assignmentDetail = computed(() => {
onMounted(async () => {
const { gradedUsers, assignmentSubmittedUsers } =
await loadAssignmentCompletionStatusData(
props.learningContentAssignment.content_assignment_id,
props.learningContentAssignment.content_assignment.id,
props.courseSession.id,
props.learningContentAssignment.id
);
@ -54,17 +52,17 @@ onMounted(async () => {
{{ learningContentAssignment.title }}
</h2>
<div class="pt-1 underline">
Circle «{{ learningContentAssignment.parentCircle.title }}»
{{ $t("a.Circle") }} «{{ learningContentAssignment.circle?.title }}»
</div>
<div v-if="assignmentDetail">
<span v-if="assignmentDetail.submission_deadline?.start">
{{ $t("Abgabetermin Ergebnisse:") }}
{{ formatDueDate(assignmentDetail.submission_deadline.start) }}
{{ formatDueDate(assignmentDetail.submission_deadline?.start) }}
</span>
<template v-if="assignmentDetail.evaluation_deadline?.start">
<br />
{{ $t("Freigabetermin Bewertungen:") }}
{{ formatDueDate(assignmentDetail.evaluation_deadline.start) }}
{{ formatDueDate(assignmentDetail.evaluation_deadline?.start) }}
</template>
</div>
<div v-else>
@ -131,7 +129,7 @@ onMounted(async () => {
<template #link>
<router-link
v-if="state.assignmentSubmittedUsers.includes(csu)"
:to="`/course/${props.courseSession.course.slug}/cockpit/assignment/${learningContentAssignment.content_assignment_id}/${csu.user_id}`"
:to="`/course/${props.courseSession.course.slug}/cockpit/assignment/${learningContentAssignment.content_assignment.id}/${csu.user_id}`"
class="link lg:w-full lg:text-right"
data-cy="show-results"
>

View File

@ -1,11 +1,9 @@
<script setup lang="ts">
import { useCurrentCourseSession } from "@/composables";
import { useCurrentCourseSession, useCourseData } from "@/composables";
import AssignmentDetails from "@/pages/cockpit/assignmentsPage/AssignmentDetails.vue";
import * as log from "loglevel";
import { computed, onMounted } from "vue";
import { useUserStore } from "@/stores/user";
import { useLearningPathStore } from "@/stores/learningPath";
import { calcLearningContentAssignments } from "@/services/assignmentService";
import type { LearningContentAssignment } from "@/types";
const props = defineProps<{
courseSlug: string;
@ -15,17 +13,15 @@ const props = defineProps<{
log.debug("AssignmentsPage created", props.courseSlug);
const courseSession = useCurrentCourseSession();
const userStore = useUserStore();
const learningPathStore = useLearningPathStore();
onMounted(async () => {
log.debug("AssignmentsPage mounted");
});
const lpQueryResult = useCourseData(props.courseSlug);
const learningContentAssignment = computed(() => {
return calcLearningContentAssignments(
learningPathStore.learningPathForUser(courseSession.value.course.slug, userStore.id)
).filter((lc) => lc.id === props.assignmentId)[0];
return lpQueryResult.findLearningContent(props.assignmentId);
});
</script>
@ -46,7 +42,7 @@ const learningContentAssignment = computed(() => {
<AssignmentDetails
v-if="learningContentAssignment"
:course-session="courseSession"
:learning-content-assignment="learningContentAssignment"
:learning-content-assignment="learningContentAssignment as LearningContentAssignment"
/>
</div>
</main>

View File

@ -32,8 +32,8 @@ const presenceCoursesDropdownOptions = computed(() => {
({
id: attendanceCourse.id,
name: `${t("Präsenzkurs")} ${
attendanceCourse.learning_content.circle.title
} ${dayjs(attendanceCourse.due_date.start).format("DD.MM.YYYY")}`,
attendanceCourse.learning_content.circle?.title
} ${dayjs(attendanceCourse.due_date?.start).format("DD.MM.YYYY")}`,
} as DropdownSelectable)
);
});

View File

@ -9,6 +9,7 @@ import type {
} from "@/types";
import log from "loglevel";
import { onMounted, reactive } from "vue";
import { stringifyParse } from "@/utils/utils";
const props = defineProps<{
courseSession: CourseSession;
@ -16,10 +17,7 @@ const props = defineProps<{
showTitle: boolean;
}>();
log.debug(
"AssignmentSubmissionProgress created",
props.learningContentAssignment.content_assignment_id
);
log.debug("AssignmentSubmissionProgress created", stringifyParse(props));
const state = reactive({
statusByUser: [] as {
@ -34,7 +32,7 @@ const state = reactive({
onMounted(async () => {
const { assignmentSubmittedUsers, gradedUsers, total } =
await loadAssignmentCompletionStatusData(
props.learningContentAssignment.content_assignment_id,
props.learningContentAssignment.content_assignment.id,
props.courseSession.id,
props.learningContentAssignment.id
);

View File

@ -1,16 +1,14 @@
<script setup lang="ts">
import LearningPathDiagram from "@/components/learningPath/LearningPathDiagram.vue";
import ItPersonRow from "@/components/ui/ItPersonRow.vue";
import type { LearningPath } from "@/services/learningPath";
import { useCourseSessionDetailQuery, useCurrentCourseSession } from "@/composables";
import SubmissionsOverview from "@/pages/cockpit/cockpitPage/SubmissionsOverview.vue";
import { useCockpitStore } from "@/stores/cockpit";
import { useCompetenceStore } from "@/stores/competence";
import { useLearningPathStore } from "@/stores/learningPath";
import log from "loglevel";
import CockpitDates from "@/pages/cockpit/cockpitPage/CockpitDates.vue";
import ItDropdownSelect from "@/components/ui/ItDropdownSelect.vue";
import UserStatusCount from "@/pages/cockpit/cockpitPage/UserStatusCount.vue";
const props = defineProps<{
courseSlug: string;
@ -19,19 +17,8 @@ const props = defineProps<{
log.debug("CockpitIndexPage created", props.courseSlug);
const cockpitStore = useCockpitStore();
const competenceStore = useCompetenceStore();
const learningPathStore = useLearningPathStore();
const courseSession = useCurrentCourseSession();
const courseSessionDetailResult = useCourseSessionDetailQuery();
function userCountStatusForCircle(userId: string) {
if (!cockpitStore.currentCircle) return { FAIL: 0, SUCCESS: 0, UNKNOWN: 0 };
const criteria = competenceStore.flatPerformanceCriteria(
userId,
cockpitStore.currentCircle.id
);
return competenceStore.calcStatusCount(criteria);
}
</script>
<template>
@ -143,18 +130,9 @@ function userCountStatusForCircle(userId: string) {
class="mt-2 flex w-full flex-col items-center justify-between lg:mt-0 lg:flex-row"
>
<LearningPathDiagram
v-if="
learningPathStore.learningPathForUser(
props.courseSlug,
csu.user_id
)
"
:learning-path="
learningPathStore.learningPathForUser(
props.courseSlug,
csu.user_id
) as LearningPath
"
:course-session-id="courseSession.id"
:course-slug="props.courseSlug"
:user-id="csu.user_id"
:show-circle-slugs="[cockpitStore.currentCircle.slug]"
diagram-type="singleSmall"
class="mr-4"
@ -162,32 +140,10 @@ function userCountStatusForCircle(userId: string) {
<p class="lg:min-w-[150px]">
{{ cockpitStore.currentCircle.title }}
</p>
<div class="ml-4 flex flex-row items-center">
<div class="mr-6 flex flex-row items-center">
<it-icon-smiley-thinking
class="mr-2 inline-block h-8 w-8"
></it-icon-smiley-thinking>
<p class="text-bold inline-block w-6">
{{ userCountStatusForCircle(csu.user_id).FAIL }}
</p>
</div>
<li class="mr-6 flex flex-row items-center">
<it-icon-smiley-happy
class="mr-2 inline-block h-8 w-8"
></it-icon-smiley-happy>
<p class="text-bold inline-block w-6">
{{ userCountStatusForCircle(csu.user_id).SUCCESS }}
</p>
</li>
<li class="flex flex-row items-center">
<it-icon-smiley-neutral
class="mr-2 inline-block h-8 w-8"
></it-icon-smiley-neutral>
<p class="text-bold inline-block w-6">
{{ userCountStatusForCircle(csu.user_id).UNKNOWN }}
</p>
</li>
</div>
<UserStatusCount
:course-slug="props.courseSlug"
:user-id="csu.user_id"
></UserStatusCount>
</div>
</template>
<template #link>

View File

@ -1,8 +1,6 @@
2
<script setup lang="ts">
import AssignmentSubmissionProgress from "@/pages/cockpit/cockpitPage/AssignmentSubmissionProgress.vue";
import { useLearningPathStore } from "@/stores/learningPath";
import { useUserStore } from "@/stores/user";
import type {
CourseSession,
LearningContent,
@ -13,11 +11,16 @@ import { computed } from "vue";
import { useTranslation } from "i18next-vue";
import FeedbackSubmissionProgress from "@/pages/cockpit/cockpitPage/FeedbackSubmissionProgress.vue";
import { learningContentTypeData } from "@/utils/typeMaps";
import { useCourseSessionDetailQuery } from "@/composables";
import {
useCourseSessionDetailQuery,
useCourseDataWithCompletion,
} from "@/composables";
import { circleFlatLearningContents } from "@/services/circle";
interface Submittable {
id: string;
circleName: string;
circleId: string;
frontendUrl: string;
title: string;
showDetailsText: string;
@ -32,24 +35,20 @@ const props = defineProps<{
log.debug("SubmissionsOverview created", props.courseSession.id);
const userStore = useUserStore();
const learningPathStore = useLearningPathStore();
const courseSessionDetailResult = useCourseSessionDetailQuery();
const { t } = useTranslation();
const lpQueryResult = useCourseDataWithCompletion();
const submittables = computed(() => {
const learningPath = learningPathStore.learningPathForUser(
props.courseSession.course.slug,
userStore.id
);
if (!learningPath) {
if (!lpQueryResult.circles.value?.length) {
return [];
}
return learningPath.circles
return lpQueryResult.circles.value
.filter((circle) => props.selectedCircle == circle.id)
.flatMap((circle) => {
const learningContents = circle.flatLearningContents.filter(
const learningContents = circleFlatLearningContents(circle).filter(
(lc) =>
lc.content_type === "learnpath.LearningContentAssignment" ||
lc.content_type === "learnpath.LearningContentFeedback"
@ -59,10 +58,11 @@ const submittables = computed(() => {
return {
id: lc.id,
circleName: circle.title,
circleId: circle.id,
frontendUrl: lc.frontend_url,
title: getLearningContentType(lc),
showDetailsText: getShowDetailsText(lc),
detailsLink: getDetailsLink(lc),
detailsLink: getDetailsLink(lc, circle.id),
content: lc,
};
});
@ -103,9 +103,9 @@ const getShowDetailsText = (lc: LearningContent) => {
return t("Feedback anschauen");
};
const getDetailsLink = (lc: LearningContent) => {
const getDetailsLink = (lc: LearningContent, circleId: string) => {
if (isFeedback(lc)) {
return `cockpit/feedback/${lc.parentCircle.id}`;
return `cockpit/feedback/${circleId}`;
}
return `cockpit/assignment/${lc.id}`;
};
@ -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
@ -160,7 +162,7 @@ const getIconName = (lc: LearningContent) => {
<FeedbackSubmissionProgress
v-if="isFeedback(submittable.content)"
:course-session="props.courseSession"
:circle-id="submittable.content.parentCircle.id"
:circle-id="submittable.circleId"
class="grow pr-8"
></FeedbackSubmissionProgress>
<div class="flex items-center lg:w-1/4 lg:justify-end">

View File

@ -0,0 +1,51 @@
<script setup lang="ts">
import { useCourseDataWithCompletion } from "@/composables";
import { computed } from "vue";
import { calcPerformanceCriteriaStatusCount } from "@/services/competence";
const props = defineProps<{
courseSlug: string;
userId: string;
circleId?: string;
}>();
const courseData = useCourseDataWithCompletion(props.courseSlug, props.userId);
const circleStatusCount = computed(() => {
if (props.circleId) {
return calcPerformanceCriteriaStatusCount(
(courseData.flatPerformanceCriteria.value ?? []).filter(
(pc) => pc.circle.id === props.circleId
)
);
}
return calcPerformanceCriteriaStatusCount(courseData.flatPerformanceCriteria.value);
});
</script>
<template>
<div v-if="courseData.loaded" class="ml-4 flex flex-row items-center">
<div class="mr-6 flex flex-row items-center">
<it-icon-smiley-thinking
class="mr-2 inline-block h-8 w-8"
></it-icon-smiley-thinking>
<p class="text-bold inline-block w-6">
{{ circleStatusCount.FAIL }}
</p>
</div>
<li class="mr-6 flex flex-row items-center">
<it-icon-smiley-happy class="mr-2 inline-block h-8 w-8"></it-icon-smiley-happy>
<p class="text-bold inline-block w-6">
{{ circleStatusCount.SUCCESS }}
</p>
</li>
<li class="flex flex-row items-center">
<it-icon-smiley-neutral
class="mr-2 inline-block h-8 w-8"
></it-icon-smiley-neutral>
<p class="text-bold inline-block w-6">
{{ circleStatusCount.UNKNOWN }}
</p>
</li>
</div>
</template>

View File

@ -1,5 +1,5 @@
<script setup lang="ts">
import { useCurrentCourseSession } from "@/composables";
import { useCurrentCourseSession, useCourseData } from "@/composables";
import ItDropdownSelect from "@/components/ui/ItDropdownSelect.vue";
import { useCockpitStore } from "@/stores/cockpit";
import ItModal from "@/components/ui/ItModal.vue";
@ -16,13 +16,11 @@ import {
} from "@/services/files";
import { useCourseSessionsStore } from "@/stores/courseSessions";
import DocumentListItem from "@/components/circle/DocumentListItem.vue";
import { useCircleStore } from "@/stores/circle";
const cockpitStore = useCockpitStore();
const courseSession = useCurrentCourseSession();
const circleStore = useCircleStore();
const courseSessionsStore = useCourseSessionsStore();
const courseData = useCourseData(courseSession.value?.course.slug);
const { t } = useTranslation();
@ -51,26 +49,18 @@ onMounted(async () => {
await fetchDocuments();
});
watch(
// workaround to load learning sequences when circle changes
() => cockpitStore.currentCircle,
async () => {
if (cockpitStore.currentCircle) {
await circleStore.loadCircle(
courseSession.value?.course.slug,
cockpitStore.currentCircle?.slug
);
const dropdownLearningSequences = computed(() => {
if (cockpitStore.currentCircle?.slug) {
const circle = courseData.findCircle(cockpitStore.currentCircle?.slug);
if (circle) {
return circle.learning_sequences.map((sequence) => ({
id: sequence.id,
name: `${sequence.title}`,
}));
}
},
{ immediate: true }
);
const dropdownLearningSequences = computed(() =>
circleStore.circle?.learningSequences.map((sequence) => ({
id: sequence.id,
name: `${sequence.title}`,
}))
);
}
return [];
});
const circleDocuments = computed(() => {
return circleDocumentsResultData.value.filter(

View File

@ -1,15 +1,12 @@
<script setup lang="ts">
import ItToggleArrow from "@/components/ui/ItToggleArrow.vue";
import { useCompetenceStore } from "@/stores/competence";
import type { CompetencePage } from "@/types";
import log from "loglevel";
import { ref, watch } from "vue";
import SinglePerformanceCriteriaRow from "@/pages/competence/SinglePerformanceCriteriaRow.vue";
const competenceStore = useCompetenceStore();
import type { ActionCompetence } from "@/types";
interface Props {
competence: CompetencePage;
competence: ActionCompetence;
courseSlug: string;
showAssessAgain?: boolean;
isInline?: boolean;
@ -60,12 +57,12 @@ const togglePerformanceCriteria = () => {
</li>
<li
v-for="performanceCriteria in competenceStore.criteriaByCompetence(competence)"
v-for="performanceCriteria in competence.performance_criteria"
:key="performanceCriteria.id"
class="my-4 border-b pb-4 last:border-0"
>
<SinglePerformanceCriteriaRow
:criteria="performanceCriteria"
:criterion="performanceCriteria"
:show-state="false"
:course-slug="props.courseSlug"
:show-assess-again="props.showAssessAgain"

View File

@ -1,8 +1,8 @@
<script setup lang="ts">
import CompetenceDetail from "@/pages/competence/ActionCompetenceDetail.vue";
import { useCompetenceStore } from "@/stores/competence";
import * as log from "loglevel";
import { ref } from "vue";
import { useCourseDataWithCompletion } from "@/composables";
log.debug("CompetenceListPage created");
@ -10,7 +10,7 @@ const props = defineProps<{
courseSlug: string;
}>();
const competenceStore = useCompetenceStore();
const courseData = useCourseDataWithCompletion(props.courseSlug);
const isOpenAll = ref(false);
@ -40,9 +40,9 @@ function toggleOpen() {
</div>
</div>
<ul v-if="competenceStore.competenceProfilePage()">
<ul v-if="courseData.actionCompetences.value?.length">
<li
v-for="competence in competenceStore.competences()"
v-for="competence in courseData.actionCompetences.value"
:key="competence.id"
class="mb-8 bg-white px-8 pt-6"
>

View File

@ -4,14 +4,14 @@ import { COMPETENCE_NAVI_CERTIFICATE_QUERY } from "@/graphql/queries";
import { useQuery } from "@urql/vue";
import { computed } from "vue";
import type { CompetenceCertificate } from "@/types";
import { useCurrentCourseSession } from "@/composables";
import { useCurrentCourseSession, useCourseDataWithCompletion } from "@/composables";
import {
assignmentsMaxEvaluationPoints,
assignmentsUserPoints,
competenceCertificateProgressStatusCount,
} from "@/pages/competence/utils";
import ItProgress from "@/components/ui/ItProgress.vue";
import { useCompetenceStore } from "@/stores/competence";
import { calcPerformanceCriteriaStatusCount } from "@/services/competence";
const props = defineProps<{
courseSlug: string;
@ -20,8 +20,7 @@ const props = defineProps<{
log.debug("CompetenceIndexPage setup", props);
const courseSession = useCurrentCourseSession();
const competenceStore = useCompetenceStore();
const courseData = useCourseDataWithCompletion(props.courseSlug);
const certificatesQuery = useQuery({
query: COMPETENCE_NAVI_CERTIFICATE_QUERY,
@ -51,7 +50,7 @@ const userPointsEvaluatedAssignments = computed(() => {
});
const performanceCriteriaStatusCount = computed(() => {
return competenceStore.calcStatusCount(competenceStore.flatPerformanceCriteria());
return calcPerformanceCriteriaStatusCount(courseData.flatPerformanceCriteria.value);
});
</script>

View File

@ -1,5 +1,4 @@
<script setup lang="ts">
import { useCompetenceStore } from "@/stores/competence";
import * as log from "loglevel";
import { onMounted } from "vue";
import { useRoute } from "vue-router";
@ -10,8 +9,6 @@ const props = defineProps<{
courseSlug: string;
}>();
const competenceStore = useCompetenceStore();
const route = useRoute();
function routeInOverview() {
@ -32,13 +29,6 @@ function routeInActionCompetences() {
onMounted(async () => {
log.debug("CompetenceParentPage mounted", props.courseSlug);
try {
const competencePageSlug = props.courseSlug + "-competencenavi-competences";
await competenceStore.loadCompetenceProfilePage(competencePageSlug);
} catch (error) {
log.error(error);
}
});
</script>

View File

@ -1,8 +1,8 @@
<script setup lang="ts">
import { useCompetenceStore } from "@/stores/competence";
import * as log from "loglevel";
import { computed } from "vue";
import _ from "lodash";
import { useCourseDataWithCompletion } from "@/composables";
const props = defineProps<{
courseSlug: string;
@ -10,28 +10,30 @@ const props = defineProps<{
log.debug("PerformanceCriteriaPage created", props);
const competenceStore = useCompetenceStore();
const courseCompletionData = useCourseDataWithCompletion(props.courseSlug);
const uniqueLearningUnits = computed(() => {
// FIXME: this complex calculation can go away,
// once the criteria are in its own learning content
// get the learningUnits sorted by circle order in the course
const circles = competenceStore.circles.map((c, index) => {
const circles = (courseCompletionData.circles.value ?? []).map((c, index) => {
return { ...c, sortKey: index };
});
return _.orderBy(
_.uniqBy(
competenceStore.flatPerformanceCriteria().map((pc) => {
return {
luId: pc.learning_unit.id,
luTitle: pc.learning_unit.title,
luSlug: pc.learning_unit.slug,
circleId: pc.circle.id,
circleTitle: pc.circle.title,
url: pc.learning_unit.evaluate_url,
sortKey: circles.find((c) => c.id === pc.circle.id)?.sortKey,
};
}),
(courseCompletionData.flatPerformanceCriteria.value ?? [])
.filter((pc) => Boolean(pc.learning_unit))
.map((pc) => {
return {
luId: pc.learning_unit?.id,
luTitle: pc.learning_unit?.title,
luSlug: pc.learning_unit?.slug,
circleId: pc.circle.id,
circleTitle: pc.circle.title,
url: pc.learning_unit?.evaluate_url,
sortKey: circles.find((c) => c.id === pc.circle.id)?.sortKey,
};
}),
"luId"
),
"sortKey"
@ -40,9 +42,9 @@ const uniqueLearningUnits = computed(() => {
const criteriaByLearningUnit = computed(() => {
return uniqueLearningUnits.value.map((lu) => {
const criteria = competenceStore
.flatPerformanceCriteria()
.filter((pc) => pc.learning_unit.id === lu.luId);
const criteria = (courseCompletionData.flatPerformanceCriteria.value ?? []).filter(
(pc) => pc.learning_unit?.id === lu.luId
);
return {
...lu,
countSuccess: criteria.filter((c) => c.completion_status === "SUCCESS").length,
@ -98,7 +100,7 @@ const criteriaByLearningUnit = computed(() => {
<div>
<router-link
:to="selfEvaluation.url"
:to="selfEvaluation.url ?? '/'"
class="link"
:data-cy="`${selfEvaluation.luSlug}-open`"
>

View File

@ -2,7 +2,7 @@
import type { PerformanceCriteria } from "@/types";
interface Props {
criteria: PerformanceCriteria;
criterion: PerformanceCriteria;
courseSlug: string;
showState?: boolean;
showAssessAgain?: boolean;
@ -19,24 +19,24 @@ const props = withDefaults(defineProps<Props>(), {
<div class="flex flex-row items-center">
<div v-if="showState" class="mr-4 h-8 w-8">
<it-icon-smiley-happy
v-if="criteria.completion_status === 'SUCCESS'"
v-if="criterion.completion_status === 'SUCCESS'"
></it-icon-smiley-happy>
<it-icon-smiley-thinking
v-else-if="criteria.completion_status === 'FAIL'"
v-else-if="criterion.completion_status === 'FAIL'"
></it-icon-smiley-thinking>
<it-icon-smiley-neutral v-else></it-icon-smiley-neutral>
</div>
<div class="mb-4 pr-4 lg:mb-0 lg:mr-8">
{{ criteria.title }}
{{ criterion.title }}
</div>
</div>
<span class="lg:whitespace-nowrap">
<router-link
v-if="props.showAssessAgain"
v-if="props.showAssessAgain && criterion.learning_unit?.evaluate_url"
class="link"
:to="criteria.learning_unit.evaluate_url"
:to="criterion.learning_unit.evaluate_url"
>
{{ $t("competences.assessAgain", { x: criteria.circle.title }) }}
{{ $t("competences.assessAgain", { x: criterion.circle.title }) }}
</router-link>
</span>
</div>

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 false;
return allFinishedInLearningSequence(learningSequence);
}
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

@ -1,16 +1,18 @@
<script setup lang="ts">
import { useCircleStore } from "@/stores/circle";
import type { CourseSessionUser } from "@/types";
import { humanizeDuration } from "@/utils/humanizeDuration";
import sumBy from "lodash/sumBy";
import log from "loglevel";
import { computed, onMounted } from "vue";
import { computed, watch } from "vue";
import { useRoute } from "vue-router";
import CircleDiagram from "./CircleDiagram.vue";
import CircleOverview from "./CircleOverview.vue";
import DocumentSection from "./DocumentSection.vue";
import LearningSequence from "./LearningSequence.vue";
import { useCourseSessionDetailQuery } from "@/composables";
import {
useCourseSessionDetailQuery,
useCourseDataWithCompletion,
} from "@/composables";
import { stringifyParse } from "@/utils/utils";
import { useCircleStore } from "@/stores/circle";
import LearningSequence from "@/pages/learningPath/circlePage/LearningSequence.vue";
export interface Props {
courseSlug: string;
@ -28,84 +30,85 @@ const props = withDefaults(defineProps<Props>(), {
profileUser: undefined,
});
log.debug("CirclePage created", props.readonly, props.profileUser);
log.debug("CirclePage created", stringifyParse(props));
const lpQueryResult = useCourseDataWithCompletion(
props.courseSlug,
props.profileUser?.user_id
);
const circle = computed(() => {
return lpQueryResult.findCircle(props.circleSlug);
});
const circleExperts = computed(() => {
if (circleStore.circle) {
return courseSessionDetailResult.filterCircleExperts(circleStore.circle.slug);
if (circle.value) {
return courseSessionDetailResult.filterCircleExperts(circle.value.slug);
}
return [];
});
const duration = computed(() => {
if (circleStore.circle) {
const minutes = sumBy(circleStore.circle.learningSequences, "minutes");
return humanizeDuration(minutes);
}
// if (circleStore.circle) {
// const minutes = sumBy(circleStore.circle.learningSequences, "minutes");
// return humanizeDuration(minutes);
// }
return "";
});
const showDuration = computed(() => {
return (
circleStore.circle && sumBy(circleStore.circle.learningSequences, "minutes") > 0
);
// return (
// circleStore.circle && sumBy(circleStore.circle.learningSequences, "minutes") > 0
// );
return false;
});
onMounted(async () => {
log.debug(
"CirclePage mounted",
props.courseSlug,
props.circleSlug,
props.profileUser
);
watch(
() => circle.value,
() => {
if (circle.value) {
log.debug("circle loaded", circle);
try {
if (props.profileUser) {
await circleStore.loadCircle(
props.courseSlug,
props.circleSlug,
props.profileUser.user_id
);
} else {
await circleStore.loadCircle(props.courseSlug, props.circleSlug);
}
try {
if (route.hash.startsWith("#ls-") || route.hash.startsWith("#lu-")) {
const slugEnd = route.hash.replace("#", "");
if (route.hash.startsWith("#ls-") || route.hash.startsWith("#lu-")) {
const slugEnd = route.hash.replace("#", "");
let wagtailPage = null;
if (slugEnd.startsWith("ls-")) {
wagtailPage = circleStore.circle?.learningSequences.find((ls) => {
return ls.slug.endsWith(slugEnd);
});
} else if (slugEnd.startsWith("lu-")) {
const learningUnits = circleStore.circle?.learningSequences.flatMap(
(ls) => ls.learningUnits
);
if (learningUnits) {
wagtailPage = learningUnits.find((lu) => {
return lu.slug.endsWith(slugEnd);
});
if (circle.value) {
let wagtailPage = null;
if (slugEnd.startsWith("ls-")) {
wagtailPage = circle.value.learning_sequences.find((ls) => {
return ls.slug.endsWith(slugEnd);
});
} else if (slugEnd.startsWith("lu-")) {
const learningUnits = circle.value.learning_sequences.flatMap(
(ls) => ls.learning_units
);
if (learningUnits) {
wagtailPage = learningUnits.find((lu) => {
return lu.slug.endsWith(slugEnd);
});
}
}
if (wagtailPage) {
document
.getElementById(wagtailPage.slug)
?.scrollIntoView({ behavior: "smooth" });
}
}
}
}
if (wagtailPage) {
document
.getElementById(wagtailPage.slug)
?.scrollIntoView({ behavior: "smooth" });
} catch (error) {
log.error(error);
}
}
} catch (error) {
log.error(error);
}
});
);
</script>
<template>
<div>
<div v-if="circle">
<Teleport to="body">
<CircleOverview
:circle="circleStore.circle"
:circle="circle"
:show="circleStore.page === 'OVERVIEW'"
@closemodal="circleStore.page = 'INDEX'"
/>
@ -145,7 +148,7 @@ onMounted(async () => {
</router-link>
<h1 class="text-blue-dark text-4xl lg:text-6xl" data-cy="circle-title">
{{ circleStore.circle?.title }}
{{ circle?.title }}
</h1>
<div v-if="showDuration" class="mt-2">
@ -154,7 +157,7 @@ onMounted(async () => {
</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
@ -176,7 +179,7 @@ onMounted(async () => {
{{ $t("circlePage.circleContentBoxTitle") }}
</h3>
<div class="mt-4 leading-relaxed">
{{ circleStore.circle?.description }}
{{ circle?.description }}
</div>
<button
@ -186,13 +189,13 @@ onMounted(async () => {
{{ $t("circlePage.learnMore") }}
</button>
</div>
<DocumentSection v-if="!readonly" />
<DocumentSection v-if="!readonly" :circle="circle" />
<div v-if="!props.readonly" class="expert mt-8 border p-6">
<h3 class="text-blue-dark">{{ $t("circlePage.gotQuestions") }}</h3>
<div class="mt-4 leading-relaxed">
{{
$t("circlePage.contactExpertDescription", {
circleName: circleStore.circle?.title,
circleName: circle?.title,
})
}}
</div>
@ -220,11 +223,12 @@ onMounted(async () => {
<ol class="flex-auto bg-gray-200 px-4 py-8 lg:px-24">
<li
v-for="learningSequence in circleStore.circle?.learningSequences ||
[]"
:key="learningSequence.translation_key"
v-for="learningSequence in circle?.learning_sequences ?? []"
:key="learningSequence.id"
>
<LearningSequence
:course-slug="props.courseSlug"
:circle="circle"
:learning-sequence="learningSequence"
:readonly="props.readonly"
></LearningSequence>
@ -239,15 +243,6 @@ onMounted(async () => {
</template>
<style lang="postcss" scoped>
.circle-container {
/*background: linear-gradient(to right, white 0%, white 50%, theme(colors.gray.200) 50%, theme(colors.gray.200) 100%);*/
}
.circle {
/*max-width: 1440px;*/
/*margin: 0 auto;*/
}
.v-enter-active,
.v-leave-active {
transition: opacity 0.3s ease;

View File

@ -23,12 +23,14 @@
import DocumentListItem from "@/components/circle/DocumentListItem.vue";
import { useCurrentCourseSession } from "@/composables";
import { computed, onMounted, ref } from "vue";
import { useCircleStore } from "@/stores/circle";
import type { CircleDocument } from "@/types";
import type { CircleDocument, CircleType } from "@/types";
import { fetchCourseSessionDocuments } from "@/services/files";
const props = defineProps<{
circle: CircleType;
}>();
const courseSession = useCurrentCourseSession();
const circleStore = useCircleStore();
const circleDocumentsResultData = ref<CircleDocument[]>([]);
@ -43,7 +45,7 @@ async function fetchDocuments() {
const circleDocuments = computed(() => {
return circleDocumentsResultData.value.filter(
(d) => d.learning_sequence.circle.slug === circleStore.circle?.slug
(d) => d.learning_sequence.circle.slug === props.circle?.slug
);
});

View File

@ -1,26 +1,36 @@
<script setup lang="ts">
import ItCheckbox from "@/components/ui/ItCheckbox.vue";
import LearningContentBadge from "@/pages/learningPath/LearningContentTypeBadge.vue";
import { showIcon } from "@/pages/learningPath/circlePage/learningSequenceUtils";
import { useCircleStore } from "@/stores/circle";
import type {
CircleType,
CourseCompletionStatus,
LearningContent,
LearningContentAssignment,
LearningContentEdoniqTest,
LearningContentInterface,
LearningContentWithCompletion,
LearningSequence,
} from "@/types";
import findLast from "lodash/findLast";
import type { Ref } from "vue";
import { computed } from "vue";
import { humanizeDuration } from "../../../utils/humanizeDuration";
import {
itCheckboxDefaultIconCheckedTailwindClass,
itCheckboxDefaultIconUncheckedTailwindClass,
} from "@/constants";
import ItCheckbox from "@/components/ui/ItCheckbox.vue";
import {
allFinishedInLearningSequence,
calcSelfEvaluationStatus,
circleFlatLearningContents,
someFinishedInLearningSequence,
} from "@/services/circle";
import { useCourseDataWithCompletion } from "@/composables";
import { findLastIndex } from "lodash";
type Props = {
courseSlug: string;
learningSequence: LearningSequence;
circle: CircleType;
readonly?: boolean;
};
@ -30,65 +40,64 @@ const props = withDefaults(defineProps<Props>(), {
const circleStore = useCircleStore();
function toggleCompleted(learningContent: LearningContentInterface) {
const lpQueryResult = useCourseDataWithCompletion(props.courseSlug);
function toggleCompleted(learningContent: LearningContentWithCompletion) {
let completionStatus: CourseCompletionStatus = "SUCCESS";
if (learningContent.completion_status === "SUCCESS") {
completionStatus = "FAIL";
}
circleStore.markCompletion(learningContent, completionStatus);
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;
});
const continueTranslationKeyTuple = computed(() => {
if (props.learningSequence && circleStore.circle) {
const lastFinished = findLast(
circleStore.circle.flatLearningContents,
const continueTranslationKeyTuple: Ref<[string | undefined, boolean]> = computed(() => {
if (props.learningSequence) {
const flatLearningContents = circleFlatLearningContents(props.circle);
const lastFinishedIndex = findLastIndex(
circleFlatLearningContents(props.circle),
(learningContent) => {
return learningContent.completion_status === "SUCCESS";
}
);
if (!lastFinished) {
if (lastFinishedIndex === -1) {
// must be the first
return [circleStore.circle.flatLearningContents[0].translation_key, true];
return [flatLearningContents[0].id, true];
}
if (lastFinished && lastFinished.nextLearningContent) {
return [lastFinished.nextLearningContent.translation_key, false];
if (flatLearningContents[lastFinishedIndex + 1]) {
return [flatLearningContents[lastFinishedIndex + 1].id, false];
}
}
return "";
return [undefined, false];
});
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"];
}
}
@ -123,22 +132,27 @@ function checkboxIconUncheckedTailwindClass(lc: LearningContent) {
</script>
<template>
<div :id="learningSequence.slug" class="learning-sequence mb-8">
<div
:id="learningSequence.slug"
class="learning-sequence mb-8"
data-cy="lp-learning-sequence"
>
<div class="mb-2 flex items-center gap-4 text-blue-900">
<component :is="learningSequence.icon" v-if="showIcon(learningSequence.icon)" />
<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">
<li
v-for="learningUnit in learningSequence.learningUnits"
v-for="learningUnit in learningSequence.learning_units"
:id="learningUnit.slug"
:key="learningUnit.id"
data-cy="lp-learning-unit"
class="pt-3 lg:pt-6"
>
<div
@ -148,14 +162,15 @@ 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
v-for="learningContent in learningUnit.learningContents"
v-for="learningContent in learningUnit.learning_contents"
:key="learningContent.id"
data-cy="lp-learning-content"
>
<div class="pb-6">
<div class="flex items-center gap-4">
@ -181,15 +196,15 @@ function checkboxIconUncheckedTailwindClass(lc: LearningContent) {
"
@toggle="toggleCompleted(learningContent)"
@click="
(event: MouseEvent) => {
// when disabled open the learning content directly
if (!learningContent.can_user_self_toggle_course_completion) {
circleStore.openLearningContent(learningContent);
event.preventDefault();
event.stopPropagation();
}
}
"
(event: MouseEvent) => {
// when disabled open the learning content directly
if (!learningContent.can_user_self_toggle_course_completion) {
circleStore.openLearningContent(learningContent);
event.preventDefault();
event.stopPropagation();
}
}
"
/>
<div
class="flex flex-auto flex-col gap-4 xl:flex-row xl:justify-between"
@ -236,7 +251,7 @@ function checkboxIconUncheckedTailwindClass(lc: LearningContent) {
<div
v-if="
learningContent.translation_key === continueTranslationKeyTuple[0] &&
learningContent.id === continueTranslationKeyTuple[0] &&
!props.readonly
"
class="my-4"
@ -259,20 +274,20 @@ function checkboxIconUncheckedTailwindClass(lc: LearningContent) {
</ol>
<div
v-if="learningUnit.children.length"
v-if="learningUnit.performance_criteria.length"
:class="{ 'cursor-pointer': !props.readonly }"
:data-cy="`${learningUnit.slug}`"
@click="!props.readonly && circleStore.openSelfEvaluation(learningUnit)"
>
<div
v-if="circleStore.calcSelfEvaluationStatus(learningUnit) === 'SUCCESS'"
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="circleStore.calcSelfEvaluationStatus(learningUnit) === 'FAIL'"
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" />
@ -284,7 +299,7 @@ function checkboxIconUncheckedTailwindClass(lc: LearningContent) {
</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>
</ol>
</div>

View File

@ -1,12 +1,9 @@
<script setup lang="ts">
import LearningContentParent from "@/pages/learningPath/learningContentPage/LearningContentParent.vue";
import { useCircleStore } from "@/stores/circle";
import type { LearningContent } from "@/types";
import * as log from "loglevel";
import type { Ref } from "vue";
import { getCurrentInstance, onMounted, onUpdated, ref, watch } from "vue";
log.debug("LearningContentView created");
import { computed, getCurrentInstance, onUpdated } from "vue";
import { useCourseDataWithCompletion } from "@/composables";
import { stringifyParse } from "@/utils/utils";
const props = defineProps<{
courseSlug: string;
@ -14,43 +11,14 @@ const props = defineProps<{
contentSlug: string;
}>();
const learningContent: Ref<LearningContent | undefined> = ref(undefined);
log.debug("LearningContentView created", stringifyParse(props));
const circleStore = useCircleStore();
const loadLearningContent = async () => {
try {
learningContent.value = await circleStore.loadLearningContent(
props.courseSlug,
props.circleSlug,
props.contentSlug
);
} catch (error) {
log.error(error);
}
};
watch(
() => props.contentSlug,
async () => {
log.debug(
"LearningContentView props.contentSlug changed",
props.courseSlug,
props.circleSlug,
props.contentSlug
);
await loadLearningContent();
}
const courseData = useCourseDataWithCompletion(props.courseSlug);
const learningContent = computed(() =>
courseData.findLearningContent(props.contentSlug, props.circleSlug)
);
onMounted(async () => {
log.debug(
"LearningContentView mounted",
props.courseSlug,
props.circleSlug,
props.contentSlug
);
await loadLearningContent();
const circle = computed(() => {
return courseData.findCircle(props.circleSlug);
});
onUpdated(() => {
@ -73,7 +41,11 @@ onUpdated(() => {
</script>
<template>
<LearningContentParent v-if="learningContent" :learning-content="learningContent" />
<LearningContentParent
v-if="learningContent && circle"
:learning-content="learningContent"
:circle="circle"
/>
</template>
<style lang="postcss" scoped></style>

View File

@ -2,8 +2,12 @@
import LearningContentContainer from "@/pages/learningPath/learningContentPage/LearningContentContainer.vue";
import DocumentListBlock from "@/pages/learningPath/learningContentPage/blocks/DocumentListBlock.vue";
import EdoniqTestBlock from "@/pages/learningPath/learningContentPage/blocks/EdoniqTestBlock.vue";
import { useCircleStore } from "@/stores/circle";
import type { LearningContent, LearningContentType } from "@/types";
import type {
CircleType,
LearningContent,
LearningContentContentType,
LearningContentWithCompletion,
} from "@/types";
import eventBus from "@/utils/eventBus";
import log from "loglevel";
import type { Component } from "vue";
@ -17,19 +21,24 @@ import PlaceholderBlock from "./blocks/PlaceholderBlock.vue";
import RichTextBlock from "./blocks/RichTextBlock.vue";
import VideoBlock from "./blocks/VideoBlock.vue";
import { getPreviousRoute } from "@/router/history";
const circleStore = useCircleStore();
import { stringifyParse } from "@/utils/utils";
import { useCourseDataWithCompletion } from "@/composables";
import { useCircleStore } from "@/stores/circle";
const props = defineProps<{
learningContent: LearningContent;
circle: CircleType;
}>();
log.debug("LearningContentParent setup", props.learningContent);
log.debug("LearningContentParent setup", stringifyParse(props));
const courseCompletionData = useCourseDataWithCompletion();
const circleStore = useCircleStore();
const previousRoute = getPreviousRoute();
// can't use the type as component name, as some are reserved HTML components, e.g. video
const COMPONENTS: Record<LearningContentType, Component> = {
const COMPONENTS: Record<LearningContentContentType, Component> = {
"learnpath.LearningContentAssignment": AssignmentBlock,
"learnpath.LearningContentAttendanceCourse": AttendanceCourseBlock,
"learnpath.LearningContentDocumentList": DocumentListBlock,
@ -48,7 +57,14 @@ const component = computed(() => {
});
function handleFinishedLearningContent() {
circleStore.continueFromLearningContent(props.learningContent, previousRoute);
circleStore.continueFromLearningContent(
props.learningContent,
props.circle,
previousRoute,
(lc: LearningContentWithCompletion) => {
courseCompletionData.markCompletion(lc, "SUCCESS");
}
);
}
eventBus.on("finishedLearningContent", handleFinishedLearningContent);
@ -60,7 +76,9 @@ onUnmounted(() => {
<template>
<LearningContentContainer
@exit="circleStore.closeLearningContent(props.learningContent, previousRoute)"
@exit="
circleStore.closeLearningContent(props.learningContent, circle, previousRoute)
"
>
<div>
<component :is="component" :content="learningContent"></component>

View File

@ -7,7 +7,7 @@ import dayjs from "dayjs";
interface Props {
assignment: Assignment;
submissionDeadlineStart?: string;
submissionDeadlineStart?: string | null;
}
const props = withDefaults(defineProps<Props>(), {

View File

@ -3,7 +3,11 @@ import DateEmbedding from "@/components/dueDates/DateEmbedding.vue";
import ItButton from "@/components/ui/ItButton.vue";
import ItCheckbox from "@/components/ui/ItCheckbox.vue";
import ItSuccessAlert from "@/components/ui/ItSuccessAlert.vue";
import { useCourseSessionDetailQuery, useCurrentCourseSession } from "@/composables";
import {
useCourseSessionDetailQuery,
useCurrentCourseSession,
useCourseData,
} from "@/composables";
import { bustItGetCache } from "@/fetchHelpers";
import { UPSERT_ASSIGNMENT_COMPLETION_MUTATION } from "@/graphql/mutations";
import AssignmentSubmissionResponses from "@/pages/learningPath/learningContentPage/assignment/AssignmentSubmissionResponses.vue";
@ -15,14 +19,13 @@ import { computed, reactive } from "vue";
import { useTranslation } from "i18next-vue";
import eventBus from "@/utils/eventBus";
import dayjs from "dayjs";
import { useCircleStore } from "@/stores/circle";
const props = defineProps<{
assignment: Assignment;
learningContentId: string;
assignmentCompletion?: AssignmentCompletion;
courseSessionId: string;
submissionDeadlineStart?: string;
submissionDeadlineStart?: string | null;
}>();
const emit = defineEmits<{
@ -31,7 +34,7 @@ const emit = defineEmits<{
const courseSession = useCurrentCourseSession();
const courseSessionDetailResult = useCourseSessionDetailQuery();
const circleStore = useCircleStore();
const courseData = useCourseData(courseSession.value.course.slug);
const { t } = useTranslation();
@ -40,9 +43,15 @@ const state = reactive({
confirmPerson: false,
});
const learningContent = computed(() => {
return courseData.findLearningContent(props.learningContentId);
});
const circleExperts = computed(() => {
if (circleStore.circle) {
return courseSessionDetailResult.filterCircleExperts(circleStore.circle.slug);
if (learningContent.value?.circle) {
return courseSessionDetailResult.filterCircleExperts(
learningContent.value?.circle.slug
);
}
return [];
});
@ -133,7 +142,7 @@ const onSubmit = async () => {
data-cy="confirm-submit-person"
@toggle="state.confirmPerson = !state.confirmPerson"
></ItCheckbox>
<div class="flex flex-row items-center pb-6 pl-[49px]">
<div v-if="circleExpert" class="flex flex-row items-center pb-6 pl-[49px]">
<img
alt="Notification icon"
class="mr-2 h-[45px] min-w-[45px] rounded-full"

View File

@ -34,7 +34,7 @@ const queryResult = useQuery({
query: ASSIGNMENT_COMPLETION_QUERY,
variables: {
courseSessionId: courseSession.value.id,
assignmentId: props.learningContent.content_assignment_id,
assignmentId: props.learningContent.content_assignment.id,
learningContentId: props.learningContent.id,
},
pause: true,
@ -95,7 +95,7 @@ watchEffect(() => {
onMounted(async () => {
log.debug(
"AssignmentView mounted",
props.learningContent.content_assignment_id,
props.learningContent.content_assignment.id,
props.learningContent
);
@ -122,7 +122,7 @@ const currentTask = computed(() => {
const initUpsertAssignmentCompletion = async () => {
try {
await upsertAssignmentCompletionMutation.executeMutation({
assignmentId: props.learningContent.content_assignment_id,
assignmentId: props.learningContent.content_assignment.id,
courseSessionId: courseSession.value.id,
learningContentId: props.learningContent.id,
completionDataString: JSON.stringify({}),
@ -234,7 +234,7 @@ const assignmentUser = computed(() => {
<AssignmentTaskView
v-else-if="currentTask"
:task="currentTask"
:assignment-id="props.learningContent.content_assignment_id"
:assignment-id="props.learningContent.content_assignment.id"
:assignment-completion="assignmentCompletion"
:learning-content-id="props.learningContent.id"
></AssignmentTaskView>

View File

@ -5,8 +5,8 @@
<p class="grid-in-value">
{{
formatDueDate(
props.attendanceCourse.due_date.start,
props.attendanceCourse.due_date.end
props.attendanceCourse.due_date?.start ?? "",
props.attendanceCourse.due_date?.end
)
}}
</p>
@ -28,11 +28,11 @@
<script setup lang="ts">
import { formatDueDate } from "@/components/dueDates/dueDatesUtils";
import type { CourseSessionAttendanceCourse } from "@/types";
import { computed } from "vue";
import type { CourseSessionAttendanceCourseObjectType } from "@/gql/graphql";
export interface Props {
attendanceCourse: CourseSessionAttendanceCourse;
attendanceCourse: CourseSessionAttendanceCourseObjectType;
}
const props = defineProps<Props>();

View File

@ -9,7 +9,7 @@ const props = defineProps<{
<template>
<AssignmentView
:assignment-id="props.content.content_assignment_id"
:assignment-id="props.content.content_assignment.id"
:learning-content="props.content"
/>
</template>

View File

@ -1,11 +1,30 @@
<script setup lang="ts">
import MediaLink from "@/components/mediaLibrary/MediaLink.vue";
import type { LearningContentDocumentList } from "@/types";
import type {
LearningContentDocumentList,
MediaLibraryContentBlockValue,
} from "@/types";
import LearningContentSimpleLayout from "../layouts/LearningContentSimpleLayout.vue";
import { onMounted, ref } from "vue";
import log from "loglevel";
import { itGetCached } from "@/fetchHelpers";
const props = defineProps<{
content: LearningContentDocumentList;
}>();
type BlockDocument = {
id: string;
value: MediaLibraryContentBlockValue;
};
const documents = ref<BlockDocument[]>([]);
onMounted(async () => {
log.debug("DocumentListBlock mounted");
const response = await itGetCached(`/api/course/page/${props.content.slug}/`);
documents.value = response.documents;
});
</script>
<template>
@ -19,7 +38,7 @@ const props = defineProps<{
<div>
<ul class="border-t">
<li
v-for="item in content.documents"
v-for="item in documents"
:key="item.id"
class="flex items-center justify-between border-b py-4"
>

View File

@ -11,10 +11,7 @@ import { ASSIGNMENT_COMPLETION_QUERY } from "@/graphql/queries";
import { useCourseSessionDetailQuery, useCurrentCourseSession } from "@/composables";
import dayjs from "dayjs";
import ItSuccessAlert from "@/components/ui/ItSuccessAlert.vue";
import {
formatDueDate,
getDateString,
} from "../../../../components/dueDates/dueDatesUtils";
import { formatDueDate, getDateString } from "@/components/dueDates/dueDatesUtils";
const { t } = useTranslation();
@ -33,7 +30,7 @@ const queryResult = useQuery({
query: ASSIGNMENT_COMPLETION_QUERY,
variables: {
courseSessionId: courseSession.value.id,
assignmentId: props.content.content_assignment_id,
assignmentId: props.content.content_assignment.id,
learningContentId: props.content.id,
},
});
@ -52,9 +49,12 @@ const extendedTimeTest = ref(false);
const deadlineInPast = computed(() => {
// with 16 minutes buffer
return dayjs(courseSessionEdoniqTest.value?.deadline.start)
.add(16, "minute")
.isBefore(dayjs());
if (courseSessionEdoniqTest.value?.deadline?.start) {
return dayjs(courseSessionEdoniqTest.value?.deadline.start)
.add(16, "minute")
.isBefore(dayjs());
}
return false;
});
async function startTest() {
@ -89,7 +89,7 @@ async function startTest() {
<p class="mt-2 text-lg">
{{
$t("edoniqTest.submitDateDescription", {
x: formatDueDate(courseSessionEdoniqTest.deadline.start),
x: formatDueDate(courseSessionEdoniqTest?.deadline?.start ?? ""),
})
}}
</p>
@ -157,7 +157,7 @@ async function startTest() {
</div>
<div v-else>
{{ $t("a.Abgabetermin") }}:
{{ getDateString(dayjs(courseSessionEdoniqTest?.deadline.start)) }}
{{ getDateString(dayjs(courseSessionEdoniqTest?.deadline?.start)) }}
</div>
</div>
</div>

View File

@ -9,7 +9,6 @@ import {
YES_NO,
} from "@/pages/learningPath/learningContentPage/feedback/feedback.constants";
import LearningContentMultiLayout from "@/pages/learningPath/learningContentPage/layouts/LearningContentMultiLayout.vue";
import { useCircleStore } from "@/stores/circle";
import type { LearningContentFeedback } from "@/types";
import { useMutation } from "@urql/vue";
import { useRouteQuery } from "@vueuse/router";
@ -22,7 +21,6 @@ const props = defineProps<{
content: LearningContentFeedback;
}>();
const courseSession = useCurrentCourseSession();
const circleStore = useCircleStore();
const courseSessionDetailResult = useCourseSessionDetailQuery();
const { t } = useTranslation();
@ -30,12 +28,12 @@ const { t } = useTranslation();
const stepNo = useRouteQuery("step", "0", { transform: Number, mode: "push" });
const title = computed(
() => `«${circleStore.circle?.title}»: ${t("feedback.areYouSatisfied")}`
() => `«${props.content.circle?.title}»: ${t("feedback.areYouSatisfied")}`
);
const circleExperts = computed(() => {
if (circleStore.circle) {
return courseSessionDetailResult.filterCircleExperts(circleStore.circle.slug);
if (props.content?.circle?.slug) {
return courseSessionDetailResult.filterCircleExperts(props.content.circle.slug);
}
return [];
});

View File

@ -2,41 +2,51 @@
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 { LearningPath } from "@/services/learningPath";
import type { Topic } from "@/types";
import { onMounted, ref } from "vue";
import type { CircleType, LearningContentWithCompletion } from "@/types";
import { computed, ref, watch } from "vue";
const props = defineProps<{
learningPath: LearningPath | undefined;
circle: Circle;
topic: Topic;
circle: CircleType;
nextLearningContent: LearningContentWithCompletion | undefined;
isFirstCircle: boolean;
isLastCircle: boolean;
isCurrentCircle: boolean;
overrideCircleUrl?: string;
}>();
const circleElement = ref<HTMLElement | null>(null);
onMounted(() => {
if (props.isCurrentCircle) {
const isCurrentCircle = computed(() => {
return props.nextLearningContent?.circle?.id === props.circle.id;
});
function scrollToCircle() {
if (isCurrentCircle.value) {
setTimeout(() => {
circleElement?.value?.scrollIntoView({
behavior: "smooth",
inline: "center",
block: "nearest",
});
}, 400);
});
}
});
}
watch(
() => isCurrentCircle.value,
(isCurrent) => {
if (isCurrent) {
scrollToCircle();
}
},
{ immediate: true }
);
</script>
<template>
<router-link
:to="overrideCircleUrl ? overrideCircleUrl : props.circle.frontend_url"
:data-cy="`circle-${props.circle.title}`"
class="flex flex-col items-center pb-6"
class="cy-lp-circle flex flex-col items-center pb-6"
>
<div ref="circleElement" class="flex flex-row items-center pb-2">
<div class="w-12">
@ -55,10 +65,9 @@ onMounted(() => {
{{ props.circle.title }}
</div>
<div v-if="props.isCurrentCircle" class="whitespace-nowrap">
<div v-if="isCurrentCircle" class="whitespace-nowrap">
<LearningPathContinueButton
:has-progress="!props.learningPath?.continueData?.has_no_progress"
:url="props.learningPath?.continueData?.url"
:next-learning-content="props.nextLearningContent"
></LearningPathContinueButton>
</div>
</router-link>

View File

@ -2,29 +2,35 @@
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 { LearningPath } from "@/services/learningPath";
import { onMounted, ref } from "vue";
import { computed, ref, watch } from "vue";
import type { CircleType, LearningContentWithCompletion } from "@/types";
const props = defineProps<{
learningPath: LearningPath | undefined;
circle: Circle;
isCurrentCircle: boolean;
circle: CircleType;
nextLearningContent: LearningContentWithCompletion | undefined;
}>();
const circleElement = ref<HTMLElement | null>(null);
onMounted(() => {
if (props.isCurrentCircle) {
setTimeout(() => {
circleElement?.value?.scrollIntoView({
behavior: "smooth",
inline: "nearest",
block: "center",
});
}, 400);
}
const isCurrentCircle = computed(() => {
return props.nextLearningContent?.circle?.id === props.circle.id;
});
watch(
() => isCurrentCircle.value,
(isCurrent) => {
if (isCurrent) {
setTimeout(() => {
circleElement?.value?.scrollIntoView({
behavior: "smooth",
inline: "center",
block: "nearest",
});
});
}
},
{ immediate: true }
);
</script>
<template>
@ -43,8 +49,7 @@ onMounted(() => {
<div v-if="isCurrentCircle" class="whitespace-nowrap pl-4">
<LearningPathContinueButton
:has-progress="!props.learningPath?.continueData?.has_no_progress"
:url="props.learningPath?.continueData?.url"
:next-learning-content="props.nextLearningContent"
></LearningPathContinueButton>
</div>
</router-link>

View File

@ -1,24 +1,24 @@
<script setup lang="ts">
interface Props {
hasProgress?: boolean;
url?: string;
}
import type { LearningContentWithCompletion } from "@/types";
const props = withDefaults(defineProps<Props>(), {
hasProgress: false,
url: "",
});
const props = defineProps<{
nextLearningContent: LearningContentWithCompletion | undefined;
}>();
</script>
<template>
<router-link
class="btn-blue mt-2 pl-6"
:to="props.url"
:to="nextLearningContent?.continueUrl ?? '/'"
data-cy="lp-continue-button"
translate
>
<span>
{{ props.hasProgress ? $t("general.nextStep") : $t("general.start") }}
{{
props.nextLearningContent?.firstInCircle
? $t("general.start")
: $t("general.nextStep")
}}
</span>
</router-link>
</template>

View File

@ -1,17 +1,14 @@
<script setup lang="ts">
import LearningPathCircleListTile from "@/pages/learningPath/learningPathPage/LearningPathCircleListTile.vue";
import type { Circle } from "@/services/circle";
import type { LearningPath } from "@/services/learningPath";
import { computed } from "vue";
import type { LearningContentWithCompletion, LearningPathType } from "@/types";
const props = defineProps<{
learningPath: LearningPath | undefined;
learningPath: LearningPathType | undefined;
nextLearningContent: LearningContentWithCompletion | undefined;
}>();
const topics = computed(() => props.learningPath?.topics ?? []);
const isCurrentCircle = (circle: Circle) =>
props.learningPath?.nextLearningContent?.parentCircle === circle;
</script>
<template>
@ -22,9 +19,8 @@ const isCurrentCircle = (circle: Circle) =>
<LearningPathCircleListTile
v-for="circle in topic.circles"
:key="circle.id"
:learning-path="learningPath"
:circle="circle"
:is-current-circle="isCurrentCircle(circle)"
:next-learning-content="props.nextLearningContent"
></LearningPathCircleListTile>
</div>
</template>

View File

@ -6,19 +6,16 @@ import CircleProgress from "@/pages/learningPath/learningPathPage/LearningPathPr
import LearningPathTopics from "@/pages/learningPath/learningPathPage/LearningPathTopics.vue";
import type { ViewType } from "@/pages/learningPath/learningPathPage/LearningPathViewSwitch.vue";
import LearningPathViewSwitch from "@/pages/learningPath/learningPathPage/LearningPathViewSwitch.vue";
import { useLearningPathStore } from "@/stores/learningPath";
import { useUserStore } from "@/stores/user";
import { breakpointsTailwind, useBreakpoints } from "@vueuse/core";
import * as log from "loglevel";
import { computed, onMounted, ref } from "vue";
import { computed, ref } from "vue";
import { useCourseDataWithCompletion } from "@/composables";
import { someFinishedInLearningSequence } from "@/services/circle";
const props = defineProps<{
courseSlug: string;
}>();
const breakpoints = useBreakpoints(breakpointsTailwind);
const learningPathStore = useLearningPathStore();
const userStore = useUserStore();
// Layout state
const useMobileLayout = breakpoints.smaller("sm");
@ -26,38 +23,21 @@ const selectedView = ref<ViewType>(
(window.localStorage.getItem("learningPathView") as ViewType) || "path"
);
onMounted(async () => {
log.debug("LearningPathPage mounted");
const lpQueryResult = useCourseDataWithCompletion(props.courseSlug);
try {
await learningPathStore.loadLearningPath(props.courseSlug + "-lp");
} catch (error) {
log.error(error);
}
});
const learningPath = computed(() => {
if (userStore.loggedIn && learningPathStore.state.learningPaths.size > 0) {
const learningPathKey = `${props.courseSlug}-lp-${userStore.id}`;
return learningPathStore.state.learningPaths.get(learningPathKey);
}
return undefined;
});
const learningPath = computed(() => lpQueryResult.learningPath.value);
const course = computed(() => lpQueryResult.course.value);
const circlesCount = computed(() => {
if (learningPath.value) {
return learningPath.value.circles.length;
}
return 0;
return lpQueryResult.circles.value?.length ?? 0;
});
const inProgressCirclesCount = computed(() => {
if (learningPath.value) {
return learningPath.value.circles.filter(
if (lpQueryResult.circles.value?.length) {
return lpQueryResult.circles.value.filter(
(circle) =>
circle.learningSequences.filter((ls) =>
circle.someFinishedInLearningSequence(ls.translation_key)
).length
circle.learning_sequences.filter((ls) => someFinishedInLearningSequence(ls))
.length
).length;
}
return 0;
@ -80,7 +60,7 @@ const changeViewType = (viewType: ViewType) => {
{{ $t("learningPathPage.welcomeBack") }}
</p>
<h2 data-cy="learning-path-title">
{{ learningPath?.title }}
{{ course?.title }}
</h2>
</div>
@ -101,46 +81,52 @@ const changeViewType = (viewType: ViewType) => {
<!-- Bottom -->
<div class="bg-white">
<div class="flex flex-col justify-between px-6 sm:flex-row sm:px-12">
<!-- Topics -->
<div
v-if="selectedView == 'path'"
class="order-2 pb-8 sm:order-1 sm:pb-0 sm:pt-4"
>
<LearningPathTopics :topics="learningPath?.topics ?? []"></LearningPathTopics>
<div v-if="lpQueryResult.learningPath">
<div class="flex flex-col justify-between px-6 sm:flex-row sm:px-12">
<!-- Topics -->
<div
v-if="selectedView == 'path'"
class="order-2 pb-8 sm:order-1 sm:pb-0 sm:pt-4"
>
<LearningPathTopics
:topics="learningPath?.topics ?? []"
></LearningPathTopics>
</div>
<div v-else class="flex-grow"></div>
<!-- View switch -->
<LearningPathViewSwitch
class="order-1 py-8 sm:order-2 sm:py-0 sm:pt-4"
:initial-view="selectedView"
@select-view="changeViewType($event)"
></LearningPathViewSwitch>
</div>
<div v-else class="flex-grow"></div>
<!-- View switch -->
<LearningPathViewSwitch
class="order-1 py-8 sm:order-2 sm:py-0 sm:pt-4"
:initial-view="selectedView"
@select-view="changeViewType($event)"
></LearningPathViewSwitch>
</div>
<!-- Path view -->
<div v-if="selectedView == 'path'" class="flex flex-col" data-cy="lp-path-view">
<LearningPathPathView
:learning-path="learningPath"
:use-mobile-layout="useMobileLayout"
:next-learning-content="lpQueryResult.nextLearningContent.value"
></LearningPathPathView>
</div>
<!-- Path view -->
<div v-if="selectedView == 'path'" class="flex flex-col" data-cy="lp-path-view">
<LearningPathPathView
:learning-path="learningPath"
:use-mobile-layout="useMobileLayout"
></LearningPathPathView>
</div>
<!-- List view -->
<div
v-if="selectedView == 'list'"
class="flex flex-col pl-6 sm:pl-24"
data-cy="lp-list-view"
>
<LearningPathListView :learning-path="learningPath"></LearningPathListView>
</div>
<div
v-if="useMobileLayout"
class="p-6"
:class="useMobileLayout ? 'bg-gray-200' : ''"
>
<!--<LearningPathAppointmentsMock></LearningPathAppointmentsMock>-->
<!-- List view -->
<div
v-if="selectedView == 'list'"
class="flex flex-col pl-6 sm:pl-24"
data-cy="lp-list-view"
>
<LearningPathListView
:learning-path="learningPath"
:next-learning-content="lpQueryResult.nextLearningContent.value"
></LearningPathListView>
</div>
<div
v-if="useMobileLayout"
class="p-6"
:class="useMobileLayout ? 'bg-gray-200' : ''"
></div>
</div>
</div>
</div>

View File

@ -1,13 +1,13 @@
<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 { LearningPath } from "@/services/learningPath";
import { useScroll } from "@vueuse/core";
import { ref } from "vue";
import type { LearningContentWithCompletion, LearningPathType } from "@/types";
const props = defineProps<{
learningPath: LearningPath | undefined;
learningPath: LearningPathType | undefined;
nextLearningContent: LearningContentWithCompletion | undefined;
useMobileLayout: boolean;
hideButtons?: boolean;
overrideCircleUrlBase?: string;
@ -25,9 +25,6 @@ const isLastCircle = (topicIndex: number, circleIndex: number, numCircles: numbe
topicIndex === (props.learningPath?.topics ?? []).length - 1 &&
circleIndex === numCircles - 1;
const isCurrentCircle = (circle: Circle) =>
props.learningPath?.nextLearningContent?.parentCircle === circle;
const scrollRight = () => scrollLearnPathDiagram(scrollIncrement);
const scrollLeft = () => scrollLearnPathDiagram(-scrollIncrement);
@ -58,6 +55,7 @@ const scrollLearnPathDiagram = (offset: number) => {
<p
:id="`topic-${topic.slug}`"
class="inline-block h-12 self-start px-4 font-bold text-gray-800"
data-cy="lp-topic"
>
{{ topic.title }}
</p>
@ -65,14 +63,12 @@ const scrollLearnPathDiagram = (offset: number) => {
<LearningPathCircleColumn
v-for="(circle, circleIndex) in topic.circles"
:key="circle.id"
:learning-path="learningPath"
:circle="circle"
:topic="topic"
:next-learning-content="props.nextLearningContent"
:is-first-circle="isFirstCircle(topicIndex, circleIndex)"
:is-last-circle="
isLastCircle(topicIndex, circleIndex, topic.circles.length)
"
:is-current-circle="isCurrentCircle(circle) && !props.hideButtons"
:override-circle-url="
props.overrideCircleUrlBase
? `${props.overrideCircleUrlBase}/${circle.slug}`

View File

@ -1,11 +1,11 @@
<script setup lang="ts">
import type { Topic } from "@/types";
import type { TopicType } from "@/types";
const props = defineProps<{
topics: Topic[];
topics: TopicType[];
}>();
const scrollToTopic = (topic: Topic) => {
const scrollToTopic = (topic: TopicType) => {
const id = `topic-${topic.slug}`;
const el = document.getElementById(id);
el?.scrollIntoView({ behavior: "smooth", inline: "center", block: "nearest" });

View File

@ -2,14 +2,18 @@ import type {
CircleSectorData,
CircleSectorProgress,
} from "@/pages/learningPath/learningPathPage/LearningPathCircle.vue";
import type { Circle } from "@/services/circle";
import {
allFinishedInLearningSequence,
someFinishedInLearningSequence,
} from "@/services/circle";
import type { CircleType } from "@/types";
export function calculateCircleSectorData(circle: Circle): CircleSectorData[] {
return circle.learningSequences.map((ls) => {
export function calculateCircleSectorData(circle: CircleType): CircleSectorData[] {
return circle.learning_sequences.map((ls) => {
let progress: CircleSectorProgress = "none";
if (circle.allFinishedInLearningSequence(ls.translation_key)) {
if (allFinishedInLearningSequence(ls)) {
progress = "finished";
} else if (circle.someFinishedInLearningSequence(ls.translation_key)) {
} else if (someFinishedInLearningSequence(ls)) {
progress = "in_progress";
}
return {

View File

@ -1,10 +1,9 @@
<script setup lang="ts">
import { useCircleStore } from "@/stores/circle";
import type { LearningUnit } from "@/types";
import type { CircleType, LearningUnit } from "@/types";
import * as log from "loglevel";
import { useCurrentCourseSession } from "@/composables";
import { COMPLETION_FAILURE, COMPLETION_SUCCESS } from "@/constants";
import { useCurrentCourseSession, useCourseDataWithCompletion } from "@/composables";
import LearningContentContainer from "@/pages/learningPath/learningContentPage/LearningContentContainer.vue";
import LearningContentMultiLayout from "@/pages/learningPath/learningContentPage/layouts/LearningContentMultiLayout.vue";
import eventBus from "@/utils/eventBus";
@ -17,16 +16,17 @@ log.debug("LearningContent.vue setup");
const circleStore = useCircleStore();
const courseSession = useCurrentCourseSession();
const courseCompletionData = useCourseDataWithCompletion();
const questionIndex = useRouteQuery("step", "0", { transform: Number, mode: "push" });
const previousRoute = getPreviousRoute();
const props = defineProps<{
learningUnit: LearningUnit;
circle: CircleType;
}>();
const questions = computed(() => props.learningUnit?.children);
const questions = computed(() => props.learningUnit?.performance_criteria ?? []);
const currentQuestion = computed(() => questions.value[questionIndex.value]);
const showPreviousButton = computed(() => questionIndex.value != 0);
const showNextButton = computed(
@ -44,7 +44,7 @@ function handleContinue() {
questionIndex.value += 1;
} else {
log.debug("continue to next learning content");
circleStore.continueFromSelfEvaluation(props.learningUnit);
circleStore.continueFromSelfEvaluation(props.learningUnit, props.circle);
}
}
@ -56,7 +56,7 @@ function handleBack() {
}
function handleFinishedLearningContent() {
circleStore.closeSelfEvaluation(props.learningUnit, previousRoute);
circleStore.closeSelfEvaluation(props.learningUnit, props.circle, previousRoute);
}
eventBus.on("finishedLearningContent", handleFinishedLearningContent);
@ -69,7 +69,9 @@ onUnmounted(() => {
<template>
<div v-if="learningUnit">
<LearningContentContainer
@exit="circleStore.closeSelfEvaluation(props.learningUnit, previousRoute)"
@exit="
circleStore.closeSelfEvaluation(props.learningUnit, props.circle, previousRoute)
"
>
<LearningContentMultiLayout
:current-step="questionIndex"
@ -101,7 +103,7 @@ onUnmounted(() => {
'border-2': currentQuestion.completion_status === 'SUCCESS',
}"
data-cy="success"
@click="circleStore.markCompletion(currentQuestion, COMPLETION_SUCCESS)"
@click="courseCompletionData.markCompletion(currentQuestion, 'SUCCESS')"
>
<it-icon-smiley-happy class="mr-4 h-16 w-16"></it-icon-smiley-happy>
<span class="text-large font-bold">
@ -111,12 +113,11 @@ onUnmounted(() => {
<button
class="inline-flex flex-1 items-center border p-4 text-left"
:class="{
'border-orange-500':
currentQuestion.completion_status === COMPLETION_FAILURE,
'border-2': currentQuestion.completion_status === COMPLETION_FAILURE,
'border-orange-500': currentQuestion.completion_status === 'FAIL',
'border-2': currentQuestion.completion_status === 'FAIL',
}"
data-cy="fail"
@click="circleStore.markCompletion(currentQuestion, 'FAIL')"
@click="courseCompletionData.markCompletion(currentQuestion, 'FAIL')"
>
<it-icon-smiley-thinking
class="mr-4 h-16 w-16"

View File

@ -2,9 +2,8 @@
import * as log from "loglevel";
import SelfEvaluation from "@/pages/learningPath/selfEvaluationPage/SelfEvaluation.vue";
import { useCircleStore } from "@/stores/circle";
import type { LearningUnit } from "@/types";
import { onMounted, reactive } from "vue";
import { computed } from "vue";
import { useCourseDataWithCompletion } from "@/composables";
log.debug("LearningUnitSelfEvaluationView created");
@ -14,32 +13,21 @@ const props = defineProps<{
learningUnitSlug: string;
}>();
const circleStore = useCircleStore();
const state: { learningUnit?: LearningUnit } = reactive({});
onMounted(async () => {
log.debug(
"LearningUnitSelfEvaluationView mounted",
props.courseSlug,
props.circleSlug,
props.learningUnitSlug
);
try {
state.learningUnit = await circleStore.loadSelfEvaluation(
props.courseSlug,
props.circleSlug,
props.learningUnitSlug
);
} catch (error) {
log.error(error);
}
const courseData = useCourseDataWithCompletion(props.courseSlug);
const learningUnit = computed(() =>
courseData.findLearningUnit(props.learningUnitSlug, props.circleSlug)
);
const circle = computed(() => {
return courseData.findCircle(props.circleSlug);
});
</script>
<template>
<SelfEvaluation v-if="state.learningUnit" :learning-unit="state.learningUnit" />
<SelfEvaluation
v-if="learningUnit && circle"
:learning-unit="learningUnit"
:circle="circle"
/>
</template>
<style lang="postcss" scoped></style>

View File

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

View File

@ -1,15 +0,0 @@
import { describe, it } from "vitest";
import { LearningPath } from "../learningPath";
import data from "./learning_path_json.json";
describe("LearningPath.parseJson", () => {
it("can parse learning sequences from api response", () => {
const learningPath = LearningPath.fromJson(data, [], undefined);
expect(learningPath.circles.length).toBe(2);
expect(learningPath.circles[0].title).toBe("Fahrzeug");
expect(learningPath.circles[1].title).toBe("Reisen");
expect(learningPath.topics.length).toBe(2);
});
});

View File

@ -1,447 +0,0 @@
{
"id": 10,
"title": "Test Lernpfad",
"slug": "test-lehrgang-lp",
"content_type": "learnpath.LearningPath",
"translation_key": "9cf4fea4-9d6f-4297-ab99-68a65bf07bb5",
"frontend_url": "/course/test-lehrgang/learn",
"children": [
{
"id": 11,
"title": "Circle \u00dcK",
"slug": "test-lehrgang-lp-topic-circle-\u00fck",
"content_type": "learnpath.Topic",
"translation_key": "983f97f7-fd68-4678-860f-7a19bab0b94d",
"frontend_url": "",
"is_visible": false
},
{
"id": 12,
"title": "Fahrzeug",
"slug": "test-lehrgang-lp-circle-fahrzeug",
"content_type": "learnpath.Circle",
"translation_key": "0286b096-2a55-4242-a277-ba15d478b79a",
"frontend_url": "/course/test-lehrgang/learn/fahrzeug",
"children": [
{
"id": 13,
"title": "Vorbereitung",
"slug": "test-lehrgang-lp-circle-fahrzeug-ls-vorbereitung",
"content_type": "learnpath.LearningSequence",
"translation_key": "dbc0e05f-a899-4524-b021-39a97ac1c542",
"frontend_url": "/course/test-lehrgang/learn/fahrzeug#ls-vorbereitung",
"icon": "it-icon-ls-start"
},
{
"id": 14,
"title": "Vorbereitung",
"slug": "test-lehrgang-lp-circle-fahrzeug-lu-vorbereitung",
"content_type": "learnpath.LearningUnit",
"translation_key": "626d656a-15d6-49ce-8b20-c035482802cd",
"frontend_url": "/course/test-lehrgang/learn/fahrzeug#lu-vorbereitung",
"evaluate_url": "/course/test-lehrgang/learn/fahrzeug/evaluate/vorbereitung",
"course_category": {
"id": 1,
"title": "Allgemein",
"general": true
},
"children": [
{
"id": 18,
"title": "Innerhalb des Handlungsfelds \u00abFahrzeug\u00bb bin ich f\u00e4hig, die Ziele und Pl\u00e4ne des Kunden zu ergr\u00fcnden (SOLL).",
"slug": "test-lehrgang-competence-crit-x11-allgemein",
"content_type": "competence.PerformanceCriteria",
"translation_key": "d49be54d-51e5-4bf4-9238-365006c3b95d",
"frontend_url": "",
"competence_id": "X1.1"
},
{
"id": 19,
"title": "Innerhalb des Handlungsfelds \u00abFahrzeug\u00bb bin ich f\u00e4hig, die IST-Situation des Kunden mit der geeigneten Gespr\u00e4chs-/Fragetechnik zu erfassen.",
"slug": "test-lehrgang-competence-crit-x11-allgemein-1",
"content_type": "competence.PerformanceCriteria",
"translation_key": "2fb68d58-3ab7-4192-865c-1e67ab9bcd15",
"frontend_url": "",
"competence_id": "X1.1"
}
]
},
{
"id": 15,
"title": "Verschaffe dir einen \u00dcberblick",
"slug": "test-lehrgang-lp-circle-fahrzeug-lc-verschaffe-dir-einen-\u00fcberblick",
"content_type": "learnpath.LearningContentPlaceholder",
"translation_key": "47698ce1-0e4f-446d-a23d-8a9e9c906ff7",
"frontend_url": "/course/test-lehrgang/learn/fahrzeug/verschaffe-dir-einen-\u00fcberblick",
"minutes": 15,
"description": "Platzhalter",
"content_url": ""
},
{
"id": 16,
"title": "Mediathek Fahrzeug",
"slug": "test-lehrgang-lp-circle-fahrzeug-lc-mediathek-fahrzeug",
"content_type": "learnpath.LearningContentMediaLibrary",
"translation_key": "34e79a3b-c1f9-49ff-b779-0149d614f02c",
"frontend_url": "/course/test-lehrgang/learn/fahrzeug/mediathek-fahrzeug",
"minutes": 15,
"description": "",
"content_url": "/media/\u00fcberbetriebliche-kurse-media/category/fahrzeug"
},
{
"id": 17,
"title": "Vorbereitungsauftrag",
"slug": "test-lehrgang-lp-circle-fahrzeug-lc-vorbereitungsauftrag",
"content_type": "learnpath.LearningContentPlaceholder",
"translation_key": "8feca9cd-4937-4406-b44d-564f341e8bfe",
"frontend_url": "/course/test-lehrgang/learn/fahrzeug/vorbereitungsauftrag",
"minutes": 15,
"description": "Platzhalter",
"content_url": ""
},
{
"id": 20,
"title": "Training",
"slug": "test-lehrgang-lp-circle-fahrzeug-ls-training",
"content_type": "learnpath.LearningSequence",
"translation_key": "b09f87c7-01fb-4967-98c1-894ac3144595",
"frontend_url": "/course/test-lehrgang/learn/fahrzeug#ls-training",
"icon": "it-icon-ls-apply"
},
{
"id": 21,
"title": "Unterlagen",
"slug": "test-lehrgang-lp-circle-fahrzeug-lu-unterlagen",
"content_type": "learnpath.LearningUnit",
"translation_key": "772d5352-87fa-46a7-8470-368d59565d3a",
"frontend_url": "/course/test-lehrgang/learn/fahrzeug#lu-unterlagen",
"evaluate_url": "/course/test-lehrgang/learn/fahrzeug/evaluate/unterlagen",
"course_category": {
"id": 1,
"title": "Allgemein",
"general": true
},
"children": []
},
{
"id": 22,
"title": "Unterlagen f\u00fcr den Unterricht",
"slug": "test-lehrgang-lp-circle-fahrzeug-lc-unterlagen-f\u00fcr-den-unterricht",
"content_type": "learnpath.LearningContentPlaceholder",
"translation_key": "ace9f1e8-5cb7-4b7c-b1c8-d43f2e4f7269",
"frontend_url": "/course/test-lehrgang/learn/fahrzeug/unterlagen-f\u00fcr-den-unterricht",
"minutes": 15,
"description": "Platzhalter",
"content_url": ""
},
{
"id": 23,
"title": "Pr\u00e4senztag",
"slug": "test-lehrgang-lp-circle-fahrzeug-lu-pr\u00e4senztag",
"content_type": "learnpath.LearningUnit",
"translation_key": "18bc5d1d-ddcf-4e54-b58c-58f1e8833af2",
"frontend_url": "/course/test-lehrgang/learn/fahrzeug#lu-pr\u00e4senztag",
"evaluate_url": "/course/test-lehrgang/learn/fahrzeug/evaluate/pr\u00e4senztag",
"course_category": {
"id": 1,
"title": "Allgemein",
"general": true
},
"children": []
},
{
"id": 24,
"title": "Pr\u00e4senztag Fahrzeug",
"slug": "test-lehrgang-lp-circle-fahrzeug-lc-pr\u00e4senztag-fahrzeug",
"content_type": "learnpath.LearningContentAttendanceCourse",
"translation_key": "2441afae-83ea-4fb5-a938-8db4352ed6c5",
"frontend_url": "/course/test-lehrgang/learn/fahrzeug/pr\u00e4senztag-fahrzeug",
"minutes": 15,
"description": "Platzhalter Beschreibung",
"content_url": ""
},
{
"id": 25,
"title": "Kompetenznachweis",
"slug": "test-lehrgang-lp-circle-fahrzeug-lu-kompetenznachweis",
"content_type": "learnpath.LearningUnit",
"translation_key": "b115d4e0-f487-4d03-a7cf-08d90bb4813d",
"frontend_url": "/course/test-lehrgang/learn/fahrzeug#lu-kompetenznachweis",
"evaluate_url": "/course/test-lehrgang/learn/fahrzeug/evaluate/kompetenznachweis",
"course_category": {
"id": 1,
"title": "Allgemein",
"general": true
},
"children": []
},
{
"id": 26,
"title": "Wissens- und Verst\u00e4ndnisfragen",
"slug": "test-lehrgang-lp-circle-fahrzeug-lc-wissens-und-verst\u00e4ndnisfragen",
"content_type": "learnpath.LearningContentPlaceholder",
"translation_key": "053c32bd-6174-444b-95fe-35ad2e15edf5",
"frontend_url": "/course/test-lehrgang/learn/fahrzeug/wissens-und-verst\u00e4ndnisfragen",
"minutes": 15,
"description": "Platzhalter",
"content_url": ""
},
{
"id": 27,
"title": "Transfer",
"slug": "test-lehrgang-lp-circle-fahrzeug-ls-transfer",
"content_type": "learnpath.LearningSequence",
"translation_key": "58939dc7-dd19-4996-b4bf-aba348be092a",
"frontend_url": "/course/test-lehrgang/learn/fahrzeug#ls-transfer",
"icon": "it-icon-ls-end"
},
{
"id": 28,
"title": "Transfer",
"slug": "test-lehrgang-lp-circle-fahrzeug-lu-transfer",
"content_type": "learnpath.LearningUnit",
"translation_key": "185568d3-9ba3-433d-9480-4f492d9d3235",
"frontend_url": "/course/test-lehrgang/learn/fahrzeug#lu-transfer",
"evaluate_url": "/course/test-lehrgang/learn/fahrzeug/evaluate/transfer",
"course_category": {
"id": 1,
"title": "Allgemein",
"general": true
},
"children": []
},
{
"id": 29,
"title": "Reflexion",
"slug": "test-lehrgang-lp-circle-fahrzeug-lc-reflexion",
"content_type": "learnpath.LearningContentPlaceholder",
"translation_key": "c62d4cf6-2505-40f7-8764-41fa1ea0057c",
"frontend_url": "/course/test-lehrgang/learn/fahrzeug/reflexion",
"minutes": 15,
"description": "Platzhalter",
"content_url": ""
},
{
"id": 30,
"title": "\u00dcberpr\u00fcfen einer Motorfahrzeug-Versicherungspolice",
"slug": "test-lehrgang-lp-circle-fahrzeug-lc-\u00fcberpr\u00fcfen-einer-motorfahrzeug-versicherungspolice",
"content_type": "learnpath.LearningContentAssignment",
"translation_key": "53cc2b76-ea59-47a2-a15a-ebf19897e9b1",
"frontend_url": "/course/test-lehrgang/learn/fahrzeug/\u00fcberpr\u00fcfen-einer-motorfahrzeug-versicherungspolice",
"minutes": 15,
"description": "",
"content_url": "",
"content_assignment_id": 9
},
{
"id": 31,
"title": "Feedback",
"slug": "test-lehrgang-lp-circle-fahrzeug-lc-feedback",
"content_type": "learnpath.LearningContentFeedback",
"translation_key": "d78bded2-a760-492c-9249-283230d98ce0",
"frontend_url": "/course/test-lehrgang/learn/fahrzeug/feedback",
"minutes": 15,
"description": "",
"content_url": ""
}
],
"description": "In diesem Circle erf\u00e4hrst du wie der Lehrgang aufgebaut ist.\nZudem lernst du die wichtigsten Grundlagen,\ndamit du erfolgreich mit deinem Lernpfad (durch-)starten kannst.",
"goals": "\n <p class=\"mt-4\">In diesem Circle erf\u00e4hrst du wie der Lehrgang aufgebaut ist. Zudem lernst du die wichtigsten Grundlagen,\n damit du erfolgreich mit deinem Lernpfad und in deinem Job (durch-)starten kannst.</p>\n <p class=\"mt-4\">Du baust das Grundlagenwissen f\u00fcr die folgenden Themenfelder auf:</p>\n <ul>\n <li class=\"flex items-center\"><it-icon-check class=\"hidden h-12 w-12 flex-none text-sky-500 lg:inline-block it-icon\"></it-icon-check>Aufbau von myVBV und wie du dich im Lernpfad zurechtfindest</li>\n <li class=\"flex items-center\"><it-icon-check class=\"hidden h-12 w-12 flex-none text-sky-500 lg:inline-block it-icon\"></it-icon-check>Worauf die Ausbildung und die Zulassungspr\u00fcfung zum/zur Versicherungsvermittler/-in VBV basieren</li>\n <li class=\"flex items-center\"><it-icon-check class=\"hidden h-12 w-12 flex-none text-sky-500 lg:inline-block it-icon\"></it-icon-check>Organisation deiner Lernreise und deiner Zusammenarbeit mit deiner Lernbegleitung und einem\n Lernpartner/einer Lernpartnerin</li>\n <li class=\"flex items-center\"><it-icon-check class=\"hidden h-12 w-12 flex-none text-sky-500 lg:inline-block it-icon\"></it-icon-check>Tipps und Tricks zur Organisation eines erfolgreichen Arbeitsalltags</li>\n <li class=\"flex items-center\"><it-icon-check class=\"hidden h-12 w-12 flex-none text-sky-500 lg:inline-block it-icon\"></it-icon-check>Umgang mit den sozialen Medien und Datenschutz</li>\n <li class=\"flex items-center\"><it-icon-check class=\"hidden h-12 w-12 flex-none text-sky-500 lg:inline-block it-icon\"></it-icon-check>Basiswissen Versicherungswirtschaft</li>\n <li class=\"flex items-center\"><it-icon-check class=\"hidden h-12 w-12 flex-none text-sky-500 lg:inline-block it-icon\"></it-icon-check>Basiswissen Versicherungsrecht</li>\n </ul>\n <p class=\"mt-4\">Du arbeitest an folgenden Leistungskriterien aus dem Qualifikationsprofil:</p>\n <h3>Arbeitsalltag/Lerneinheit: \u00abLucas Auftritt in den sozialen Medien und der Umgang mit sensiblen Daten\u00bb</h3>\n <p class=\"mt-4\">Ich bin f\u00e4hig, \u2026</p>\n <ul>\n <li class=\"flex items-center\"><it-icon-check class=\"hidden h-12 w-12 flex-none text-sky-500 lg:inline-block it-icon\"></it-icon-check>A3.1: \u2026 in Zusammenarbeit mit den IT-Spezialisten und der Marketingabteilung die Inhalte f\u00fcr den zu\n realisierenden Medienauftritt zielgruppengerecht festzulegen</li>\n <li class=\"flex items-center\"><it-icon-check class=\"hidden h-12 w-12 flex-none text-sky-500 lg:inline-block it-icon\"></it-icon-check>A3.2: \u2026 f\u00fcr die verschiedenen Kundensegmente die passenden sozialen Medien zu definieren</li>\n <li class=\"flex items-center\"><it-icon-check class=\"hidden h-12 w-12 flex-none text-sky-500 lg:inline-block it-icon\"></it-icon-check>A3.3. \u2026 die Inhalte compliant zu halten</li>\n </ul>\n "
},
{
"id": 32,
"title": "Circle VV",
"slug": "test-lehrgang-lp-topic-circle-vv",
"content_type": "learnpath.Topic",
"translation_key": "19611237-22e1-40e6-b5b1-a34ff470df14",
"frontend_url": "",
"is_visible": false
},
{
"id": 33,
"title": "Reisen",
"slug": "test-lehrgang-lp-circle-reisen",
"content_type": "learnpath.Circle",
"translation_key": "2aaf0215-693a-407c-9f1c-bdb80f982c92",
"frontend_url": "/course/test-lehrgang/learn/reisen",
"children": [
{
"id": 34,
"title": "Starten",
"slug": "test-lehrgang-lp-circle-reisen-ls-starten",
"content_type": "learnpath.LearningSequence",
"translation_key": "e4b0eac3-3a7c-435f-8151-f69c40b35fd6",
"frontend_url": "/course/test-lehrgang/learn/reisen#ls-starten",
"icon": "it-icon-ls-start"
},
{
"id": 35,
"title": "Einf\u00fchrung",
"slug": "test-lehrgang-lp-circle-reisen-lu-einf\u00fchrung",
"content_type": "learnpath.LearningUnit",
"translation_key": "9f0d6302-d058-4f93-b08e-9dbd4b8b8ed3",
"frontend_url": "/course/test-lehrgang/learn/reisen#lu-einf\u00fchrung",
"evaluate_url": "/course/test-lehrgang/learn/reisen/evaluate/einf\u00fchrung",
"course_category": {
"id": 1,
"title": "Allgemein",
"general": true
},
"children": []
},
{
"id": 36,
"title": "Verschaff dir einen \u00dcberblick",
"slug": "test-lehrgang-lp-circle-reisen-lc-verschaff-dir-einen-\u00fcberblick",
"content_type": "learnpath.LearningContentVideo",
"translation_key": "e666b414-175f-439d-9dfd-e1c434a8cc0e",
"frontend_url": "/course/test-lehrgang/learn/reisen/verschaff-dir-einen-\u00fcberblick",
"minutes": 15,
"description": "Willkommen im Lehrgang Versicherungsvermitler VBV",
"content_url": "https://player.vimeo.com/video/772512710?h=30f912f15a"
},
{
"id": 37,
"title": "Mediathek Reisen",
"slug": "test-lehrgang-lp-circle-reisen-lc-mediathek-reisen",
"content_type": "learnpath.LearningContentMediaLibrary",
"translation_key": "3b4cae41-185f-40f2-86c0-f96057214ada",
"frontend_url": "/course/test-lehrgang/learn/reisen/mediathek-reisen",
"minutes": 15,
"description": "",
"content_url": "/media/test-lehrgang-media/category/reisen"
},
{
"id": 38,
"title": "Analyse",
"slug": "test-lehrgang-lp-circle-reisen-ls-analyse",
"content_type": "learnpath.LearningSequence",
"translation_key": "84be9e5b-6517-4a6d-85a3-1bdf90f78780",
"frontend_url": "/course/test-lehrgang/learn/reisen#ls-analyse",
"icon": "it-icon-ls-apply"
},
{
"id": 39,
"title": "Bedarfsanalyse, Ist- und Soll-Situation",
"slug": "test-lehrgang-lp-circle-reisen-lu-reisen",
"content_type": "learnpath.LearningUnit",
"translation_key": "7cc1e966-75db-4703-8de4-1a3171372299",
"frontend_url": "/course/test-lehrgang/learn/reisen#lu-reisen",
"evaluate_url": "/course/test-lehrgang/learn/reisen/evaluate/reisen",
"course_category": {
"id": 3,
"title": "Reisen",
"general": false
},
"children": [
{
"id": 41,
"title": "Ich bin f\u00e4hig zu Reisen eine Gespr\u00e4chsf\u00fchrung zu machen",
"slug": "test-lehrgang-competence-crit-y11-reisen",
"content_type": "competence.PerformanceCriteria",
"translation_key": "b82dfd37-649f-488c-a78e-c6a3257c3f43",
"frontend_url": "",
"competence_id": "Y1.1"
},
{
"id": 42,
"title": "Ich bin f\u00e4hig zu Reisen eine Analyse zu machen",
"slug": "test-lehrgang-competence-crit-y21-reisen",
"content_type": "competence.PerformanceCriteria",
"translation_key": "9cf4e552-9dc1-46f8-b3e2-800e7bfd4afe",
"frontend_url": "",
"competence_id": "Y2.1"
}
]
},
{
"id": 40,
"title": "Emma und Ayla campen durch Amerika - Analyse",
"slug": "test-lehrgang-lp-circle-reisen-lc-emma-und-ayla-campen-durch-amerika-analyse",
"content_type": "learnpath.LearningContentLearningModule",
"translation_key": "a2b7889c-1143-4cc1-b4f7-0e611de60ee1",
"frontend_url": "/course/test-lehrgang/learn/reisen/emma-und-ayla-campen-durch-amerika-analyse",
"minutes": 15,
"description": "",
"content_url": "https://s3.eu-central-1.amazonaws.com/myvbv-wbt.iterativ.ch/emma-und-ayla-campen-durch-amerika-analyse-xapi-FZoZOP9y/index.html"
},
{
"id": 43,
"title": "Transfer",
"slug": "test-lehrgang-lp-circle-reisen-ls-transfer",
"content_type": "learnpath.LearningSequence",
"translation_key": "655a349d-48e4-4831-b518-872d0714d9e3",
"frontend_url": "/course/test-lehrgang/learn/reisen#ls-transfer",
"icon": "it-icon-ls-end"
},
{
"id": 44,
"title": "Transfer, Reflexion, Feedback",
"slug": "test-lehrgang-lp-circle-reisen-lu-transfer-reflexion-feedback",
"content_type": "learnpath.LearningUnit",
"translation_key": "8d7cc58a-3a91-49ea-906f-c1de57fec0b2",
"frontend_url": "/course/test-lehrgang/learn/reisen#lu-transfer-reflexion-feedback",
"evaluate_url": "/course/test-lehrgang/learn/reisen/evaluate/transfer-reflexion-feedback",
"course_category": {
"id": 1,
"title": "Allgemein",
"general": true
},
"children": []
},
{
"id": 45,
"title": "Auswandern: Woran muss ich denken?",
"slug": "test-lehrgang-lp-circle-reisen-lc-auswandern-woran-muss-ich-denken",
"content_type": "learnpath.LearningContentPlaceholder",
"translation_key": "691d7659-8bd9-4baa-92fd-022e9d418c46",
"frontend_url": "/course/test-lehrgang/learn/reisen/auswandern-woran-muss-ich-denken",
"minutes": 15,
"description": "Platzhalter",
"content_url": ""
},
{
"id": 46,
"title": "Fachcheck Reisen",
"slug": "test-lehrgang-lp-circle-reisen-lc-fachcheck-reisen",
"content_type": "learnpath.LearningContentPlaceholder",
"translation_key": "26294bc1-9dfe-4c17-a231-02a1387e8dcf",
"frontend_url": "/course/test-lehrgang/learn/reisen/fachcheck-reisen",
"minutes": 15,
"description": "Platzhalter",
"content_url": ""
},
{
"id": 47,
"title": "Reflexion",
"slug": "test-lehrgang-lp-circle-reisen-lc-reflexion",
"content_type": "learnpath.LearningContentPlaceholder",
"translation_key": "cd091a5d-63e8-4a4d-8178-d0224e869146",
"frontend_url": "/course/test-lehrgang/learn/reisen/reflexion",
"minutes": 15,
"description": "Platzhalter",
"content_url": ""
},
{
"id": 48,
"title": "Feedback",
"slug": "test-lehrgang-lp-circle-reisen-lc-feedback",
"content_type": "learnpath.LearningContentFeedback",
"translation_key": "ca35688c-f8ee-4aaf-b435-6e84163d9ea6",
"frontend_url": "/course/test-lehrgang/learn/reisen/feedback",
"minutes": 15,
"description": "",
"content_url": ""
}
],
"description": "In diesem Circle erf\u00e4hrst du wie der Lehrgang aufgebaut ist. Zudem lernst du die wichtigsten Grundlagen, damit du\nerfolgreich mit deinem Lernpfad und in deinem Job (durch-)starten kannst.",
"goals": "\n <p class=\"mt-4\">In diesem Circle erf\u00e4hrst du wie der Lehrgang aufgebaut ist. Zudem lernst du die wichtigsten Grundlagen,\n damit du erfolgreich mit deinem Lernpfad und in deinem Job (durch-)starten kannst.</p>\n <p class=\"mt-4\">Du baust das Grundlagenwissen f\u00fcr die folgenden Themenfelder auf:</p>\n <ul>\n <li class=\"flex items-center\"><it-icon-check class=\"hidden h-12 w-12 flex-none text-sky-500 lg:inline-block it-icon\"></it-icon-check>Aufbau von myVBV und wie du dich im Lernpfad zurechtfindest</li>\n <li class=\"flex items-center\"><it-icon-check class=\"hidden h-12 w-12 flex-none text-sky-500 lg:inline-block it-icon\"></it-icon-check>Worauf die Ausbildung und die Zulassungspr\u00fcfung zum/zur Versicherungsvermittler/-in VBV basieren</li>\n <li class=\"flex items-center\"><it-icon-check class=\"hidden h-12 w-12 flex-none text-sky-500 lg:inline-block it-icon\"></it-icon-check>Organisation deiner Lernreise und deiner Zusammenarbeit mit deiner Lernbegleitung und einem\n Lernpartner/einer Lernpartnerin</li>\n <li class=\"flex items-center\"><it-icon-check class=\"hidden h-12 w-12 flex-none text-sky-500 lg:inline-block it-icon\"></it-icon-check>Tipps und Tricks zur Organisation eines erfolgreichen Arbeitsalltags</li>\n <li class=\"flex items-center\"><it-icon-check class=\"hidden h-12 w-12 flex-none text-sky-500 lg:inline-block it-icon\"></it-icon-check>Umgang mit den sozialen Medien und Datenschutz</li>\n <li class=\"flex items-center\"><it-icon-check class=\"hidden h-12 w-12 flex-none text-sky-500 lg:inline-block it-icon\"></it-icon-check>Basiswissen Versicherungswirtschaft</li>\n <li class=\"flex items-center\"><it-icon-check class=\"hidden h-12 w-12 flex-none text-sky-500 lg:inline-block it-icon\"></it-icon-check>Basiswissen Versicherungsrecht</li>\n </ul>\n <p class=\"mt-4\">Du arbeitest an folgenden Leistungskriterien aus dem Qualifikationsprofil:</p>\n <h3>Arbeitsalltag/Lerneinheit: \u00abLucas Auftritt in den sozialen Medien und der Umgang mit sensiblen Daten\u00bb</h3>\n <p class=\"mt-4\">Ich bin f\u00e4hig, \u2026</p>\n <ul>\n <li class=\"flex items-center\"><it-icon-check class=\"hidden h-12 w-12 flex-none text-sky-500 lg:inline-block it-icon\"></it-icon-check>A3.1: \u2026 in Zusammenarbeit mit den IT-Spezialisten und der Marketingabteilung die Inhalte f\u00fcr den zu\n realisierenden Medienauftritt zielgruppengerecht festzulegen</li>\n <li class=\"flex items-center\"><it-icon-check class=\"hidden h-12 w-12 flex-none text-sky-500 lg:inline-block it-icon\"></it-icon-check>A3.2: \u2026 f\u00fcr die verschiedenen Kundensegmente die passenden sozialen Medien zu definieren</li>\n <li class=\"flex items-center\"><it-icon-check class=\"hidden h-12 w-12 flex-none text-sky-500 lg:inline-block it-icon\"></it-icon-check>A3.3. \u2026 die Inhalte compliant zu halten</li>\n </ul>\n "
}
],
"course": {
"id": -1,
"title": "Test Lehrgang",
"category_name": "Handlungsfeld",
"slug": "test-lehrgang"
}
}

View File

@ -1,11 +1,9 @@
import { useCourseSessionDetailQuery } from "@/composables";
import { itGet } from "@/fetchHelpers";
import type { LearningPath } from "@/services/learningPath";
import type {
Assignment,
AssignmentCompletion,
CourseSessionUser,
LearningContentAssignment,
UserAssignmentCompletionStatus,
} from "@/types";
import { sum } from "d3";
@ -16,17 +14,6 @@ export interface GradedUser {
points: number;
}
export function calcLearningContentAssignments(learningPath?: LearningPath) {
// TODO: filter by circle
if (!learningPath) return [];
return learningPath.circles.flatMap((circle) => {
return circle.flatLearningContents.filter(
(lc) => lc.content_type === "learnpath.LearningContentAssignment"
) as LearningContentAssignment[];
});
}
export async function loadAssignmentCompletionStatusData(
assignmentId: string,
courseSessionId: string,

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 circleFlatChildren(circle: CircleType) {
return [
...circleFlatLearningContents(circle),
...circleFlatLearningUnits(circle).flatMap((lu) => {
return lu.performance_criteria;
}),
];
}
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...");
}
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;
});
learningSequence.minutes += learningUnit.minutes;
});
export function circleFlatLearningUnits(circle: CircleType) {
return circle.learning_sequences.flatMap((ls) => {
return ls.learning_units;
});
return result;
}
export class Circle implements WagtailCircle {
readonly content_type = "learnpath.Circle";
readonly learningSequences: LearningSequence[];
nextCircle?: Circle;
previousCircle?: Circle;
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
): 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 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);
}
}
export function learningSequenceFlatChildren(ls: LearningSequence) {
return [
...ls.learning_units.flatMap((lu) => {
return lu.learning_contents;
}),
...ls.learning_units.flatMap((lu) => {
return lu.performance_criteria;
}),
];
}
export function someFinishedInLearningSequence(ls: LearningSequence) {
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")
) {
return "SUCCESS";
}
if (
learningUnit.performance_criteria.every(
(q) => q.completion_status === "FAIL" || q.completion_status === "SUCCESS"
)
) {
return "FAIL";
}
}
return "UNKNOWN";
}

View File

@ -0,0 +1,19 @@
import type { PerformanceCriteria } from "@/types";
import groupBy from "lodash/groupBy";
export function calcPerformanceCriteriaStatusCount(criteria: PerformanceCriteria[]) {
if (criteria) {
const grouped = groupBy(criteria, "completion_status");
return {
UNKNOWN: grouped?.UNKNOWN?.length || 0,
SUCCESS: grouped?.SUCCESS?.length || 0,
FAIL: grouped?.FAIL?.length || 0,
};
}
return {
UNKNOWN: 0,
SUCCESS: 0,
FAIL: 0,
};
}

View File

@ -1,175 +0,0 @@
import orderBy from "lodash/orderBy";
import { Circle } from "@/services/circle";
import { useCourseSessionsStore } from "@/stores/courseSessions";
import { useLearningPathStore } from "@/stores/learningPath";
import type {
Course,
CourseCompletion,
LearningContentInterface,
LearningPathChild,
Topic,
WagtailLearningPath,
} from "@/types";
export interface ContinueData {
url: string;
has_no_progress: boolean;
}
function getLastCompleted(courseSlug: string, completionData: CourseCompletion[]) {
if (completionData.length === 0) {
return undefined;
}
const courseSessionsStore = useCourseSessionsStore();
const courseSession = courseSessionsStore.courseSessionForCourse(courseSlug);
return orderBy(completionData, ["updated_at"], "desc").find((c: CourseCompletion) => {
return (
c.completion_status === "SUCCESS" &&
c.course_session_id === courseSession?.id &&
c.page_type.startsWith("learnpath.LearningContent")
);
});
}
export class LearningPath implements WagtailLearningPath {
readonly content_type = "learnpath.LearningPath";
public topics: Topic[];
public circles: Circle[];
public nextLearningContent?: LearningContentInterface;
public static fromJson(
json: WagtailLearningPath,
completionData: CourseCompletion[],
userId: string | undefined
): LearningPath {
return new LearningPath(
json.id,
json.slug,
json.course.title,
json.translation_key,
json.frontend_url,
json.course,
json.children,
userId,
completionData
);
}
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 course: Course,
public children: LearningPathChild[],
public userId: string | undefined,
completionData?: CourseCompletion[]
) {
// parse children
this.topics = [];
this.circles = [];
let topic: Topic | undefined;
this.children.forEach((page) => {
if (page.content_type === "learnpath.Topic") {
if (topic) {
this.topics.push(topic);
}
topic = Object.assign(page, { circles: [] });
}
if (page.content_type === "learnpath.Circle") {
const circle = Circle.fromJson(page, this);
if (completionData && completionData.length > 0) {
circle.parseCompletionData(completionData);
}
if (topic) {
topic.circles.push(circle);
}
circle.previousCircle = this.circles[this.circles.length - 1];
if (circle.previousCircle) {
circle.previousCircle.nextCircle = circle;
}
this.circles.push(circle);
}
});
if (topic) {
this.topics.push(topic);
}
if (completionData) {
this.calcNextLearningContent(completionData);
}
}
public async reloadCompletionData() {
const learningPathStore = useLearningPathStore();
const completionData = await learningPathStore.loadCourseSessionCompletionData(
this.course.slug,
this.userId
);
for (const circle of this.circles) {
circle.parseCompletionData(completionData);
}
}
public calcNextLearningContent(completionData: CourseCompletion[]): void {
this.nextLearningContent = undefined;
const lastCompletedLearningContent = getLastCompleted(
this.course.slug,
completionData
);
if (lastCompletedLearningContent) {
const lastCircle = this.circles.find((circle) => {
return circle.flatLearningContents.find(
(learningContent) =>
learningContent.id === lastCompletedLearningContent.page_id
);
});
if (lastCircle) {
const lastLearningContent = lastCircle.flatLearningContents.find(
(learningContent) =>
learningContent.id === lastCompletedLearningContent.page_id
);
if (lastLearningContent && lastLearningContent.nextLearningContent) {
this.nextLearningContent = lastLearningContent.nextLearningContent;
} else {
if (lastCircle.nextCircle) {
this.nextLearningContent = lastCircle.nextCircle.flatLearningContents[0];
}
}
}
} else {
if (this.circles[0]) {
this.nextLearningContent = this.circles[0].flatLearningContents[0];
}
}
}
public get continueData(): ContinueData {
if (this.nextLearningContent) {
const circle = this.nextLearningContent.parentCircle;
const url =
this.nextLearningContent.parentLearningSequence?.frontend_url ||
circle.frontend_url;
const isFirst =
this.nextLearningContent.translation_key ===
this.circles[0].flatLearningContents[0].translation_key;
return {
url,
has_no_progress: isFirst,
};
}
return {
url: "",
has_no_progress: true,
};
}
}

View File

@ -1,28 +1,23 @@
import type { Circle } from "@/services/circle";
import { useCompletionStore } from "@/stores/completion";
import { useLearningPathStore } from "@/stores/learningPath";
import { useUserStore } from "@/stores/user";
import type {
CourseCompletionStatus,
LearningContentInterface,
CircleType,
LearningContent,
LearningContentWithCompletion,
LearningUnit,
LearningUnitPerformanceCriteria,
PerformanceCriteria,
} from "@/types";
import * as log from "loglevel";
import { defineStore } from "pinia";
import type { RouteLocationNormalized } from "vue-router";
export type CircleStoreState = {
circle: Circle | undefined;
page: "INDEX" | "OVERVIEW";
};
function createLearningUnitHash(learningUnit: LearningUnit | undefined) {
const luSlug = learningUnit?.slug;
const circleSlug = learningUnit?.parentCircle?.slug;
if (luSlug && circleSlug) {
return "#" + luSlug.replace(`${circleSlug}-`, "");
function createLearningUnitHash(
circle: CircleType,
learningUnitSlug: string | undefined
) {
if (learningUnitSlug && circle) {
return "#" + learningUnitSlug.replace(`${circle.slug}-`, "");
}
return "";
@ -32,111 +27,30 @@ export const useCircleStore = defineStore({
id: "circle",
state: () => {
return {
circle: undefined,
page: "INDEX",
} as CircleStoreState;
},
getters: {},
actions: {
async loadCircle(
courseSlug: string,
circleSlug: string,
userId: string | undefined = undefined
): Promise<Circle> {
if (!userId) {
const userStore = useUserStore();
userId = userStore.id;
}
this.circle = undefined;
const learningPathSlug = courseSlug + "-lp";
const learningPathStore = useLearningPathStore();
const learningPath = await learningPathStore.loadLearningPath(
learningPathSlug,
userId
);
if (learningPath) {
this.circle = learningPath.circles.find((circle) => {
return circle.slug.endsWith(circleSlug);
});
}
if (!this.circle) {
throw `No circle found with slug: ${circleSlug}`;
}
return this.circle;
},
async loadLearningContent(
courseSlug: string,
circleSlug: string,
learningContentSlug: string
) {
const circle = await this.loadCircle(courseSlug, circleSlug);
const result = circle.flatLearningContents.find((learningContent) => {
return learningContent.slug.endsWith(learningContentSlug);
});
if (!result) {
throw `No learning content found with slug: ${learningContentSlug}`;
}
return result;
},
async loadSelfEvaluation(
courseSlug: string,
circleSlug: string,
learningUnitSlug: string
) {
const circle = await this.loadCircle(courseSlug, circleSlug);
const learningUnit = circle.flatLearningUnits.find((child) => {
return child.slug.endsWith(learningUnitSlug);
});
if (!learningUnit) {
throw `No self evaluation found with slug: ${learningUnitSlug}`;
}
return learningUnit;
},
async markCompletion(
page:
| LearningContentInterface
| LearningUnitPerformanceCriteria
| PerformanceCriteria
| undefined,
completion_status: CourseCompletionStatus = "SUCCESS"
) {
const completionStore = useCompletionStore();
try {
if (page) {
page.completion_status = completion_status;
const completionData = await completionStore.markPage(page);
if (this.circle) {
this.circle.parseCompletionData(completionData);
}
}
} catch (error) {
log.error(error);
return error;
}
},
openLearningContent(learningContent: LearningContentInterface) {
openLearningContent(learningContent: LearningContent) {
this.router.push({
path: learningContent.frontend_url,
});
},
closeLearningContent(
learningContent: LearningContentInterface,
learningContent: LearningContentWithCompletion,
circle: CircleType,
returnRoute?: RouteLocationNormalized
) {
if (returnRoute) {
this.router.push(returnRoute);
} else {
this.router.push({
path: `${this.circle?.frontend_url}`,
hash: createLearningUnitHash(learningContent.parentLearningUnit),
path: `${circle.frontend_url}`,
hash: createLearningUnitHash(
circle,
learningContent.parentLearningUnit?.slug
),
});
}
},
@ -147,50 +61,37 @@ export const useCircleStore = defineStore({
},
closeSelfEvaluation(
learningUnit: LearningUnit,
circle: CircleType,
returnRoute?: RouteLocationNormalized
) {
if (returnRoute) {
this.router.push(returnRoute);
} else {
this.router.push({
path: `${this.circle?.frontend_url}`,
hash: createLearningUnitHash(learningUnit),
path: `${circle.frontend_url}`,
hash: createLearningUnitHash(circle, learningUnit.slug),
});
}
},
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
currentLearningContent: LearningContentWithCompletion,
circle: CircleType,
returnRoute?: RouteLocationNormalized,
markCompletionFn?: (learningContent: LearningContentWithCompletion) => void
) {
if (currentLearningContent) {
if (currentLearningContent.can_user_self_toggle_course_completion) {
this.markCompletion(currentLearningContent, "SUCCESS");
} else {
// reload completion data anyway
currentLearningContent.parentCircle?.parentLearningPath?.reloadCompletionData();
if (markCompletionFn) {
markCompletionFn(currentLearningContent);
}
}
this.closeLearningContent(currentLearningContent, returnRoute);
this.closeLearningContent(currentLearningContent, circle, returnRoute);
} else {
log.error("currentLearningContent is undefined");
}
},
continueFromSelfEvaluation(learningUnit: LearningUnit) {
this.closeSelfEvaluation(learningUnit);
continueFromSelfEvaluation(learningUnit: LearningUnit, circle: CircleType) {
this.closeSelfEvaluation(learningUnit, circle);
},
},
});

View File

@ -1,4 +1,4 @@
import { useLearningPathStore } from "@/stores/learningPath";
import { useCourseData } from "@/composables";
import { useUserStore } from "@/stores/user";
import type { CircleLight, CourseSessionUser, ExpertSessionUser } from "@/types";
import log from "loglevel";
@ -12,7 +12,6 @@ export type CockpitStoreState = {
courseSessionMembers: CourseSessionUser[] | undefined;
circles: CircleCockpit[] | undefined;
currentCircle: CircleCockpit | undefined;
currentCourseSlug: string | undefined;
};
export const useCockpitStore = defineStore({
@ -22,7 +21,6 @@ export const useCockpitStore = defineStore({
courseSessionMembers: undefined,
circles: [],
currentCircle: undefined,
currentCourseSlug: undefined,
} as CockpitStoreState;
},
actions: {
@ -31,21 +29,8 @@ export const useCockpitStore = defineStore({
currentCourseSessionUser: CourseSessionUser | undefined
) {
log.debug("loadCircles called", courseSlug);
this.currentCourseSlug = courseSlug;
const circles = await courseCircles(
this.currentCourseSlug,
currentCourseSessionUser
);
this.circles = circles.map((c) => {
return {
id: c.id,
slug: c.slug,
title: c.title,
name: c.title,
} as const;
});
this.circles = await courseCircles(courseSlug, currentCourseSessionUser);
if (this.circles?.length) {
await this.setCurrentCourseCircle(this.circles[0].slug);
@ -64,28 +49,27 @@ async function courseCircles(
courseSlug: string,
currentCourseSessionUser: CourseSessionUser | undefined
) {
const userStore = useUserStore();
const userId = userStore.id;
if (currentCourseSessionUser && currentCourseSessionUser.role === "EXPERT") {
const expert = currentCourseSessionUser as ExpertSessionUser;
return expert.circles;
return expert.circles.map((c) => {
return { ...c, name: c.title };
});
}
const userStore = useUserStore();
// Return all circles from learning path for admin users
if (userStore.is_superuser) {
const learningPathStore = useLearningPathStore();
const learningPathCircles = learningPathStore
.learningPathForUser(courseSlug, userId)
?.circles.map((c) => {
return {
id: c.id,
title: c.title,
slug: c.slug,
translation_key: c.translation_key,
};
});
return learningPathCircles || [];
const lpQueryResult = useCourseData(courseSlug);
await lpQueryResult.resultPromise;
return (lpQueryResult.circles.value ?? []).map((c) => {
return {
id: c.id,
slug: c.slug,
title: c.title,
name: c.title,
} as const;
});
}
return [];

View File

@ -1,179 +0,0 @@
import { useCurrentCourseSession } from "@/composables";
import { itGetCached } from "@/fetchHelpers";
import { useCompletionStore } from "@/stores/completion";
import { useUserStore } from "@/stores/user";
import type {
CircleLight,
CompetencePage,
CompetenceProfilePage,
PerformanceCriteria,
} from "@/types";
import _ from "lodash";
import cloneDeep from "lodash/cloneDeep";
import groupBy from "lodash/groupBy";
import orderBy from "lodash/orderBy";
import { defineStore } from "pinia";
export type CompetenceStoreState = {
competenceProfilePages: Map<string, CompetenceProfilePage>;
circles: CircleLight[];
};
export const useCompetenceStore = defineStore({
id: "competence",
state: () => {
return {
competenceProfilePages: new Map<string, CompetenceProfilePage>(),
circles: [],
} as CompetenceStoreState;
},
getters: {},
actions: {
calcStatusCount(criteria: PerformanceCriteria[]) {
if (criteria) {
const grouped = groupBy(criteria, "completion_status");
return {
UNKNOWN: grouped?.UNKNOWN?.length || 0,
SUCCESS: grouped?.SUCCESS?.length || 0,
FAIL: grouped?.FAIL?.length || 0,
};
}
return {
UNKNOWN: 0,
SUCCESS: 0,
FAIL: 0,
};
},
criteriaByCompetence(competence: CompetencePage) {
return competence.children;
},
competenceProfilePage(userId: string | undefined = undefined) {
if (!userId) {
const userStore = useUserStore();
userId = userStore.id;
}
return this.competenceProfilePages.get(userId);
},
flatPerformanceCriteria(
userId: string | undefined = undefined,
circleId: string | undefined = undefined
) {
if (!userId) {
const userStore = useUserStore();
userId = userStore.id;
}
if (this.competenceProfilePages.get(userId)) {
const competenceProfilePage = this.competenceProfilePages.get(userId);
if (competenceProfilePage) {
let criteria = orderBy(
competenceProfilePage.children.flatMap((competence) => {
return competence.children;
}),
["competence_id"],
["asc"]
);
if (circleId) {
criteria = criteria.filter((c) => circleId === c.circle.id);
}
return criteria;
}
}
return [];
},
competences(userId: string | undefined = undefined) {
if (!userId) {
const userStore = useUserStore();
userId = userStore.id;
}
if (this.competenceProfilePages.get(userId)) {
const competenceProfilePage = this.competenceProfilePages.get(userId);
if (competenceProfilePage?.children.length) {
return _.orderBy(
competenceProfilePage.children.filter((competence) => {
const criteria = competence.children;
return criteria.length > 0;
}),
["competence_id"],
["asc"]
);
}
}
return [];
},
async loadCompetenceProfilePage(
slug: string,
userId: string | undefined = undefined,
reload = false
) {
if (!userId) {
const userStore = useUserStore();
userId = userStore.id;
}
if (this.competenceProfilePages.has(userId) && !reload) {
const competenceProfilePage = this.competenceProfilePages.get(userId);
await this.parseCompletionData(userId);
return competenceProfilePage;
}
const competenceProfilePage = await itGetCached(`/api/course/page/${slug}/`, {
reload: reload,
});
if (!competenceProfilePage) {
throw `No competenceProfilePageData found with: ${slug}`;
}
this.competenceProfilePages.set(userId, cloneDeep(competenceProfilePage));
this.circles = competenceProfilePage.circles;
await this.parseCompletionData(userId);
return this.competenceProfilePages.get(userId);
},
async parseCompletionData(userId: string) {
const competenceProfilePage = this.competenceProfilePages.get(userId);
if (competenceProfilePage) {
const completionStore = useCompletionStore();
const courseSession = useCurrentCourseSession();
if (courseSession) {
const completionData = await completionStore.loadCourseSessionCompletionData(
courseSession.value.id,
userId
);
if (completionData) {
competenceProfilePage.children.forEach((competence) => {
competence.children.forEach((performanceCriteria) => {
const completion = completionData.find(
(c) => c.page_id === performanceCriteria.id
);
if (completion) {
performanceCriteria.completion_status = completion.completion_status;
performanceCriteria.completion_status_updated_at =
completion.updated_at;
} else {
performanceCriteria.completion_status = "UNKNOWN";
performanceCriteria.completion_status_updated_at = "";
}
});
});
this.competenceProfilePages.set(userId, competenceProfilePage);
}
}
}
},
},
});

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

@ -1,121 +0,0 @@
import { itGetCached } from "@/fetchHelpers";
import { LearningPath } from "@/services/learningPath";
import { useCompletionStore } from "@/stores/completion";
import { useCourseSessionsStore } from "@/stores/courseSessions";
import { useUserStore } from "@/stores/user";
import type { CourseCompletion } from "@/types";
import eventBus from "@/utils/eventBus";
import cloneDeep from "lodash/cloneDeep";
import log from "loglevel";
import { defineStore } from "pinia";
import { computed, reactive } from "vue";
export type LearningPathStoreState = {
learningPaths: Map<string, LearningPath>;
page: "INDEX" | "OVERVIEW";
};
type LearningPathKey = string;
function getLearningPathKey(
slug: string,
userId: string | number | undefined
): LearningPathKey {
return `${slug}-${userId}`;
}
export const useLearningPathStore = defineStore("learningPath", () => {
const state: LearningPathStoreState = reactive({
learningPaths: new Map<LearningPathKey, LearningPath>(),
page: "INDEX",
});
const learningPathForUser = computed(() => {
return (courseSlug: string, userId: string | number | undefined) => {
if (state.learningPaths.size > 0) {
const learningPathKey = getLearningPathKey(`${courseSlug}-lp`, userId);
return state.learningPaths.get(learningPathKey);
}
return undefined;
};
});
async function loadCourseSessionCompletionData(
courseSlug: string,
userId: string | undefined = undefined
) {
// FIXME: should not be here anymore with VBV-305
const completionStore = useCompletionStore();
let completionData: CourseCompletion[] = [];
if (userId) {
const courseSessionsStore = useCourseSessionsStore();
const courseSession = courseSessionsStore.courseSessionForCourse(courseSlug);
if (courseSession) {
completionData = await completionStore.loadCourseSessionCompletionData(
courseSession.id,
userId
);
return completionData;
}
}
return [];
}
async function loadLearningPath(
slug: string,
userId: string | undefined = undefined,
reload = false,
fail = true
) {
if (!userId) {
const userStore = useUserStore();
userId = userStore.id;
}
const key = getLearningPathKey(slug, userId);
if (state.learningPaths.has(key) && !reload) {
return state.learningPaths.get(key);
}
const learningPathData = await itGetCached(`/api/course/page/${slug}/`);
if (!learningPathData && fail) {
throw `No learning path found with: ${slug}`;
}
const completionData = await loadCourseSessionCompletionData(
learningPathData.course.slug,
userId
);
const learningPath = LearningPath.fromJson(
cloneDeep(learningPathData),
completionData,
userId
);
state.learningPaths.set(key, learningPath);
return learningPath;
}
eventBus.on("switchedCourseSession", (courseSession) => {
log.debug("handle switchedCourseSession", courseSession);
// FIXME: clean up with VBV-305
// Die Completion Daten werden nur für die aktuelle Durchführung geladen.
// Deshalb müssen die hier neu geladen werden...
state.learningPaths.forEach((lp) => {
if (lp.userId) {
lp.reloadCompletionData();
}
});
});
return {
state,
learningPathForUser,
loadCourseSessionCompletionData,
loadLearningPath,
};
});

View File

@ -1,11 +1,40 @@
import type { AssignmentCompletionStatus as AssignmentCompletionStatusGenerated } from "@/gql/graphql";
import type { Circle } from "@/services/circle";
import type {
ActionCompetenceObjectType,
AssignmentAssignmentAssignmentTypeChoices,
AssignmentCompletionObjectType,
AssignmentCompletionStatus as AssignmentCompletionStatusGenerated,
AssignmentObjectType,
CircleObjectType,
CourseCourseSessionUserRoleChoices,
CourseSessionObjectType,
CourseSessionUserObjectsType,
LearningContentAssignmentObjectType,
LearningContentAttendanceCourseObjectType,
LearningContentDocumentListObjectType,
LearningContentEdoniqTestObjectType,
LearningContentFeedbackObjectType,
LearningContentLearningModuleObjectType,
LearningContentMediaLibraryObjectType,
LearningContentPlaceholderObjectType,
LearningContentRichTextObjectType,
LearningContentVideoObjectType,
LearningPathObjectType,
LearningSequenceObjectType,
LearningUnitObjectType,
PerformanceCriteriaObjectType,
TopicObjectType,
} from "@/gql/graphql";
import type { Component } from "vue";
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;
@ -13,8 +42,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 {
@ -23,168 +50,120 @@ export interface CircleLight {
readonly title: string;
}
// refine generated types
export type LearningContentAssignment = LearningContentAssignmentObjectType & {
readonly content_type: "learnpath.LearningContentAssignment";
};
export type LearningContentAttendanceCourse =
LearningContentAttendanceCourseObjectType & {
readonly content_type: "learnpath.LearningContentAttendanceCourse";
};
export type LearningContentDocumentList = LearningContentDocumentListObjectType & {
readonly content_type: "learnpath.LearningContentDocumentList";
};
export type LearningContentEdoniqTest = LearningContentEdoniqTestObjectType & {
readonly content_type: "learnpath.LearningContentEdoniqTest";
};
export type LearningContentFeedback = LearningContentFeedbackObjectType & {
readonly content_type: "learnpath.LearningContentFeedback";
};
export type LearningContentLearningModule = LearningContentLearningModuleObjectType & {
readonly content_type: "learnpath.LearningContentLearningModule";
};
export type LearningContentMediaLibrary = LearningContentMediaLibraryObjectType & {
readonly content_type: "learnpath.LearningContentMediaLibrary";
};
export type LearningContentPlaceholder = LearningContentPlaceholderObjectType & {
readonly content_type: "learnpath.LearningContentPlaceholder";
};
export type LearningContentRichText = LearningContentRichTextObjectType & {
readonly content_type: "learnpath.LearningContentRichText";
};
export type LearningContentVideo = LearningContentVideoObjectType & {
readonly content_type: "learnpath.LearningContentVideo";
};
export type LearningContent =
| LearningContentAssignment
| LearningContentAttendanceCourse
| LearningContentDocumentList
| LearningContentEdoniqTest
| LearningContentFeedback
| LearningContentLearningModule
| LearningContentMediaLibrary
| LearningContentPlaceholder
| LearningContentRichText
| LearningContentEdoniqTest
| LearningContentVideo;
export type LearningContentType = LearningContent["content_type"];
export type LearningContentWithCompletion = LearningContent &
Completable & {
continueUrl?: string;
firstInCircle?: boolean;
parentLearningUnit?: {
id: string;
slug: string;
title: string;
};
};
export interface LearningContentInterface extends BaseCourseWagtailPage {
readonly content_type: LearningContentType;
readonly minutes: number;
readonly description: string;
readonly content_url: string;
readonly can_user_self_toggle_course_completion: boolean;
parentCircle: Circle;
parentLearningSequence?: LearningSequence;
parentLearningUnit?: LearningUnit;
nextLearningContent?: LearningContent;
previousLearningContent?: LearningContent;
}
export type LearningContentContentType = LearningContent["content_type"];
export interface LearningContentAssignment extends LearningContentInterface {
readonly content_type: "learnpath.LearningContentAssignment";
readonly content_assignment_id: string;
readonly assignment_type: AssignmentType;
readonly competence_certificate?: {
id: string;
title: string;
slug: string;
content_type: string;
translation_key: string;
frontend_url: string;
} | null;
}
export type LearningUnit = Omit<
LearningUnitObjectType,
"content_type" | "learning_contents" | "performance_criteria"
> & {
content_type: "learnpath.LearningUnit";
learning_contents: LearningContentWithCompletion[];
performance_criteria: LearningUnitPerformanceCriteria[];
circle?: CircleLight;
};
export interface LearningContentAttendanceCourse extends LearningContentInterface {
readonly content_type: "learnpath.LearningContentAttendanceCourse";
}
export type LearningSequence = Omit<
LearningSequenceObjectType,
"content_type" | "learning_units"
> & {
content_type: "learnpath.LearningSequence";
learning_units: LearningUnit[];
};
export interface LearningContentDocument {
readonly type: "document";
readonly id: string;
readonly value: MediaLibraryContentBlockValue;
}
export type CircleType = Omit<
CircleObjectType,
"content_type" | "learning_sequences"
> & {
content_type: "learnpath.Circle";
learning_sequences: LearningSequence[];
};
export interface LearningContentDocumentList extends LearningContentInterface {
readonly content_type: "learnpath.LearningContentDocumentList";
readonly documents: LearningContentDocument[];
}
export type TopicType = Omit<TopicObjectType, "content_type" | "circles"> & {
content_type: "learnpath.Topic";
circles: CircleType[];
};
export interface LearningContentFeedback extends LearningContentInterface {
readonly content_type: "learnpath.LearningContentFeedback";
}
export type LearningPathType = Omit<
LearningPathObjectType,
"content_type" | "topics"
> & {
content_type: "learnpath.LearningPath";
topics: TopicType[];
};
export interface LearningContentLearningModule extends LearningContentInterface {
readonly content_type: "learnpath.LearningContentLearningModule";
}
export interface LearningContentMediaLibrary extends LearningContentInterface {
readonly content_type: "learnpath.LearningContentMediaLibrary";
}
export interface LearningContentPlaceholder extends LearningContentInterface {
readonly content_type: "learnpath.LearningContentPlaceholder";
}
export interface LearningContentRichText extends LearningContentInterface {
readonly content_type: "learnpath.LearningContentRichText";
readonly text: string;
}
export interface LearningContentEdoniqTest extends LearningContentInterface {
readonly content_type: "learnpath.LearningContentEdoniqTest";
readonly checkbox_text: string;
readonly has_extended_time_test: boolean;
readonly content_assignment_id: string;
readonly competence_certificate?: {
id: string;
title: string;
slug: string;
content_type: string;
translation_key: string;
frontend_url: string;
} | null;
}
export interface LearningContentVideo extends LearningContentInterface {
readonly content_type: "learnpath.LearningContentVideo";
}
export interface LearningUnitPerformanceCriteria extends BaseCourseWagtailPage {
readonly content_type: "competence.PerformanceCriteria";
readonly competence_id: string;
parentLearningSequence?: LearningSequence;
parentLearningUnit?: LearningUnit;
}
export interface LearningUnit extends BaseCourseWagtailPage {
readonly content_type: "learnpath.LearningUnit";
readonly evaluate_url: string;
readonly course_category: CourseCategory;
readonly title_hidden: boolean;
children: LearningUnitPerformanceCriteria[];
// additional frontend fields
learningContents: LearningContent[];
minutes: number;
parentLearningSequence?: LearningSequence;
parentCircle?: Circle;
last?: boolean;
}
export interface LearningSequence extends BaseCourseWagtailPage {
readonly content_type: "learnpath.LearningSequence";
icon: string;
learningUnits: LearningUnit[];
minutes: number;
}
export type CircleChild = LearningContentInterface | LearningUnit | LearningSequence;
export interface WagtailLearningPath extends BaseCourseWagtailPage {
readonly content_type: "learnpath.LearningPath";
course: Course;
children: LearningPathChild[];
}
export interface CircleGoal {
type: "goal";
value: string;
id: string;
}
export interface CircleJobSituation {
type: "job_situation";
value: string;
id: string;
}
export interface WagtailCircle extends BaseCourseWagtailPage {
readonly content_type: "learnpath.Circle";
readonly description: string;
readonly goal_description: string;
readonly goals: CircleGoal[];
readonly job_situation_description: string;
readonly job_situations: CircleJobSituation[];
readonly children: CircleChild[];
}
export interface Topic extends BaseCourseWagtailPage {
readonly content_type: "learnpath.Topic";
readonly is_visible: boolean;
circles: Circle[];
}
export type LearningPathChild = Topic | WagtailCircle;
export type LearningUnitPerformanceCriteria = Omit<
PerformanceCriteriaObjectType,
"content_type"
> &
Completable & {
readonly content_type: "competence.PerformanceCriteria";
circle?: CircleLight;
};
export interface CourseCompletion {
readonly id: string;
@ -361,57 +340,31 @@ export interface AssignmentEvaluationTask {
};
}
export type AssignmentType =
| "CASEWORK"
| "PREP_ASSIGNMENT"
| "REFLECTION"
| "CONDITION_ACCEPTANCE"
| "EDONIQ_TEST";
export type AssignmentType = AssignmentAssignmentAssignmentTypeChoices;
export interface Assignment extends BaseCourseWagtailPage {
export type Assignment = Omit<
AssignmentObjectType,
"content_type" | "performance_objectives" | "tasks" | "evaluation_tasks"
> & {
readonly content_type: "assignment.Assignment";
readonly assignment_type: AssignmentType;
readonly needs_expert_evaluation: boolean;
readonly intro_text: string;
readonly effort_required: string;
readonly performance_objectives: AssignmentPerformanceObjective[];
readonly evaluation_description: string;
readonly evaluation_document_url: string;
readonly tasks: AssignmentTask[];
readonly evaluation_tasks: AssignmentEvaluationTask[];
readonly max_points: number;
readonly competence_certificate?: {
id: string;
title: string;
slug: string;
content_type: string;
translation_key: string;
frontend_url: string;
} | null;
}
};
export interface PerformanceCriteria extends BaseCourseWagtailPage {
readonly content_type: "competence.PerformanceCriteria";
readonly competence_id: string;
readonly circle: CircleLight;
readonly course_category: CourseCategory;
readonly learning_unit: BaseCourseWagtailPage & {
evaluate_url: string;
export type PerformanceCriteria = Omit<PerformanceCriteriaObjectType, "content_type"> &
Completable & {
readonly content_type: "competence.PerformanceCriteria";
circle: CircleLight;
};
}
export interface CompetencePage extends BaseCourseWagtailPage {
readonly content_type: "competence.CompetencePage";
readonly competence_id: string;
readonly children: PerformanceCriteria[];
}
export interface CompetenceProfilePage extends BaseCourseWagtailPage {
readonly content_type: "competence.CompetenceProfilePage";
readonly course: Course;
readonly circles: CircleLight[];
readonly children: CompetencePage[];
}
export type ActionCompetence = Omit<
ActionCompetenceObjectType,
"content_type" | "performance_criteria"
> & {
readonly content_type: "competence.ActionCompetence";
performance_criteria: PerformanceCriteria[];
};
export interface CompetenceCertificateAssignment extends BaseCourseWagtailPage {
assignment_type: "CASEWORK" | "EDONIQ_TEST";
@ -471,47 +424,6 @@ export interface CircleDocument {
};
}
export interface CourseSessionAttendanceCourse {
id: string;
due_date: SimpleDueDate;
location: string;
trainer: string;
circle_title: string;
learning_content: {
id: string;
title: string;
circle: CircleLight;
};
}
export interface CourseSessionAssignment {
id: string;
submission_deadline: SimpleDueDate;
evaluation_deadline: SimpleDueDate;
learning_content: {
id: string;
content_assignment: {
id: string;
title: string;
assignment_type: AssignmentType;
};
};
}
export interface CourseSessionEdoniqTest {
id: number;
course_session_id: string;
deadline: SimpleDueDate;
learning_content: {
id: string;
content_assignment: {
id: string;
title: string;
assignment_type: AssignmentType;
};
};
}
export interface CourseSession {
id: string;
created_at: string;
@ -523,46 +435,15 @@ export interface CourseSession {
due_dates: DueDate[];
}
export type Role = "MEMBER" | "EXPERT" | "TUTOR";
export type Role = CourseCourseSessionUserRoleChoices;
export interface CourseSessionUser {
user_id: string;
first_name: string;
last_name: string;
email: string;
avatar_url: string;
role: Role;
circles: {
id: string;
title: string;
slug: string;
translation_key: string;
}[];
}
export type CourseSessionUser = CourseSessionUserObjectsType;
export interface ExpertSessionUser extends CourseSessionUser {
role: "EXPERT";
circles: {
id: string;
title: string;
slug: string;
translation_key: string;
}[];
}
export interface CourseSessionDetail {
id: string;
title: string;
course: {
id: string;
title: string;
slug: string;
};
assignments: CourseSessionAssignment[];
attendance_courses: CourseSessionAttendanceCourse[];
edoniq_tests: CourseSessionEdoniqTest[];
users: CourseSessionUser[];
}
export type CourseSessionDetail = CourseSessionObjectType;
// document upload
export interface DocumentUploadData {
@ -641,37 +522,11 @@ export interface AssignmentTaskCompletionData {
};
}
export interface AssignmentCompletion {
id: string;
created_at: string;
updated_at: string;
submitted_at: string;
evaluation_submitted_at: string | null;
assignment_user: string;
assignment: number;
course_session: number;
completion_status: AssignmentCompletionStatus;
evaluation_user: string | null;
export type AssignmentCompletion = AssignmentCompletionObjectType & {
// assignment_user: Pick<UserObjectType, "id" | "__typename">;
// evaluation_user: Pick<UserObjectType, "id" | "__typename">;
completion_data: AssignmentCompletionData;
task_completion_data: AssignmentTaskCompletionData;
edoniq_extended_time_flag: boolean;
evaluation_points: number | null;
evaluation_max_points: number | null;
evaluation_passed: boolean | null;
}
export type UpsertUserAssignmentCompletion = {
assignment_id: string;
course_session_id: string;
completion_status: AssignmentCompletionStatus;
completion_data: AssignmentCompletionData;
};
export type EvaluationCompletionData = UpsertUserAssignmentCompletion & {
assignment_user_id: string;
evaluation_points?: number;
evaluation_max_points?: number;
evaluation_passed?: boolean;
};
export interface UserAssignmentCompletionStatus {

View File

@ -5,6 +5,10 @@ export function assertUnreachable(msg: string): never {
throw new Error("Didn't expect to get here, " + msg);
}
export function stringifyParse(obj: any): any {
return JSON.parse(JSON.stringify(obj));
}
function createCourseUrl(courseSlug: string | undefined, specificSub: string): string {
if (!courseSlug) {
return "/";

View File

@ -43,6 +43,7 @@ describe("circle.cy.js", () => {
"Verschaffe dir einen Überblick"
);
cy.get('[data-cy="complete-and-continue"]').click({ force: true });
cy.get('[data-cy="circle-title"]').should("contain", "Fahrzeug");
cy.get('[data-cy="ls-continue-button"]').click();
cy.get('[data-cy="lc-title"]').should(
@ -50,6 +51,7 @@ describe("circle.cy.js", () => {
"Handlungsfeld «Fahrzeug»"
);
cy.get('[data-cy="complete-and-continue"]').click({ force: true });
cy.get('[data-cy="circle-title"]').should("contain", "Fahrzeug");
cy.get(
'[data-cy="test-lehrgang-lp-circle-fahrzeug-lc-verschaffe-dir-einen-überblick-checkbox"]'
@ -87,4 +89,35 @@ describe("circle.cy.js", () => {
cy.get('[data-cy="close-learning-content"]').click();
cy.get('[data-cy="circle-title"]').should("contain", "Fahrzeug");
});
it("checks number of sequences and contents", () => {
cy.get('[data-cy="lp-learning-sequence"]').should("have.length", 3);
cy.get('[data-cy="lp-learning-sequence"]')
.first()
.should("contain", "Vorbereitung");
cy.get('[data-cy="lp-learning-sequence"]')
.eq(1)
.should("contain", "Training");
cy.get('[data-cy="lp-learning-sequence"]')
.last()
.should("contain", "Transfer");
cy.get('[data-cy="lp-learning-content"]').should("have.length", 10);
cy.get('[data-cy="lp-learning-content"]')
.first()
.should("contain", "Verschaffe dir einen Überblick");
cy.get('[data-cy="lp-learning-content"]')
.eq(4)
.should("contain", "Präsenzkurs Fahrzeug");
cy.get('[data-cy="lp-learning-content"]')
.eq(7)
.should("contain", "Reflexion");
cy.get('[data-cy="lp-learning-content"]')
.last()
.should("contain", "Feedback");
cy.visit("/course/test-lehrgang/learn/reisen");
cy.get('[data-cy="lp-learning-sequence"]').should("have.length", 3);
cy.get('[data-cy="lp-learning-content"]').should("have.length", 7);
});
});

View File

@ -52,4 +52,14 @@ describe("learningPath.cy.js", () => {
.click();
cy.get('[data-cy="circle-title"]').should("contain", "Reisen");
});
it("checks contents", () => {
cy.get('[data-cy="lp-topic"]').should("have.length", 2);
cy.get('[data-cy="lp-topic"]').first().should("contain", "Circle ÜK");
cy.get('[data-cy="lp-topic"]').eq(1).should("contain", "Circle VV");
cy.get(".cy-lp-circle").should("have.length", 2);
cy.get(".cy-lp-circle").first().should("contain", "Fahrzeug");
cy.get(".cy-lp-circle").eq(1).should("contain", "Reisen");
});
});

View File

@ -1,16 +1,20 @@
import graphene
from graphene import List
from graphene_django import DjangoObjectType
from vbv_lernwelt.assignment.graphql.types import AssignmentObjectType
from vbv_lernwelt.competence.models import (
ActionCompetence,
CompetenceCertificate,
CompetenceCertificateList,
PerformanceCriteria,
)
from vbv_lernwelt.course.graphql.interfaces import CoursePageInterface
from vbv_lernwelt.learnpath.graphql.types import LearningUnitObjectType
class CompetenceCertificateObjectType(DjangoObjectType):
assignments = List(AssignmentObjectType)
assignments = List(graphene.NonNull(AssignmentObjectType), required=True)
class Meta:
model = CompetenceCertificate
@ -22,11 +26,40 @@ class CompetenceCertificateObjectType(DjangoObjectType):
class CompetenceCertificateListObjectType(DjangoObjectType):
competence_certificates = List(CompetenceCertificateObjectType)
competence_certificates = List(
graphene.NonNull(CompetenceCertificateObjectType), required=True
)
class Meta:
model = CompetenceCertificateList
interfaces = (CoursePageInterface,)
fields = []
def resolve_competence_certificates(self, info):
return CompetenceCertificate.objects.child_of(self)
class PerformanceCriteriaObjectType(DjangoObjectType):
learning_unit = graphene.Field(LearningUnitObjectType)
class Meta:
model = PerformanceCriteria
interfaces = (CoursePageInterface,)
fields = ["competence_id"]
def resolve_learning_unit(self: PerformanceCriteria, info):
return self.learning_unit
class ActionCompetenceObjectType(DjangoObjectType):
performance_criteria = graphene.List(
graphene.NonNull(PerformanceCriteriaObjectType), required=True
)
class Meta:
model = ActionCompetence
interfaces = (CoursePageInterface,)
fields = ["competence_id"]
def resolve_performance_criteria(self, info):
return self.get_children().specific()

View File

@ -105,6 +105,9 @@ class ActionCompetence(CourseBasePage):
use_json_field=True,
)
def get_frontend_url(self):
return f""
content_panels = [
FieldPanel("title"),
FieldPanel("competence_id"),
@ -141,6 +144,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

@ -6,7 +6,7 @@ from graphene_django import DjangoObjectType
from vbv_lernwelt.core.models import User
class UserType(DjangoObjectType):
class UserObjectType(DjangoObjectType):
class Meta:
model = User
fields = (

View File

@ -354,15 +354,15 @@ In diesem Circle erfährst du wie die überbetrieblichen Kurse aufgebaut sind. Z
],
)
LearningUnitFactory(title="Kompetenznachweis", title_hidden=True, parent=circle)
LearningContentEdoniqTestFactory(
title="Wissens- und Verständnisfragen",
parent=circle,
description=RichText(
"<p>Folgender Test mit Wissens- und Verständnisfragen ist Teil des Kompetenznachweises. Der Test kann nur einmal durchgeführt werden und ist notenrelevant.</p>"
),
checkbox_text="Hiermit bestätige ich, dass ich die Anweisungen verstanden und die Redlichkeitserklärung akzeptiert habe.",
test_url="https://exam.vbv-afa.ch/e-tutor/v4/user/course/pre_course_object?aid=1689096897473,2147466097",
)
# LearningContentEdoniqTestFactory(
# title="Wissens- und Verständnisfragen",
# parent=circle,
# description=RichText(
# "<p>Folgender Test mit Wissens- und Verständnisfragen ist Teil des Kompetenznachweises. Der Test kann nur einmal durchgeführt werden und ist notenrelevant.</p>"
# ),
# checkbox_text="Hiermit bestätige ich, dass ich die Anweisungen verstanden und die Redlichkeitserklärung akzeptiert habe.",
# test_url="https://exam.vbv-afa.ch/e-tutor/v4/user/course/pre_course_object?aid=1689096897473,2147466097",
# )
LearningUnitFactory(title="Feedback", title_hidden=True, parent=circle)
LearningContentFeedbackFactory(
parent=circle,

View File

@ -5,14 +5,13 @@ from vbv_lernwelt.learnpath.models import Circle
class CoursePageInterface(graphene.Interface):
id = graphene.ID()
title = graphene.String()
slug = graphene.String()
content_type = graphene.String()
live = graphene.Boolean()
translation_key = graphene.String()
frontend_url = graphene.String()
circle = graphene.Field("vbv_lernwelt.learnpath.graphql.types.CircleObjectType")
id = graphene.ID(required=True)
title = graphene.String(required=True)
slug = graphene.String(required=True)
content_type = graphene.String(required=True)
live = graphene.Boolean(required=True)
translation_key = graphene.String(required=True)
frontend_url = graphene.String(required=True)
course = graphene.Field("vbv_lernwelt.course.graphql.types.CourseObjectType")
def resolve_frontend_url(self, info):
@ -21,11 +20,5 @@ class CoursePageInterface(graphene.Interface):
def resolve_content_type(self, info):
return get_django_content_type(self)
def resolve_circle(self, info):
circle = self.get_ancestors().exact_type(Circle).first()
if circle:
return circle.specific
return None
def resolve_course(self, info):
return self.get_course()

View File

@ -1,22 +1,62 @@
import graphene
from django.db.models import Q
from graphql import GraphQLError
from vbv_lernwelt.course.graphql.types import CourseObjectType, CourseSessionObjectType
from vbv_lernwelt.course.models import Course, CourseSession
from vbv_lernwelt.course.permissions import has_course_access
from vbv_lernwelt.learnpath.graphql.types import (
LearningContentAssignmentObjectType,
LearningContentAttendanceCourseObjectType,
LearningContentDocumentListObjectType,
LearningContentEdoniqTestObjectType,
LearningContentFeedbackObjectType,
LearningContentLearningModuleObjectType,
LearningContentMediaLibraryObjectType,
LearningContentPlaceholderObjectType,
LearningContentRichTextObjectType,
LearningContentVideoObjectType,
)
class CourseQuery(graphene.ObjectType):
course = graphene.Field(CourseObjectType, id=graphene.ID())
course = graphene.Field(
CourseObjectType,
id=graphene.ID(),
slug=graphene.String(),
)
course_session = graphene.Field(CourseSessionObjectType, id=graphene.ID())
def resolve_course(root, info, id):
course = Course.objects.get(pk=id)
def resolve_course(self, info, id=None, slug=None):
if id is None and slug is None:
raise GraphQLError("Either 'id', 'slug' must be provided.")
course = Course.objects.get(Q(id=id) | Q(slug=slug))
if has_course_access(info.context.user, course):
return course
raise PermissionError("You do not have access to this course")
def resolve_course_session(root, info, id):
def resolve_course_session(self, info, id):
course_session = CourseSession.objects.get(pk=id)
if has_course_access(info.context.user, course_session.course):
return course_session
raise PermissionError("You do not have access to this course session")
# dummy import, so that graphene recognizes the types
learning_content_media_library = graphene.Field(
LearningContentMediaLibraryObjectType
)
learning_content_assignment = graphene.Field(LearningContentAssignmentObjectType)
learning_content_attendance_course = graphene.Field(
LearningContentAttendanceCourseObjectType
)
learning_content_feedback = graphene.Field(LearningContentFeedbackObjectType)
learning_content_learning_module = graphene.Field(
LearningContentLearningModuleObjectType
)
learning_content_placeholder = graphene.Field(LearningContentPlaceholderObjectType)
learning_content_rich_text = graphene.Field(LearningContentRichTextObjectType)
learning_content_test = graphene.Field(LearningContentEdoniqTestObjectType)
learning_content_video = graphene.Field(LearningContentVideoObjectType)
learning_content_document_list = graphene.Field(
LearningContentDocumentListObjectType
)

View File

@ -7,6 +7,7 @@ from graphene_django import DjangoObjectType
from graphql import GraphQLError
from rest_framework.exceptions import PermissionDenied
from vbv_lernwelt.competence.graphql.types import ActionCompetenceObjectType
from vbv_lernwelt.course.models import (
CircleDocument,
Course,
@ -84,30 +85,40 @@ def resolve_course_page(
class CourseObjectType(DjangoObjectType):
learning_path = graphene.Field(LearningPathObjectType)
learning_path = graphene.Field(LearningPathObjectType, required=True)
action_competences = graphene.List(
graphene.NonNull(ActionCompetenceObjectType), required=True
)
class Meta:
model = Course
fields = ("id", "title", "category_name", "slug", "learning_path")
fields = ("id", "title", "category_name", "slug")
def resolve_learning_path(self, info):
return self.get_learning_path()
@staticmethod
def resolve_learning_path(root: Course, info):
return root.get_learning_path()
@staticmethod
def resolve_action_competences(root: Course, info):
return root.get_action_competences()
class CourseSessionUserExpertCircleType(ObjectType):
id = graphene.ID()
title = graphene.String()
slug = graphene.String()
id = graphene.ID(required=True)
title = graphene.String(required=True)
slug = graphene.String(required=True)
class CourseSessionUserObjectsType(DjangoObjectType):
user_id = graphene.UUID()
first_name = graphene.String()
last_name = graphene.String()
email = graphene.String()
avatar_url = graphene.String()
role = graphene.String()
circles = graphene.List(CourseSessionUserExpertCircleType)
user_id = graphene.UUID(required=True)
first_name = graphene.String(required=True)
last_name = graphene.String(required=True)
email = graphene.String(required=True)
avatar_url = graphene.String(required=True)
# role = graphene.String(required=True)
circles = graphene.List(
graphene.NonNull(CourseSessionUserExpertCircleType), required=True
)
class Meta:
model = CourseSessionUser
@ -166,12 +177,16 @@ class CircleDocumentObjectType(DjangoObjectType):
class CourseSessionObjectType(DjangoObjectType):
attendance_courses = graphene.List(CourseSessionAttendanceCourseObjectType)
assignments = graphene.List(CourseSessionAssignmentObjectType)
edoniq_tests = graphene.List(CourseSessionEdoniqTestObjectType)
documents = graphene.List(CircleDocumentObjectType)
users = graphene.List(CourseSessionUserObjectsType)
attendance_courses = graphene.List(
graphene.NonNull(CourseSessionAttendanceCourseObjectType), required=True
)
assignments = graphene.List(
graphene.NonNull(CourseSessionAssignmentObjectType), required=True
)
edoniq_tests = graphene.List(
graphene.NonNull(CourseSessionEdoniqTestObjectType), required=True
)
users = graphene.List(graphene.NonNull(CourseSessionUserObjectsType), required=True)
class Meta:
model = CourseSession
@ -195,10 +210,5 @@ class CourseSessionObjectType(DjangoObjectType):
def resolve_edoniq_tests(self, info):
return CourseSessionEdoniqTest.objects.filter(course_session=self)
def resolve_documents(self, info):
return CircleDocument.objects.filter(
course_session=self, file__upload_finished_at__isnull=False
)
def resolve_users(self, info):
return CourseSessionUser.objects.filter(course_session_id=self.id).distinct()

View File

@ -27,26 +27,18 @@ class Course(models.Model):
def get_course_url(self):
return f"/course/{self.slug}"
def get_cockpit_url(self):
return f"/{self.get_course_url()}/cockpit"
def get_learning_path(self):
from vbv_lernwelt.learnpath.models import LearningPath
return self.coursepage.get_children().exact_type(LearningPath).first().specific
def get_learning_path_url(self):
return self.get_learning_path().get_frontend_url()
def get_action_competences(self):
from vbv_lernwelt.competence.models import ActionCompetence
def get_cockpit_url(self):
return self.get_learning_path().get_cockpit_url()
def get_competence_url(self):
from vbv_lernwelt.competence.models import ActionCompetenceListPage
competence_page = (
self.coursepage.get_descendants()
.exact_type(ActionCompetenceListPage)
.first()
)
return competence_page.specific.get_frontend_url()
return self.coursepage.get_descendants().exact_type(ActionCompetence).specific()
def get_media_library_url(self):
from vbv_lernwelt.media_library.models import MediaLibraryPage

View File

@ -33,13 +33,14 @@ class CourseSessionAttendanceCourseObjectType(DjangoObjectType):
AttendanceUserObjectType, source="attendance_user_list"
)
due_date = graphene.Field(DueDateObjectType)
learning_content = graphene.Field(LearningContentAttendanceCourseObjectType)
learning_content = graphene.Field(
LearningContentAttendanceCourseObjectType, required=True
)
class Meta:
model = CourseSessionAttendanceCourse
fields = (
"id",
"course_session_id",
"learning_content",
"location",
"trainer",
@ -53,17 +54,18 @@ class CourseSessionAttendanceCourseObjectType(DjangoObjectType):
class CourseSessionAssignmentObjectType(DjangoObjectType):
course_session_id = graphene.ID(source="course_session_id")
learning_content_id = graphene.ID(source="learning_content_id")
course_session_id = graphene.ID(source="course_session_id", required=True)
learning_content_id = graphene.ID(source="learning_content_id", required=True)
submission_deadline = graphene.Field(DueDateObjectType)
evaluation_deadline = graphene.Field(DueDateObjectType)
learning_content = graphene.Field(LearningContentAssignmentObjectType)
learning_content = graphene.Field(
LearningContentAssignmentObjectType, required=True
)
class Meta:
model = CourseSessionAssignment
fields = (
"id",
"course_session_id",
"learning_content",
"submission_deadline",
"evaluation_deadline",
@ -71,16 +73,18 @@ class CourseSessionAssignmentObjectType(DjangoObjectType):
class CourseSessionEdoniqTestObjectType(DjangoObjectType):
course_session_id = graphene.ID(source="course_session_id")
learning_content_id = graphene.ID(source="learning_content_id")
course_session_id = graphene.ID(source="course_session_id", required=True)
learning_content_id = graphene.ID(source="learning_content_id", required=True)
deadline = graphene.Field(DueDateObjectType)
learning_content = graphene.Field(LearningContentEdoniqTestObjectType)
learning_content = graphene.Field(
LearningContentEdoniqTestObjectType, required=True
)
class Meta:
model = CourseSessionEdoniqTest
fields = (
"id",
"course_session_id",
"learning_content",
"deadline",
)

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

@ -13,7 +13,6 @@ from vbv_lernwelt.course.models import CourseCategory, CoursePage
from vbv_lernwelt.learnpath.tests.learning_path_factories import (
CircleFactory,
LearningContentAssignmentFactory,
LearningContentEdoniqTestFactory,
LearningContentFeedbackFactory,
LearningContentLearningModuleFactory,
LearningContentMediaLibraryFactory,
@ -362,14 +361,13 @@ def create_circle_fahrzeug(lp, title="Fahrzeug"):
# checkbox_text="Hiermit bestätige ich, dass ich die Anweisungen verstanden und die Redlichkeitserklärung akzeptiert habe.",
# content_url="https://s3.eu-central-1.amazonaws.com/myvbv-wbt.iterativ.ch/fachcheck-fahrzeug-xapi-LFv8YiyM/index.html#/",
# )
LearningContentAssignmentFactory(
title="Reflexion",
assignment_type="REFLECTION",
parent=circle,
content_assignment=Assignment.objects.get(
slug__startswith=f"{circle.get_course().slug}-assignment-reflexion"
),
),
# LearningContentAssignmentFactory(
# title="Reflexion",
# assignment_type="REFLECTION",
# content_assignment=Assignment.objects.get(
# slug__startswith=f"{circle.get_course().slug}-assignment-reflexion"
# ),
# ),
LearningContentFeedbackFactory(
parent=circle,
)

View File

@ -1,21 +1,8 @@
import graphene
from vbv_lernwelt.course.graphql.types import resolve_course_page
from vbv_lernwelt.learnpath.graphql.types import (
CircleObjectType,
LearningContentAssignmentObjectType,
LearningContentAttendanceCourseObjectType,
LearningContentDocumentListObjectType,
LearningContentEdoniqTestObjectType,
LearningContentFeedbackObjectType,
LearningContentLearningModuleObjectType,
LearningContentMediaLibraryObjectType,
LearningContentPlaceholderObjectType,
LearningContentRichTextObjectType,
LearningContentVideoObjectType,
LearningPathObjectType,
)
from vbv_lernwelt.learnpath.models import Circle, LearningPath
from vbv_lernwelt.learnpath.graphql.types import LearningPathObjectType
from vbv_lernwelt.learnpath.models import LearningPath
class LearningPathQuery:
@ -26,10 +13,6 @@ class LearningPathQuery:
course_id=graphene.ID(),
course_slug=graphene.String(),
)
circle = graphene.Field(CircleObjectType, id=graphene.ID(), slug=graphene.String())
def resolve_circle(root, info, id=None, slug=None):
return resolve_course_page(Circle, root, info, id=id, slug=slug)
def resolve_learning_path(
root, info, id=None, slug=None, course_id=None, course_slug=None
@ -43,53 +26,3 @@ class LearningPathQuery:
course_id=course_id,
course_slug=course_slug,
)
# dummy import, so that graphene recognizes the types
learning_content_media_library = graphene.Field(
LearningContentMediaLibraryObjectType
)
learning_content_assignment = graphene.Field(LearningContentAssignmentObjectType)
learning_content_attendance_course = graphene.Field(
LearningContentAttendanceCourseObjectType
)
learning_content_feedback = graphene.Field(LearningContentFeedbackObjectType)
learning_content_learning_module = graphene.Field(
LearningContentLearningModuleObjectType
)
learning_content_placeholder = graphene.Field(LearningContentPlaceholderObjectType)
learning_content_rich_text = graphene.Field(LearningContentRichTextObjectType)
learning_content_test = graphene.Field(LearningContentEdoniqTestObjectType)
learning_content_video = graphene.Field(LearningContentVideoObjectType)
learning_content_document_list = graphene.Field(
LearningContentDocumentListObjectType
)
def resolve_learning_content_media_library(self, info):
return None
def resolve_learning_content_assignment(self, info):
return None
def resolve_learning_content_attendance_course(self, info):
return None
def resolve_learning_content_feedback(self, info):
return None
def resolve_learning_content_learning_module(self, info):
return None
def resolve_learning_content_placeholder(self, info):
return None
def resolve_learning_content_rich_text(self, info):
return None
def resolve_learning_content_test(self, info):
return None
def resolve_learning_content_video(self, info):
return None
def resolve_learning_content_document_list(self, info):
return None

View File

@ -25,10 +25,22 @@ from vbv_lernwelt.learnpath.models import (
logger = structlog.get_logger(__name__)
class CircleLightObjectType(graphene.ObjectType):
id = graphene.ID(required=True)
title = graphene.String(required=True)
slug = graphene.String(required=True)
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(CircleLightObjectType)
@staticmethod
def resolve_circle(root, info):
return root.get_parent_circle()
@classmethod
def resolve_type(cls, instance, info):
@ -63,96 +75,156 @@ class LearningContentInterface(CoursePageInterface):
class LearningContentAttendanceCourseObjectType(DjangoObjectType):
class Meta:
model = LearningContentAttendanceCourse
interfaces = (LearningContentInterface,)
interfaces = (
CoursePageInterface,
LearningContentInterface,
)
fields = []
class LearningContentVideoObjectType(DjangoObjectType):
class Meta:
model = LearningContentVideo
interfaces = (LearningContentInterface,)
interfaces = (
CoursePageInterface,
LearningContentInterface,
)
fields = []
class LearningContentPlaceholderObjectType(DjangoObjectType):
class Meta:
model = LearningContentPlaceholder
interfaces = (LearningContentInterface,)
interfaces = (
CoursePageInterface,
LearningContentInterface,
)
fields = []
class LearningContentFeedbackObjectType(DjangoObjectType):
class Meta:
model = LearningContentFeedback
interfaces = (LearningContentInterface,)
interfaces = (
CoursePageInterface,
LearningContentInterface,
)
fields = []
class LearningContentLearningModuleObjectType(DjangoObjectType):
class Meta:
model = LearningContentLearningModule
interfaces = (LearningContentInterface,)
interfaces = (
CoursePageInterface,
LearningContentInterface,
)
fields = []
class LearningContentMediaLibraryObjectType(DjangoObjectType):
class Meta:
model = LearningContentMediaLibrary
interfaces = (LearningContentInterface,)
interfaces = (
CoursePageInterface,
LearningContentInterface,
)
fields = []
class LearningContentEdoniqTestObjectType(DjangoObjectType):
competence_certificate = graphene.Field(
"vbv_lernwelt.competence.graphql.types.CompetenceCertificateObjectType",
)
has_extended_time_test = graphene.Boolean(required=True)
class Meta:
model = LearningContentEdoniqTest
interfaces = (LearningContentInterface,)
fields = [
"content_assignment",
]
interfaces = (
CoursePageInterface,
LearningContentInterface,
)
fields = ["content_assignment", "checkbox_text", "has_extended_time_test"]
def resolve_competence_certificate(self: LearningContentEdoniqTest, info):
return self.content_assignment.competence_certificate
def resolve_has_extended_time_test(self: LearningContentEdoniqTest, info):
return self.has_extended_time_test
class LearningContentRichTextObjectType(DjangoObjectType):
class Meta:
model = LearningContentRichText
interfaces = (LearningContentInterface,)
fields = []
interfaces = (
CoursePageInterface,
LearningContentInterface,
)
fields = ["text"]
class LearningContentAssignmentObjectType(DjangoObjectType):
competence_certificate = graphene.Field(
"vbv_lernwelt.competence.graphql.types.CompetenceCertificateObjectType",
)
class Meta:
model = LearningContentAssignment
interfaces = (LearningContentInterface,)
interfaces = (
CoursePageInterface,
LearningContentInterface,
)
fields = [
"content_assignment",
"assignment_type",
]
def resolve_competence_certificate(self: LearningContentAssignment, info):
return self.content_assignment.competence_certificate
class LearningContentDocumentListObjectType(DjangoObjectType):
class Meta:
model = LearningContentDocumentList
interfaces = (LearningContentInterface,)
interfaces = (
CoursePageInterface,
LearningContentInterface,
)
fields = []
class LearningUnitObjectType(DjangoObjectType):
learning_contents = graphene.List(LearningContentInterface)
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:
model = LearningUnit
interfaces = (CoursePageInterface,)
fields = []
fields = ["evaluate_url", "title_hidden"]
@staticmethod
def resolve_learning_contents(root: LearningUnit, info, **kwargs):
def resolve_evaluate_url(self: LearningUnit, info, **kwargs):
return self.get_evaluate_url()
def resolve_performance_criteria(self: LearningUnit, info, **kwargs):
return self.performancecriteria_set.all()
def resolve_learning_contents(self: LearningUnit, info, **kwargs):
siblings = None
if hasattr(info.context, "circle_descendants"):
circle_descendants = info.context.circle_descendants
index = circle_descendants.index(root)
index = circle_descendants.index(self)
siblings = circle_descendants[index + 1 :]
if not siblings:
siblings = root.get_siblings().live().specific()
siblings = self.get_siblings().live().specific()
learning_contents = []
for sibling in siblings:
@ -167,7 +239,9 @@ class LearningUnitObjectType(DjangoObjectType):
class LearningSequenceObjectType(DjangoObjectType):
learning_units = graphene.List(LearningUnitObjectType)
learning_units = graphene.List(
graphene.NonNull(LearningUnitObjectType), required=True
)
class Meta:
model = LearningSequence
@ -196,7 +270,9 @@ class LearningSequenceObjectType(DjangoObjectType):
class CircleObjectType(DjangoObjectType):
learning_sequences = graphene.List(LearningSequenceObjectType)
learning_sequences = graphene.List(
graphene.NonNull(LearningSequenceObjectType), required=True
)
class Meta:
model = Circle
@ -206,21 +282,23 @@ class CircleObjectType(DjangoObjectType):
"goals",
]
@staticmethod
def resolve_learning_sequences(root: Circle, info, **kwargs):
def resolve_learning_sequences(self: Circle, info, **kwargs):
circle_descendants = None
if hasattr(info.context, "learning_path_descendants"):
children = info.context.learning_path_descendants
circle_start_index = children.index(root)
circle_start_index = children.index(self)
next_children = children[circle_start_index + 1 :]
next_circle_index = find_first_index(
children[circle_start_index + 1 :],
next_children,
pred=lambda child: child.specific_class == Circle,
)
circle_descendants = children[circle_start_index + 1 : next_circle_index]
circle_descendants = next_children[0:next_circle_index]
if circle_descendants[-1].specific_class == Topic:
circle_descendants = circle_descendants[:-1]
if not circle_descendants:
circle_descendants = list(root.get_descendants().live().specific())
circle_descendants = list(self.get_descendants().live().specific())
# store flattened descendents to improve performance (no need for db queries)
info.context.circle_descendants = list(circle_descendants)
@ -233,7 +311,7 @@ class CircleObjectType(DjangoObjectType):
class TopicObjectType(DjangoObjectType):
circles = graphene.List(CircleObjectType)
circles = graphene.List(graphene.NonNull(CircleObjectType), required=True)
class Meta:
model = Topic
@ -265,11 +343,12 @@ class TopicObjectType(DjangoObjectType):
class LearningPathObjectType(DjangoObjectType):
topics = graphene.List(TopicObjectType)
topics = graphene.List(graphene.NonNull(TopicObjectType), required=True)
class Meta:
model = LearningPath
interfaces = (CoursePageInterface,)
fields = []
@staticmethod
def resolve_topics(root: LearningPath, info, **kwargs):

View File

@ -0,0 +1,19 @@
# Generated by Django 3.2.20 on 2023-10-13 23:28
from django.db import migrations
def delete_edoniq_test_without_assigmnet(apps, schema_editor):
LearningContentEdoniqTest = apps.get_model("learnpath", "LearningContentEdoniqTest")
LearningContentEdoniqTest.objects.filter(content_assignment__isnull=True).delete()
class Migration(migrations.Migration):
dependencies = [
("assignment", "0010_assignmentcompletion_edoniq_extended_time_flag"),
("learnpath", "0008_add_edoniq_sequence_id"),
]
operations = [
migrations.RunPython(delete_edoniq_test_without_assigmnet),
]

View File

@ -0,0 +1,21 @@
# Generated by Django 3.2.20 on 2023-10-13 23:32
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("assignment", "0010_assignmentcompletion_edoniq_extended_time_flag"),
("learnpath", "0009_alter_learningcontentedoniqtest_content_assignment"),
]
operations = [
migrations.AlterField(
model_name="learningcontentedoniqtest",
name="content_assignment",
field=models.ForeignKey(
on_delete=django.db.models.deletion.PROTECT, to="assignment.assignment"
),
),
]

View File

@ -35,9 +35,6 @@ class LearningPath(CourseBasePage):
def get_frontend_url(self):
return f"/course/{self.slug.replace('-lp', '')}/learn"
def get_cockpit_url(self):
return f"/course/{self.slug.replace('-lp', '')}/cockpit"
class Topic(CourseBasePage):
serialize_field_names = ["is_visible"]
@ -58,6 +55,9 @@ class Topic(CourseBasePage):
def get_admin_display_title(self):
return f"Thema: {self.draft_title}"
def get_frontend_url(self):
return ""
class Meta:
verbose_name = "Topic"
@ -372,10 +372,7 @@ class LearningContentEdoniqTest(LearningContent):
content_assignment = models.ForeignKey(
"assignment.Assignment",
# `null=True` is only set because of existing data...
null=True,
blank=True,
on_delete=models.SET_NULL,
on_delete=models.PROTECT,
)
@property