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/static/storybook
/server/vbv_lernwelt/templates/vue/index.html /server/vbv_lernwelt/templates/vue/index.html
/server/vbv_lernwelt/media /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 - cd client
- pwd - pwd
- npm install - npm install
- npm run codegen
- npm test - npm test
js-linting: &js-linting js-linting: &js-linting

View File

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

View File

@ -8,4 +8,4 @@ const schema = readFileSync("./src/gql/schema.graphql", "utf-8");
const minifiedSchema = minifyIntrospectionQuery(getIntrospectedSchema(schema)); const minifiedSchema = minifyIntrospectionQuery(getIntrospectedSchema(schema));
// Write the minified schema to a new file // 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", "storybook": "storybook dev -p 6006",
"tailwind": "tailwindcss -i tailwind.css -o ../server/vbv_lernwelt/static/css/tailwind.css --watch", "tailwind": "tailwindcss -i tailwind.css -o ../server/vbv_lernwelt/static/css/tailwind.css --watch",
"test": "vitest run", "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": { "dependencies": {
"@headlessui/tailwindcss": "^0.1.3", "@headlessui/tailwindcss": "^0.1.3",

View File

@ -3,7 +3,7 @@ import dayjs from "dayjs";
import LocalizedFormat from "dayjs/plugin/localizedFormat"; import LocalizedFormat from "dayjs/plugin/localizedFormat";
import i18next from "i18next"; import i18next from "i18next";
export const formatDueDate = (start: string, end?: string) => { export const formatDueDate = (start: string, end?: string | null) => {
dayjs.extend(LocalizedFormat); dayjs.extend(LocalizedFormat);
const startDayjs = dayjs(start); const startDayjs = dayjs(start);
const startDateString = getDateString(startDayjs); 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"> <script setup lang="ts">
import LearningPathCircle from "@/pages/learningPath/learningPathPage/LearningPathCircle.vue"; import LearningPathCircle from "@/pages/learningPath/learningPathPage/LearningPathCircle.vue";
import { calculateCircleSectorData } from "@/pages/learningPath/learningPathPage/utils"; import { calculateCircleSectorData } from "@/pages/learningPath/learningPathPage/utils";
import type { LearningPath } from "@/services/learningPath";
import { computed } from "vue"; import { computed } from "vue";
import { useCourseDataWithCompletion } from "@/composables";
export type DiagramType = "horizontal" | "horizontalSmall" | "singleSmall"; export type DiagramType = "horizontal" | "horizontalSmall" | "singleSmall";
export interface Props { export interface Props {
diagramType?: DiagramType;
learningPath: LearningPath;
// set to undefined (default) to show all circles
showCircleSlugs?: string[]; showCircleSlugs?: string[];
courseSlug: string;
courseSessionId: string;
userId?: string;
diagramType?: DiagramType;
} }
const props = withDefaults(defineProps<Props>(), { const props = withDefaults(defineProps<Props>(), {
diagramType: "horizontal", diagramType: "horizontal",
showCircleSlugs: undefined, showCircleSlugs: undefined,
userId: undefined,
}); });
const lpQueryResult = useCourseDataWithCompletion(
props.courseSlug,
props.userId,
props.courseSessionId
);
const circles = computed(() => { const circles = computed(() => {
if (props.showCircleSlugs?.length) { if (props.showCircleSlugs?.length) {
return props.learningPath.circles.filter( return (lpQueryResult.circles.value ?? []).filter(
(c) => props.showCircleSlugs?.includes(c.slug) ?? true (c) => props.showCircleSlugs?.includes(c.slug) ?? true
); );
} }
return props.learningPath.circles; return lpQueryResult.circles.value ?? [];
}); });
const wrapperClasses = computed(() => { 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 { 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 { 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 log from "loglevel";
import type { ComputedRef } from "vue"; import type { ComputedRef } from "vue";
import { computed, ref, watchEffect } from "vue"; import { computed, ref, watchEffect } from "vue";
@ -56,19 +74,19 @@ export function useCourseSessionDetailQuery(courSessionId?: string) {
function findAssignment(learningContentId: string) { function findAssignment(learningContentId: string) {
return (courseSessionDetail.value?.assignments ?? []).find((a) => { return (courseSessionDetail.value?.assignments ?? []).find((a) => {
return a.learning_content.id === learningContentId; return a.learning_content?.id === learningContentId;
}); });
} }
function findEdoniqTest(learningContentId: string) { function findEdoniqTest(learningContentId: string) {
return (courseSessionDetail.value?.edoniq_tests ?? []).find((e) => { return (courseSessionDetail.value?.edoniq_tests ?? []).find((e) => {
return e.learning_content.id === learningContentId; return e.learning_content?.id === learningContentId;
}); });
} }
function findAttendanceCourse(learningContentId: string) { function findAttendanceCourse(learningContentId: string) {
return (courseSessionDetail.value?.attendance_courses ?? []).find((e) => { 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, 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 = export const itCheckboxDefaultIconCheckedTailwindClass =
"bg-[url(/static/icons/icon-checkbox-checked.svg)] hover:bg-[url(/static/icons/icon-checkbox-checked-hover.svg)]"; "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 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 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 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 ...CoursePageFields\n circle {\n id\n title\n slug\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 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 courseSessionDetail($courseSessionId: ID!) {\n course_session(id: $courseSessionId) {\n id\n title\n course {\n id\n title\n slug\n }\n users {\n id\n user_id\n first_name\n last_name\n email\n avatar_url\n role\n circles {\n id\n title\n slug\n }\n }\n attendance_courses {\n id\n location\n trainer\n due_date {\n id\n start\n end\n }\n learning_content_id\n learning_content {\n id\n title\n circle {\n id\n title\n slug\n }\n }\n }\n assignments {\n id\n submission_deadline {\n id\n start\n }\n evaluation_deadline {\n id\n start\n }\n learning_content {\n id\n title\n content_assignment {\n id\n title\n assignment_type\n }\n }\n }\n edoniq_tests {\n id\n deadline {\n id\n start\n end\n }\n learning_content {\n id\n title\n content_assignment {\n id\n title\n assignment_type\n }\n }\n }\n }\n }\n": types.CourseSessionDetailDocument, "\n query courseSessionDetail($courseSessionId: ID!) {\n course_session(id: $courseSessionId) {\n id\n title\n course {\n id\n title\n slug\n }\n users {\n id\n user_id\n first_name\n last_name\n email\n avatar_url\n role\n circles {\n id\n title\n slug\n }\n }\n attendance_courses {\n id\n location\n trainer\n due_date {\n id\n start\n end\n }\n learning_content_id\n learning_content {\n id\n title\n circle {\n id\n title\n slug\n }\n }\n }\n assignments {\n id\n submission_deadline {\n id\n start\n }\n evaluation_deadline {\n id\n start\n }\n learning_content {\n id\n title\n content_assignment {\n id\n title\n assignment_type\n }\n }\n }\n edoniq_tests {\n id\n deadline {\n id\n start\n end\n }\n learning_content {\n id\n title\n content_assignment {\n id\n title\n assignment_type\n }\n }\n }\n }\n }\n": types.CourseSessionDetailDocument,
"\n query 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, "\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. * 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"]; 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 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"];
/** /**
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients. * The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
*/ */
export function graphql(source: "\n query 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"]; 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. * 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 AssignmentAssignmentAssignmentTypeChoices = "AssignmentAssignmentAssignmentTypeChoices";
export const AssignmentAssignmentCompletionCompletionStatusChoices = "AssignmentAssignmentCompletionCompletionStatusChoices"; export const AssignmentAssignmentCompletionCompletionStatusChoices = "AssignmentAssignmentCompletionCompletionStatusChoices";
export const AssignmentCompletionMutation = "AssignmentCompletionMutation"; export const AssignmentCompletionMutation = "AssignmentCompletionMutation";
@ -9,11 +10,12 @@ export const AttendanceUserInputType = "AttendanceUserInputType";
export const AttendanceUserObjectType = "AttendanceUserObjectType"; export const AttendanceUserObjectType = "AttendanceUserObjectType";
export const AttendanceUserStatus = "AttendanceUserStatus"; export const AttendanceUserStatus = "AttendanceUserStatus";
export const Boolean = "Boolean"; export const Boolean = "Boolean";
export const CircleDocumentObjectType = "CircleDocumentObjectType"; export const CircleLightObjectType = "CircleLightObjectType";
export const CircleObjectType = "CircleObjectType"; export const CircleObjectType = "CircleObjectType";
export const CompetenceCertificateListObjectType = "CompetenceCertificateListObjectType"; export const CompetenceCertificateListObjectType = "CompetenceCertificateListObjectType";
export const CompetenceCertificateObjectType = "CompetenceCertificateObjectType"; export const CompetenceCertificateObjectType = "CompetenceCertificateObjectType";
export const CoreUserLanguageChoices = "CoreUserLanguageChoices"; export const CoreUserLanguageChoices = "CoreUserLanguageChoices";
export const CourseCourseSessionUserRoleChoices = "CourseCourseSessionUserRoleChoices";
export const CourseObjectType = "CourseObjectType"; export const CourseObjectType = "CourseObjectType";
export const CoursePageInterface = "CoursePageInterface"; export const CoursePageInterface = "CoursePageInterface";
export const CourseSessionAssignmentObjectType = "CourseSessionAssignmentObjectType"; export const CourseSessionAssignmentObjectType = "CourseSessionAssignmentObjectType";
@ -49,9 +51,10 @@ export const LearningSequenceObjectType = "LearningSequenceObjectType";
export const LearningUnitObjectType = "LearningUnitObjectType"; export const LearningUnitObjectType = "LearningUnitObjectType";
export const LearnpathLearningContentAssignmentAssignmentTypeChoices = "LearnpathLearningContentAssignmentAssignmentTypeChoices"; export const LearnpathLearningContentAssignmentAssignmentTypeChoices = "LearnpathLearningContentAssignmentAssignmentTypeChoices";
export const Mutation = "Mutation"; export const Mutation = "Mutation";
export const PerformanceCriteriaObjectType = "PerformanceCriteriaObjectType";
export const Query = "Query"; export const Query = "Query";
export const SendFeedbackMutation = "SendFeedbackMutation"; export const SendFeedbackMutation = "SendFeedbackMutation";
export const String = "String"; export const String = "String";
export const TopicObjectType = "TopicObjectType"; export const TopicObjectType = "TopicObjectType";
export const UUID = "UUID"; export const UUID = "UUID";
export const UserType = "UserType"; export const UserObjectType = "UserObjectType";

View File

@ -2,7 +2,7 @@
// @ts-ignore // @ts-ignore
import { cacheExchange } from "@urql/exchange-graphcache"; import { cacheExchange } from "@urql/exchange-graphcache";
import { Client, fetchExchange } from "@urql/vue"; import { Client, fetchExchange } from "@urql/vue";
import schema from "../gql/minifiedSchema.json"; import schema from "../gql/dist/minifiedSchema.json";
import { import {
AssignmentCompletionMutation, AssignmentCompletionMutation,
AssignmentCompletionObjectType, 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(` export const COMPETENCE_NAVI_CERTIFICATE_QUERY = graphql(`
query competenceCertificateQuery($courseSlug: String!, $courseSessionId: ID!) { query competenceCertificateQuery($courseSlug: String!, $courseSessionId: ID!) {
competence_certificate_list(course_slug: $courseSlug) { competence_certificate_list(course_slug: $courseSlug) {
@ -109,13 +95,11 @@ export const COMPETENCE_NAVI_CERTIFICATE_QUERY = graphql(`
evaluation_passed evaluation_passed
} }
learning_content { learning_content {
title ...CoursePageFields
id
slug
content_type
frontend_url
circle { 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"> <script setup lang="ts">
import { computed, onMounted, ref, watch } from "vue"; import { computed, onMounted, ref, watch } from "vue";
import { useCourseSessionsStore } from "@/stores/courseSessions"; import { useCourseSessionsStore } from "@/stores/courseSessions";
import { useLearningPathStore } from "@/stores/learningPath";
import { useTranslation } from "i18next-vue"; import { useTranslation } from "i18next-vue";
import ItDropdownSelect from "@/components/ui/ItDropdownSelect.vue"; import ItDropdownSelect from "@/components/ui/ItDropdownSelect.vue";
import type { DueDate } from "@/types"; import type { DueDate } from "@/types";
import DueDatesList from "@/components/dueDates/DueDatesList.vue"; import DueDatesList from "@/components/dueDates/DueDatesList.vue";
import { useCourseData } from "@/composables";
const { t } = useTranslation(); const { t } = useTranslation();
const UNFILTERED = Number.MAX_SAFE_INTEGER.toString(); const UNFILTERED = Number.MAX_SAFE_INTEGER.toString();
const courseSessionsStore = useCourseSessionsStore(); const courseSessionsStore = useCourseSessionsStore();
const learningPathStore = useLearningPathStore();
type Item = { type Item = {
id: string; id: string;
@ -61,16 +60,15 @@ const circles = ref<Item[]>([initialItemCircle]);
const selectedCircle = ref<Item>(circles.value[0]); const selectedCircle = ref<Item>(circles.value[0]);
async function loadCircleValues() { async function loadCircleValues() {
const data = await learningPathStore.loadLearningPath( if (selectedCourse.value) {
`${selectedCourse.value.slug}-lp`, const learningPathQuery = useCourseData(selectedCourse.value.slug);
undefined, await learningPathQuery.resultPromise;
false,
false
);
if (data) {
circles.value = [ circles.value = [
initialItemCircle, initialItemCircle,
...data.circles.map((circle) => ({ id: circle.id, name: circle.title })), ...(learningPathQuery.circles.value ?? []).map((circle) => ({
id: circle.id,
name: circle.title,
})),
]; ];
} else { } else {
circles.value = [initialItemCircle]; circles.value = [initialItemCircle];

View File

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

View File

@ -1,11 +1,11 @@
<script setup lang="ts"> <script setup lang="ts">
import { useCourseSessionDetailQuery } from "@/composables"; import {
useCourseDataWithCompletion,
useCourseSessionDetailQuery,
} from "@/composables";
import { useCockpitStore } from "@/stores/cockpit"; 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 * as log from "loglevel";
import { onMounted } from "vue"; import { onMounted, ref } from "vue";
log.debug("CockpitParentPage created"); log.debug("CockpitParentPage created");
@ -14,33 +14,28 @@ const props = defineProps<{
}>(); }>();
const cockpitStore = useCockpitStore(); const cockpitStore = useCockpitStore();
const competenceStore = useCompetenceStore();
const learningPathStore = useLearningPathStore();
const courseSessionDetailResult = useCourseSessionDetailQuery(); const courseSessionDetailResult = useCourseSessionDetailQuery();
const loaded = ref(false);
onMounted(async () => { onMounted(async () => {
log.debug("CockpitParentPage mounted", props.courseSlug); log.debug("CockpitParentPage mounted", props.courseSlug);
try { try {
await courseSessionDetailResult.waitForData(); 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( await cockpitStore.loadCircles(
props.courseSlug, props.courseSlug,
courseSessionDetailResult.findCurrentUser() 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) { } catch (error) {
log.error(error); log.error(error);
} }
@ -50,7 +45,9 @@ onMounted(async () => {
<template> <template>
<div class="bg-gray-200"> <div class="bg-gray-200">
<main> <main>
<router-view></router-view> <div v-if="loaded">
<router-view></router-view>
</div>
</main> </main>
</div> </div>
</template> </template>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,8 +1,6 @@
2 2
<script setup lang="ts"> <script setup lang="ts">
import AssignmentSubmissionProgress from "@/pages/cockpit/cockpitPage/AssignmentSubmissionProgress.vue"; import AssignmentSubmissionProgress from "@/pages/cockpit/cockpitPage/AssignmentSubmissionProgress.vue";
import { useLearningPathStore } from "@/stores/learningPath";
import { useUserStore } from "@/stores/user";
import type { import type {
CourseSession, CourseSession,
LearningContent, LearningContent,
@ -13,11 +11,16 @@ import { computed } from "vue";
import { useTranslation } from "i18next-vue"; import { useTranslation } from "i18next-vue";
import FeedbackSubmissionProgress from "@/pages/cockpit/cockpitPage/FeedbackSubmissionProgress.vue"; import FeedbackSubmissionProgress from "@/pages/cockpit/cockpitPage/FeedbackSubmissionProgress.vue";
import { learningContentTypeData } from "@/utils/typeMaps"; import { learningContentTypeData } from "@/utils/typeMaps";
import { useCourseSessionDetailQuery } from "@/composables"; import {
useCourseSessionDetailQuery,
useCourseDataWithCompletion,
} from "@/composables";
import { circleFlatLearningContents } from "@/services/circle";
interface Submittable { interface Submittable {
id: string; id: string;
circleName: string; circleName: string;
circleId: string;
frontendUrl: string; frontendUrl: string;
title: string; title: string;
showDetailsText: string; showDetailsText: string;
@ -32,24 +35,20 @@ const props = defineProps<{
log.debug("SubmissionsOverview created", props.courseSession.id); log.debug("SubmissionsOverview created", props.courseSession.id);
const userStore = useUserStore();
const learningPathStore = useLearningPathStore();
const courseSessionDetailResult = useCourseSessionDetailQuery(); const courseSessionDetailResult = useCourseSessionDetailQuery();
const { t } = useTranslation(); const { t } = useTranslation();
const lpQueryResult = useCourseDataWithCompletion();
const submittables = computed(() => { const submittables = computed(() => {
const learningPath = learningPathStore.learningPathForUser( if (!lpQueryResult.circles.value?.length) {
props.courseSession.course.slug,
userStore.id
);
if (!learningPath) {
return []; return [];
} }
return learningPath.circles return lpQueryResult.circles.value
.filter((circle) => props.selectedCircle == circle.id) .filter((circle) => props.selectedCircle == circle.id)
.flatMap((circle) => { .flatMap((circle) => {
const learningContents = circle.flatLearningContents.filter( const learningContents = circleFlatLearningContents(circle).filter(
(lc) => (lc) =>
lc.content_type === "learnpath.LearningContentAssignment" || lc.content_type === "learnpath.LearningContentAssignment" ||
lc.content_type === "learnpath.LearningContentFeedback" lc.content_type === "learnpath.LearningContentFeedback"
@ -59,10 +58,11 @@ const submittables = computed(() => {
return { return {
id: lc.id, id: lc.id,
circleName: circle.title, circleName: circle.title,
circleId: circle.id,
frontendUrl: lc.frontend_url, frontendUrl: lc.frontend_url,
title: getLearningContentType(lc), title: getLearningContentType(lc),
showDetailsText: getShowDetailsText(lc), showDetailsText: getShowDetailsText(lc),
detailsLink: getDetailsLink(lc), detailsLink: getDetailsLink(lc, circle.id),
content: lc, content: lc,
}; };
}); });
@ -103,9 +103,9 @@ const getShowDetailsText = (lc: LearningContent) => {
return t("Feedback anschauen"); return t("Feedback anschauen");
}; };
const getDetailsLink = (lc: LearningContent) => { const getDetailsLink = (lc: LearningContent, circleId: string) => {
if (isFeedback(lc)) { if (isFeedback(lc)) {
return `cockpit/feedback/${lc.parentCircle.id}`; return `cockpit/feedback/${circleId}`;
} }
return `cockpit/assignment/${lc.id}`; return `cockpit/assignment/${lc.id}`;
}; };
@ -147,7 +147,9 @@ const getIconName = (lc: LearningContent) => {
</div> </div>
<div class="flex flex-col"> <div class="flex flex-col">
<h3 class="text-bold flex items-center gap-2">{{ submittable.title }}</h3> <h3 class="text-bold flex items-center gap-2">{{ submittable.title }}</h3>
<p class="text-gray-800">Circle «{{ submittable.circleName }}»</p> <p class="text-gray-800">
{{ $t("a.Circle") }} «{{ submittable.circleName }}»
</p>
</div> </div>
</div> </div>
<AssignmentSubmissionProgress <AssignmentSubmissionProgress
@ -160,7 +162,7 @@ const getIconName = (lc: LearningContent) => {
<FeedbackSubmissionProgress <FeedbackSubmissionProgress
v-if="isFeedback(submittable.content)" v-if="isFeedback(submittable.content)"
:course-session="props.courseSession" :course-session="props.courseSession"
:circle-id="submittable.content.parentCircle.id" :circle-id="submittable.circleId"
class="grow pr-8" class="grow pr-8"
></FeedbackSubmissionProgress> ></FeedbackSubmissionProgress>
<div class="flex items-center lg:w-1/4 lg:justify-end"> <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"> <script setup lang="ts">
import { useCurrentCourseSession } from "@/composables"; import { useCurrentCourseSession, useCourseData } from "@/composables";
import ItDropdownSelect from "@/components/ui/ItDropdownSelect.vue"; import ItDropdownSelect from "@/components/ui/ItDropdownSelect.vue";
import { useCockpitStore } from "@/stores/cockpit"; import { useCockpitStore } from "@/stores/cockpit";
import ItModal from "@/components/ui/ItModal.vue"; import ItModal from "@/components/ui/ItModal.vue";
@ -16,13 +16,11 @@ import {
} from "@/services/files"; } from "@/services/files";
import { useCourseSessionsStore } from "@/stores/courseSessions"; import { useCourseSessionsStore } from "@/stores/courseSessions";
import DocumentListItem from "@/components/circle/DocumentListItem.vue"; import DocumentListItem from "@/components/circle/DocumentListItem.vue";
import { useCircleStore } from "@/stores/circle";
const cockpitStore = useCockpitStore(); const cockpitStore = useCockpitStore();
const courseSession = useCurrentCourseSession(); const courseSession = useCurrentCourseSession();
const circleStore = useCircleStore();
const courseSessionsStore = useCourseSessionsStore(); const courseSessionsStore = useCourseSessionsStore();
const courseData = useCourseData(courseSession.value?.course.slug);
const { t } = useTranslation(); const { t } = useTranslation();
@ -51,26 +49,18 @@ onMounted(async () => {
await fetchDocuments(); await fetchDocuments();
}); });
watch( const dropdownLearningSequences = computed(() => {
// workaround to load learning sequences when circle changes if (cockpitStore.currentCircle?.slug) {
() => cockpitStore.currentCircle, const circle = courseData.findCircle(cockpitStore.currentCircle?.slug);
async () => { if (circle) {
if (cockpitStore.currentCircle) { return circle.learning_sequences.map((sequence) => ({
await circleStore.loadCircle( id: sequence.id,
courseSession.value?.course.slug, name: `${sequence.title}`,
cockpitStore.currentCircle?.slug }));
);
} }
}, }
{ immediate: true } return [];
); });
const dropdownLearningSequences = computed(() =>
circleStore.circle?.learningSequences.map((sequence) => ({
id: sequence.id,
name: `${sequence.title}`,
}))
);
const circleDocuments = computed(() => { const circleDocuments = computed(() => {
return circleDocumentsResultData.value.filter( return circleDocumentsResultData.value.filter(

View File

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

View File

@ -1,8 +1,8 @@
<script setup lang="ts"> <script setup lang="ts">
import CompetenceDetail from "@/pages/competence/ActionCompetenceDetail.vue"; import CompetenceDetail from "@/pages/competence/ActionCompetenceDetail.vue";
import { useCompetenceStore } from "@/stores/competence";
import * as log from "loglevel"; import * as log from "loglevel";
import { ref } from "vue"; import { ref } from "vue";
import { useCourseDataWithCompletion } from "@/composables";
log.debug("CompetenceListPage created"); log.debug("CompetenceListPage created");
@ -10,7 +10,7 @@ const props = defineProps<{
courseSlug: string; courseSlug: string;
}>(); }>();
const competenceStore = useCompetenceStore(); const courseData = useCourseDataWithCompletion(props.courseSlug);
const isOpenAll = ref(false); const isOpenAll = ref(false);
@ -40,9 +40,9 @@ function toggleOpen() {
</div> </div>
</div> </div>
<ul v-if="competenceStore.competenceProfilePage()"> <ul v-if="courseData.actionCompetences.value?.length">
<li <li
v-for="competence in competenceStore.competences()" v-for="competence in courseData.actionCompetences.value"
:key="competence.id" :key="competence.id"
class="mb-8 bg-white px-8 pt-6" 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 { useQuery } from "@urql/vue";
import { computed } from "vue"; import { computed } from "vue";
import type { CompetenceCertificate } from "@/types"; import type { CompetenceCertificate } from "@/types";
import { useCurrentCourseSession } from "@/composables"; import { useCurrentCourseSession, useCourseDataWithCompletion } from "@/composables";
import { import {
assignmentsMaxEvaluationPoints, assignmentsMaxEvaluationPoints,
assignmentsUserPoints, assignmentsUserPoints,
competenceCertificateProgressStatusCount, competenceCertificateProgressStatusCount,
} from "@/pages/competence/utils"; } from "@/pages/competence/utils";
import ItProgress from "@/components/ui/ItProgress.vue"; import ItProgress from "@/components/ui/ItProgress.vue";
import { useCompetenceStore } from "@/stores/competence"; import { calcPerformanceCriteriaStatusCount } from "@/services/competence";
const props = defineProps<{ const props = defineProps<{
courseSlug: string; courseSlug: string;
@ -20,8 +20,7 @@ const props = defineProps<{
log.debug("CompetenceIndexPage setup", props); log.debug("CompetenceIndexPage setup", props);
const courseSession = useCurrentCourseSession(); const courseSession = useCurrentCourseSession();
const courseData = useCourseDataWithCompletion(props.courseSlug);
const competenceStore = useCompetenceStore();
const certificatesQuery = useQuery({ const certificatesQuery = useQuery({
query: COMPETENCE_NAVI_CERTIFICATE_QUERY, query: COMPETENCE_NAVI_CERTIFICATE_QUERY,
@ -51,7 +50,7 @@ const userPointsEvaluatedAssignments = computed(() => {
}); });
const performanceCriteriaStatusCount = computed(() => { const performanceCriteriaStatusCount = computed(() => {
return competenceStore.calcStatusCount(competenceStore.flatPerformanceCriteria()); return calcPerformanceCriteriaStatusCount(courseData.flatPerformanceCriteria.value);
}); });
</script> </script>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -23,12 +23,14 @@
import DocumentListItem from "@/components/circle/DocumentListItem.vue"; import DocumentListItem from "@/components/circle/DocumentListItem.vue";
import { useCurrentCourseSession } from "@/composables"; import { useCurrentCourseSession } from "@/composables";
import { computed, onMounted, ref } from "vue"; import { computed, onMounted, ref } from "vue";
import { useCircleStore } from "@/stores/circle"; import type { CircleDocument, CircleType } from "@/types";
import type { CircleDocument } from "@/types";
import { fetchCourseSessionDocuments } from "@/services/files"; import { fetchCourseSessionDocuments } from "@/services/files";
const props = defineProps<{
circle: CircleType;
}>();
const courseSession = useCurrentCourseSession(); const courseSession = useCurrentCourseSession();
const circleStore = useCircleStore();
const circleDocumentsResultData = ref<CircleDocument[]>([]); const circleDocumentsResultData = ref<CircleDocument[]>([]);
@ -43,7 +45,7 @@ async function fetchDocuments() {
const circleDocuments = computed(() => { const circleDocuments = computed(() => {
return circleDocumentsResultData.value.filter( 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"> <script setup lang="ts">
import ItCheckbox from "@/components/ui/ItCheckbox.vue";
import LearningContentBadge from "@/pages/learningPath/LearningContentTypeBadge.vue"; import LearningContentBadge from "@/pages/learningPath/LearningContentTypeBadge.vue";
import { showIcon } from "@/pages/learningPath/circlePage/learningSequenceUtils"; import { showIcon } from "@/pages/learningPath/circlePage/learningSequenceUtils";
import { useCircleStore } from "@/stores/circle"; import { useCircleStore } from "@/stores/circle";
import type { import type {
CircleType,
CourseCompletionStatus, CourseCompletionStatus,
LearningContent, LearningContent,
LearningContentAssignment, LearningContentAssignment,
LearningContentEdoniqTest, LearningContentEdoniqTest,
LearningContentInterface, LearningContentWithCompletion,
LearningSequence, LearningSequence,
} from "@/types"; } from "@/types";
import findLast from "lodash/findLast"; import type { Ref } from "vue";
import { computed } from "vue"; import { computed } from "vue";
import { humanizeDuration } from "../../../utils/humanizeDuration";
import { import {
itCheckboxDefaultIconCheckedTailwindClass, itCheckboxDefaultIconCheckedTailwindClass,
itCheckboxDefaultIconUncheckedTailwindClass, itCheckboxDefaultIconUncheckedTailwindClass,
} from "@/constants"; } from "@/constants";
import ItCheckbox from "@/components/ui/ItCheckbox.vue";
import {
allFinishedInLearningSequence,
calcSelfEvaluationStatus,
circleFlatLearningContents,
someFinishedInLearningSequence,
} from "@/services/circle";
import { useCourseDataWithCompletion } from "@/composables";
import { findLastIndex } from "lodash";
type Props = { type Props = {
courseSlug: string;
learningSequence: LearningSequence; learningSequence: LearningSequence;
circle: CircleType;
readonly?: boolean; readonly?: boolean;
}; };
@ -30,65 +40,64 @@ const props = withDefaults(defineProps<Props>(), {
const circleStore = useCircleStore(); const circleStore = useCircleStore();
function toggleCompleted(learningContent: LearningContentInterface) { const lpQueryResult = useCourseDataWithCompletion(props.courseSlug);
function toggleCompleted(learningContent: LearningContentWithCompletion) {
let completionStatus: CourseCompletionStatus = "SUCCESS"; let completionStatus: CourseCompletionStatus = "SUCCESS";
if (learningContent.completion_status === "SUCCESS") { if (learningContent.completion_status === "SUCCESS") {
completionStatus = "FAIL"; completionStatus = "FAIL";
} }
circleStore.markCompletion(learningContent, completionStatus); lpQueryResult.markCompletion(learningContent, completionStatus);
} }
const someFinished = computed(() => { const someFinished = computed(() => {
if (props.learningSequence && circleStore.circle) { if (props.learningSequence) {
return circleStore.circle.someFinishedInLearningSequence( return someFinishedInLearningSequence(props.learningSequence);
props.learningSequence.translation_key
);
} }
return false; return false;
}); });
const allFinished = computed(() => { const allFinished = computed(() => {
if (props.learningSequence && circleStore.circle) { if (props.learningSequence) {
return circleStore.circle.allFinishedInLearningSequence( return allFinishedInLearningSequence(props.learningSequence);
props.learningSequence.translation_key
);
} }
return false; return false;
}); });
const continueTranslationKeyTuple = computed(() => { const continueTranslationKeyTuple: Ref<[string | undefined, boolean]> = computed(() => {
if (props.learningSequence && circleStore.circle) { if (props.learningSequence) {
const lastFinished = findLast( const flatLearningContents = circleFlatLearningContents(props.circle);
circleStore.circle.flatLearningContents, const lastFinishedIndex = findLastIndex(
circleFlatLearningContents(props.circle),
(learningContent) => { (learningContent) => {
return learningContent.completion_status === "SUCCESS"; return learningContent.completion_status === "SUCCESS";
} }
); );
if (!lastFinished) { if (lastFinishedIndex === -1) {
// must be the first // must be the first
return [circleStore.circle.flatLearningContents[0].translation_key, true]; return [flatLearningContents[0].id, true];
} }
if (lastFinished && lastFinished.nextLearningContent) { if (flatLearningContents[lastFinishedIndex + 1]) {
return [lastFinished.nextLearningContent.translation_key, false]; return [flatLearningContents[lastFinishedIndex + 1].id, false];
} }
} }
return ""; return [undefined, false];
}); });
const learningSequenceBorderClass = computed(() => { const learningSequenceBorderClass = computed(() => {
let result: string[] = []; let result: string[] = [];
if (props.learningSequence && circleStore.circle) { if (props.learningSequence) {
if (allFinished.value) { if (allFinished.value) {
result = ["border-l-4", "border-l-green-500"]; result = ["border-l-4", "border-l-green-500"];
} else if (someFinished.value) { } else if (someFinished.value) {
result = ["border-l-4", "border-l-sky-500"]; result = ["border-l-4", "border-l-sky-500"];
} else { } else {
result = ["border-l-gray-500"]; result = ["border-l", "border-l-gray-500"];
} }
} }
@ -123,22 +132,27 @@ function checkboxIconUncheckedTailwindClass(lc: LearningContent) {
</script> </script>
<template> <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"> <div class="mb-2 flex items-center gap-4 text-blue-900">
<component :is="learningSequence.icon" v-if="showIcon(learningSequence.icon)" /> <component :is="learningSequence.icon" v-if="showIcon(learningSequence.icon)" />
<h3 class="text-large font-semibold"> <h3 class="text-large font-semibold">
{{ learningSequence.title }} {{ learningSequence.title }}
</h3> </h3>
<div v-if="learningSequence.minutes > 0"> <!-- <div v-if="learningSequence.minutes > 0">-->
{{ humanizeDuration(learningSequence.minutes) }} <!-- {{ humanizeDuration(learningSequence.minutes) }}-->
</div> <!-- </div>-->
</div> </div>
<ol class="border bg-white px-4 lg:px-6" :class="learningSequenceBorderClass"> <ol class="border bg-white px-4 lg:px-6" :class="learningSequenceBorderClass">
<li <li
v-for="learningUnit in learningSequence.learningUnits" v-for="learningUnit in learningSequence.learning_units"
:id="learningUnit.slug" :id="learningUnit.slug"
:key="learningUnit.id" :key="learningUnit.id"
data-cy="lp-learning-unit"
class="pt-3 lg:pt-6" class="pt-3 lg:pt-6"
> >
<div <div
@ -148,14 +162,15 @@ function checkboxIconUncheckedTailwindClass(lc: LearningContent) {
<div class="font-semibold"> <div class="font-semibold">
{{ learningUnit.title }} {{ learningUnit.title }}
</div> </div>
<div v-if="learningUnit.minutes > 0" class="whitespace-nowrap"> <!-- <div v-if="learningUnit.minutes > 0" class="whitespace-nowrap">-->
{{ humanizeDuration(learningUnit.minutes) }} <!-- {{ humanizeDuration(learningUnit.minutes) }}-->
</div> <!-- </div>-->
</div> </div>
<ol> <ol>
<li <li
v-for="learningContent in learningUnit.learningContents" v-for="learningContent in learningUnit.learning_contents"
:key="learningContent.id" :key="learningContent.id"
data-cy="lp-learning-content"
> >
<div class="pb-6"> <div class="pb-6">
<div class="flex items-center gap-4"> <div class="flex items-center gap-4">
@ -181,15 +196,15 @@ function checkboxIconUncheckedTailwindClass(lc: LearningContent) {
" "
@toggle="toggleCompleted(learningContent)" @toggle="toggleCompleted(learningContent)"
@click=" @click="
(event: MouseEvent) => { (event: MouseEvent) => {
// when disabled open the learning content directly // when disabled open the learning content directly
if (!learningContent.can_user_self_toggle_course_completion) { if (!learningContent.can_user_self_toggle_course_completion) {
circleStore.openLearningContent(learningContent); circleStore.openLearningContent(learningContent);
event.preventDefault(); event.preventDefault();
event.stopPropagation(); event.stopPropagation();
} }
} }
" "
/> />
<div <div
class="flex flex-auto flex-col gap-4 xl:flex-row xl:justify-between" class="flex flex-auto flex-col gap-4 xl:flex-row xl:justify-between"
@ -236,7 +251,7 @@ function checkboxIconUncheckedTailwindClass(lc: LearningContent) {
<div <div
v-if=" v-if="
learningContent.translation_key === continueTranslationKeyTuple[0] && learningContent.id === continueTranslationKeyTuple[0] &&
!props.readonly !props.readonly
" "
class="my-4" class="my-4"
@ -259,20 +274,20 @@ function checkboxIconUncheckedTailwindClass(lc: LearningContent) {
</ol> </ol>
<div <div
v-if="learningUnit.children.length" v-if="learningUnit.performance_criteria.length"
:class="{ 'cursor-pointer': !props.readonly }" :class="{ 'cursor-pointer': !props.readonly }"
:data-cy="`${learningUnit.slug}`" :data-cy="`${learningUnit.slug}`"
@click="!props.readonly && circleStore.openSelfEvaluation(learningUnit)" @click="!props.readonly && circleStore.openSelfEvaluation(learningUnit)"
> >
<div <div
v-if="circleStore.calcSelfEvaluationStatus(learningUnit) === 'SUCCESS'" v-if="calcSelfEvaluationStatus(learningUnit) === 'SUCCESS'"
class="self-evaluation-success flex items-center gap-4 pb-6" class="self-evaluation-success flex items-center gap-4 pb-6"
> >
<it-icon-smiley-happy class="mr-4 h-8 w-8 flex-none" data-cy="success" /> <it-icon-smiley-happy class="mr-4 h-8 w-8 flex-none" data-cy="success" />
<div>{{ $t("selfEvaluation.selfEvaluationYes") }}</div> <div>{{ $t("selfEvaluation.selfEvaluationYes") }}</div>
</div> </div>
<div <div
v-else-if="circleStore.calcSelfEvaluationStatus(learningUnit) === 'FAIL'" v-else-if="calcSelfEvaluationStatus(learningUnit) === 'FAIL'"
class="self-evaluation-fail flex items-center gap-4 pb-6" class="self-evaluation-fail flex items-center gap-4 pb-6"
> >
<it-icon-smiley-thinking class="mr-4 h-8 w-8 flex-none" data-cy="fail" /> <it-icon-smiley-thinking class="mr-4 h-8 w-8 flex-none" data-cy="fail" />
@ -284,7 +299,7 @@ function checkboxIconUncheckedTailwindClass(lc: LearningContent) {
</div> </div>
</div> </div>
<hr v-if="!learningUnit.last" class="-mx-4 text-gray-500" /> <!-- <hr v-if="!learningUnit.last" class="-mx-4 text-gray-500" />-->
</li> </li>
</ol> </ol>
</div> </div>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,11 +1,30 @@
<script setup lang="ts"> <script setup lang="ts">
import MediaLink from "@/components/mediaLibrary/MediaLink.vue"; 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 LearningContentSimpleLayout from "../layouts/LearningContentSimpleLayout.vue";
import { onMounted, ref } from "vue";
import log from "loglevel";
import { itGetCached } from "@/fetchHelpers";
const props = defineProps<{ const props = defineProps<{
content: LearningContentDocumentList; 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> </script>
<template> <template>
@ -19,7 +38,7 @@ const props = defineProps<{
<div> <div>
<ul class="border-t"> <ul class="border-t">
<li <li
v-for="item in content.documents" v-for="item in documents"
:key="item.id" :key="item.id"
class="flex items-center justify-between border-b py-4" 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 { useCourseSessionDetailQuery, useCurrentCourseSession } from "@/composables";
import dayjs from "dayjs"; import dayjs from "dayjs";
import ItSuccessAlert from "@/components/ui/ItSuccessAlert.vue"; import ItSuccessAlert from "@/components/ui/ItSuccessAlert.vue";
import { import { formatDueDate, getDateString } from "@/components/dueDates/dueDatesUtils";
formatDueDate,
getDateString,
} from "../../../../components/dueDates/dueDatesUtils";
const { t } = useTranslation(); const { t } = useTranslation();
@ -33,7 +30,7 @@ const queryResult = useQuery({
query: ASSIGNMENT_COMPLETION_QUERY, query: ASSIGNMENT_COMPLETION_QUERY,
variables: { variables: {
courseSessionId: courseSession.value.id, courseSessionId: courseSession.value.id,
assignmentId: props.content.content_assignment_id, assignmentId: props.content.content_assignment.id,
learningContentId: props.content.id, learningContentId: props.content.id,
}, },
}); });
@ -52,9 +49,12 @@ const extendedTimeTest = ref(false);
const deadlineInPast = computed(() => { const deadlineInPast = computed(() => {
// with 16 minutes buffer // with 16 minutes buffer
return dayjs(courseSessionEdoniqTest.value?.deadline.start) if (courseSessionEdoniqTest.value?.deadline?.start) {
.add(16, "minute") return dayjs(courseSessionEdoniqTest.value?.deadline.start)
.isBefore(dayjs()); .add(16, "minute")
.isBefore(dayjs());
}
return false;
}); });
async function startTest() { async function startTest() {
@ -89,7 +89,7 @@ async function startTest() {
<p class="mt-2 text-lg"> <p class="mt-2 text-lg">
{{ {{
$t("edoniqTest.submitDateDescription", { $t("edoniqTest.submitDateDescription", {
x: formatDueDate(courseSessionEdoniqTest.deadline.start), x: formatDueDate(courseSessionEdoniqTest?.deadline?.start ?? ""),
}) })
}} }}
</p> </p>
@ -157,7 +157,7 @@ async function startTest() {
</div> </div>
<div v-else> <div v-else>
{{ $t("a.Abgabetermin") }}: {{ $t("a.Abgabetermin") }}:
{{ getDateString(dayjs(courseSessionEdoniqTest?.deadline.start)) }} {{ getDateString(dayjs(courseSessionEdoniqTest?.deadline?.start)) }}
</div> </div>
</div> </div>
</div> </div>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -2,9 +2,8 @@
import * as log from "loglevel"; import * as log from "loglevel";
import SelfEvaluation from "@/pages/learningPath/selfEvaluationPage/SelfEvaluation.vue"; import SelfEvaluation from "@/pages/learningPath/selfEvaluationPage/SelfEvaluation.vue";
import { useCircleStore } from "@/stores/circle"; import { computed } from "vue";
import type { LearningUnit } from "@/types"; import { useCourseDataWithCompletion } from "@/composables";
import { onMounted, reactive } from "vue";
log.debug("LearningUnitSelfEvaluationView created"); log.debug("LearningUnitSelfEvaluationView created");
@ -14,32 +13,21 @@ const props = defineProps<{
learningUnitSlug: string; learningUnitSlug: string;
}>(); }>();
const circleStore = useCircleStore(); const courseData = useCourseDataWithCompletion(props.courseSlug);
const learningUnit = computed(() =>
const state: { learningUnit?: LearningUnit } = reactive({}); courseData.findLearningUnit(props.learningUnitSlug, props.circleSlug)
);
onMounted(async () => { const circle = computed(() => {
log.debug( return courseData.findCircle(props.circleSlug);
"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);
}
}); });
</script> </script>
<template> <template>
<SelfEvaluation v-if="state.learningUnit" :learning-unit="state.learningUnit" /> <SelfEvaluation
v-if="learningUnit && circle"
:learning-unit="learningUnit"
:circle="circle"
/>
</template> </template>
<style lang="postcss" scoped></style> <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 { useCourseSessionDetailQuery } from "@/composables";
import { itGet } from "@/fetchHelpers"; import { itGet } from "@/fetchHelpers";
import type { LearningPath } from "@/services/learningPath";
import type { import type {
Assignment, Assignment,
AssignmentCompletion, AssignmentCompletion,
CourseSessionUser, CourseSessionUser,
LearningContentAssignment,
UserAssignmentCompletionStatus, UserAssignmentCompletionStatus,
} from "@/types"; } from "@/types";
import { sum } from "d3"; import { sum } from "d3";
@ -16,17 +14,6 @@ export interface GradedUser {
points: number; 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( export async function loadAssignmentCompletionStatusData(
assignmentId: string, assignmentId: string,
courseSessionId: string, courseSessionId: string,

View File

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

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 { import type {
CourseCompletionStatus, CircleType,
LearningContentInterface, LearningContent,
LearningContentWithCompletion,
LearningUnit, LearningUnit,
LearningUnitPerformanceCriteria,
PerformanceCriteria,
} from "@/types"; } from "@/types";
import * as log from "loglevel"; import * as log from "loglevel";
import { defineStore } from "pinia"; import { defineStore } from "pinia";
import type { RouteLocationNormalized } from "vue-router"; import type { RouteLocationNormalized } from "vue-router";
export type CircleStoreState = { export type CircleStoreState = {
circle: Circle | undefined;
page: "INDEX" | "OVERVIEW"; page: "INDEX" | "OVERVIEW";
}; };
function createLearningUnitHash(learningUnit: LearningUnit | undefined) { function createLearningUnitHash(
const luSlug = learningUnit?.slug; circle: CircleType,
const circleSlug = learningUnit?.parentCircle?.slug; learningUnitSlug: string | undefined
if (luSlug && circleSlug) { ) {
return "#" + luSlug.replace(`${circleSlug}-`, ""); if (learningUnitSlug && circle) {
return "#" + learningUnitSlug.replace(`${circle.slug}-`, "");
} }
return ""; return "";
@ -32,111 +27,30 @@ export const useCircleStore = defineStore({
id: "circle", id: "circle",
state: () => { state: () => {
return { return {
circle: undefined,
page: "INDEX", page: "INDEX",
} as CircleStoreState; } as CircleStoreState;
}, },
getters: {}, getters: {},
actions: { actions: {
async loadCircle( openLearningContent(learningContent: LearningContent) {
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) {
this.router.push({ this.router.push({
path: learningContent.frontend_url, path: learningContent.frontend_url,
}); });
}, },
closeLearningContent( closeLearningContent(
learningContent: LearningContentInterface, learningContent: LearningContentWithCompletion,
circle: CircleType,
returnRoute?: RouteLocationNormalized returnRoute?: RouteLocationNormalized
) { ) {
if (returnRoute) { if (returnRoute) {
this.router.push(returnRoute); this.router.push(returnRoute);
} else { } else {
this.router.push({ this.router.push({
path: `${this.circle?.frontend_url}`, path: `${circle.frontend_url}`,
hash: createLearningUnitHash(learningContent.parentLearningUnit), hash: createLearningUnitHash(
circle,
learningContent.parentLearningUnit?.slug
),
}); });
} }
}, },
@ -147,50 +61,37 @@ export const useCircleStore = defineStore({
}, },
closeSelfEvaluation( closeSelfEvaluation(
learningUnit: LearningUnit, learningUnit: LearningUnit,
circle: CircleType,
returnRoute?: RouteLocationNormalized returnRoute?: RouteLocationNormalized
) { ) {
if (returnRoute) { if (returnRoute) {
this.router.push(returnRoute); this.router.push(returnRoute);
} else { } else {
this.router.push({ this.router.push({
path: `${this.circle?.frontend_url}`, path: `${circle.frontend_url}`,
hash: createLearningUnitHash(learningUnit), 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( continueFromLearningContent(
currentLearningContent: LearningContentInterface, currentLearningContent: LearningContentWithCompletion,
returnRoute?: RouteLocationNormalized circle: CircleType,
returnRoute?: RouteLocationNormalized,
markCompletionFn?: (learningContent: LearningContentWithCompletion) => void
) { ) {
if (currentLearningContent) { if (currentLearningContent) {
if (currentLearningContent.can_user_self_toggle_course_completion) { if (currentLearningContent.can_user_self_toggle_course_completion) {
this.markCompletion(currentLearningContent, "SUCCESS"); if (markCompletionFn) {
} else { markCompletionFn(currentLearningContent);
// reload completion data anyway }
currentLearningContent.parentCircle?.parentLearningPath?.reloadCompletionData();
} }
this.closeLearningContent(currentLearningContent, returnRoute); this.closeLearningContent(currentLearningContent, circle, returnRoute);
} else { } else {
log.error("currentLearningContent is undefined"); log.error("currentLearningContent is undefined");
} }
}, },
continueFromSelfEvaluation(learningUnit: LearningUnit) { continueFromSelfEvaluation(learningUnit: LearningUnit, circle: CircleType) {
this.closeSelfEvaluation(learningUnit); 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 { useUserStore } from "@/stores/user";
import type { CircleLight, CourseSessionUser, ExpertSessionUser } from "@/types"; import type { CircleLight, CourseSessionUser, ExpertSessionUser } from "@/types";
import log from "loglevel"; import log from "loglevel";
@ -12,7 +12,6 @@ export type CockpitStoreState = {
courseSessionMembers: CourseSessionUser[] | undefined; courseSessionMembers: CourseSessionUser[] | undefined;
circles: CircleCockpit[] | undefined; circles: CircleCockpit[] | undefined;
currentCircle: CircleCockpit | undefined; currentCircle: CircleCockpit | undefined;
currentCourseSlug: string | undefined;
}; };
export const useCockpitStore = defineStore({ export const useCockpitStore = defineStore({
@ -22,7 +21,6 @@ export const useCockpitStore = defineStore({
courseSessionMembers: undefined, courseSessionMembers: undefined,
circles: [], circles: [],
currentCircle: undefined, currentCircle: undefined,
currentCourseSlug: undefined,
} as CockpitStoreState; } as CockpitStoreState;
}, },
actions: { actions: {
@ -31,21 +29,8 @@ export const useCockpitStore = defineStore({
currentCourseSessionUser: CourseSessionUser | undefined currentCourseSessionUser: CourseSessionUser | undefined
) { ) {
log.debug("loadCircles called", courseSlug); log.debug("loadCircles called", courseSlug);
this.currentCourseSlug = courseSlug;
const circles = await courseCircles( this.circles = await courseCircles(courseSlug, currentCourseSessionUser);
this.currentCourseSlug,
currentCourseSessionUser
);
this.circles = circles.map((c) => {
return {
id: c.id,
slug: c.slug,
title: c.title,
name: c.title,
} as const;
});
if (this.circles?.length) { if (this.circles?.length) {
await this.setCurrentCourseCircle(this.circles[0].slug); await this.setCurrentCourseCircle(this.circles[0].slug);
@ -64,28 +49,27 @@ async function courseCircles(
courseSlug: string, courseSlug: string,
currentCourseSessionUser: CourseSessionUser | undefined currentCourseSessionUser: CourseSessionUser | undefined
) { ) {
const userStore = useUserStore();
const userId = userStore.id;
if (currentCourseSessionUser && currentCourseSessionUser.role === "EXPERT") { if (currentCourseSessionUser && currentCourseSessionUser.role === "EXPERT") {
const expert = currentCourseSessionUser as ExpertSessionUser; 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 // Return all circles from learning path for admin users
if (userStore.is_superuser) { if (userStore.is_superuser) {
const learningPathStore = useLearningPathStore(); const lpQueryResult = useCourseData(courseSlug);
const learningPathCircles = learningPathStore await lpQueryResult.resultPromise;
.learningPathForUser(courseSlug, userId)
?.circles.map((c) => { return (lpQueryResult.circles.value ?? []).map((c) => {
return { return {
id: c.id, id: c.id,
title: c.title, slug: c.slug,
slug: c.slug, title: c.title,
translation_key: c.translation_key, name: c.title,
}; } as const;
}); });
return learningPathCircles || [];
} }
return []; 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 { bustItGetCache, itGetCached, itPost } from "@/fetchHelpers";
import { useCourseSessionsStore } from "@/stores/courseSessions"; import { useCourseSessionsStore } from "@/stores/courseSessions";
import { useUserStore } from "@/stores/user"; import { useUserStore } from "@/stores/user";
import type { BaseCourseWagtailPage, CourseCompletion } from "@/types"; import type {
CourseCompletion,
LearningContentWithCompletion,
LearningUnitPerformanceCriteria,
} from "@/types";
import { defineStore } from "pinia"; import { defineStore } from "pinia";
export const useCompletionStore = defineStore({ export const useCompletionStore = defineStore({
@ -29,7 +33,7 @@ export const useCompletionStore = defineStore({
return userCompletionData || []; return userCompletionData || [];
}, },
async markPage( async markPage(
page: BaseCourseWagtailPage, page: LearningContentWithCompletion | LearningUnitPerformanceCriteria,
userId: string | undefined = undefined, userId: string | undefined = undefined,
courseSessionId: string | undefined = undefined courseSessionId: string | undefined = undefined
) { ) {

View File

@ -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 {
import type { Circle } from "@/services/circle"; 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"; import type { Component } from "vue";
export type LoginMethod = "local" | "sso"; export type LoginMethod = "local" | "sso";
export type CourseCompletionStatus = "UNKNOWN" | "FAIL" | "SUCCESS"; export type CourseCompletionStatus = "UNKNOWN" | "FAIL" | "SUCCESS";
export type Completable = {
completion_status?: CourseCompletionStatus;
completion_status_updated_at?: string;
};
export interface BaseCourseWagtailPage { export interface BaseCourseWagtailPage {
readonly id: string; readonly id: string;
readonly title: string; readonly title: string;
@ -13,8 +42,6 @@ export interface BaseCourseWagtailPage {
readonly content_type: string; readonly content_type: string;
readonly translation_key: string; readonly translation_key: string;
readonly frontend_url: string; readonly frontend_url: string;
completion_status?: CourseCompletionStatus;
completion_status_updated_at?: string;
} }
export interface CircleLight { export interface CircleLight {
@ -23,168 +50,120 @@ export interface CircleLight {
readonly title: string; 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 = export type LearningContent =
| LearningContentAssignment | LearningContentAssignment
| LearningContentAttendanceCourse | LearningContentAttendanceCourse
| LearningContentDocumentList | LearningContentDocumentList
| LearningContentEdoniqTest
| LearningContentFeedback | LearningContentFeedback
| LearningContentLearningModule | LearningContentLearningModule
| LearningContentMediaLibrary | LearningContentMediaLibrary
| LearningContentPlaceholder | LearningContentPlaceholder
| LearningContentRichText | LearningContentRichText
| LearningContentEdoniqTest
| LearningContentVideo; | 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 { export type LearningContentContentType = LearningContent["content_type"];
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 interface LearningContentAssignment extends LearningContentInterface { export type LearningUnit = Omit<
readonly content_type: "learnpath.LearningContentAssignment"; LearningUnitObjectType,
readonly content_assignment_id: string; "content_type" | "learning_contents" | "performance_criteria"
readonly assignment_type: AssignmentType; > & {
readonly competence_certificate?: { content_type: "learnpath.LearningUnit";
id: string; learning_contents: LearningContentWithCompletion[];
title: string; performance_criteria: LearningUnitPerformanceCriteria[];
slug: string; circle?: CircleLight;
content_type: string; };
translation_key: string;
frontend_url: string;
} | null;
}
export interface LearningContentAttendanceCourse extends LearningContentInterface { export type LearningSequence = Omit<
readonly content_type: "learnpath.LearningContentAttendanceCourse"; LearningSequenceObjectType,
} "content_type" | "learning_units"
> & {
content_type: "learnpath.LearningSequence";
learning_units: LearningUnit[];
};
export interface LearningContentDocument { export type CircleType = Omit<
readonly type: "document"; CircleObjectType,
readonly id: string; "content_type" | "learning_sequences"
readonly value: MediaLibraryContentBlockValue; > & {
} content_type: "learnpath.Circle";
learning_sequences: LearningSequence[];
};
export interface LearningContentDocumentList extends LearningContentInterface { export type TopicType = Omit<TopicObjectType, "content_type" | "circles"> & {
readonly content_type: "learnpath.LearningContentDocumentList"; content_type: "learnpath.Topic";
readonly documents: LearningContentDocument[]; circles: CircleType[];
} };
export interface LearningContentFeedback extends LearningContentInterface { export type LearningPathType = Omit<
readonly content_type: "learnpath.LearningContentFeedback"; LearningPathObjectType,
} "content_type" | "topics"
> & {
content_type: "learnpath.LearningPath";
topics: TopicType[];
};
export interface LearningContentLearningModule extends LearningContentInterface { export type LearningUnitPerformanceCriteria = Omit<
readonly content_type: "learnpath.LearningContentLearningModule"; PerformanceCriteriaObjectType,
} "content_type"
> &
export interface LearningContentMediaLibrary extends LearningContentInterface { Completable & {
readonly content_type: "learnpath.LearningContentMediaLibrary"; readonly content_type: "competence.PerformanceCriteria";
} circle?: CircleLight;
};
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 interface CourseCompletion { export interface CourseCompletion {
readonly id: string; readonly id: string;
@ -361,57 +340,31 @@ export interface AssignmentEvaluationTask {
}; };
} }
export type AssignmentType = export type AssignmentType = AssignmentAssignmentAssignmentTypeChoices;
| "CASEWORK"
| "PREP_ASSIGNMENT"
| "REFLECTION"
| "CONDITION_ACCEPTANCE"
| "EDONIQ_TEST";
export interface Assignment extends BaseCourseWagtailPage { export type Assignment = Omit<
AssignmentObjectType,
"content_type" | "performance_objectives" | "tasks" | "evaluation_tasks"
> & {
readonly content_type: "assignment.Assignment"; 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 performance_objectives: AssignmentPerformanceObjective[];
readonly evaluation_description: string;
readonly evaluation_document_url: string;
readonly tasks: AssignmentTask[]; readonly tasks: AssignmentTask[];
readonly evaluation_tasks: AssignmentEvaluationTask[]; 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 { export type PerformanceCriteria = Omit<PerformanceCriteriaObjectType, "content_type"> &
readonly content_type: "competence.PerformanceCriteria"; Completable & {
readonly competence_id: string; readonly content_type: "competence.PerformanceCriteria";
readonly circle: CircleLight; circle: CircleLight;
readonly course_category: CourseCategory;
readonly learning_unit: BaseCourseWagtailPage & {
evaluate_url: string;
}; };
}
export interface CompetencePage extends BaseCourseWagtailPage { export type ActionCompetence = Omit<
readonly content_type: "competence.CompetencePage"; ActionCompetenceObjectType,
readonly competence_id: string; "content_type" | "performance_criteria"
readonly children: PerformanceCriteria[]; > & {
} readonly content_type: "competence.ActionCompetence";
performance_criteria: PerformanceCriteria[];
export interface CompetenceProfilePage extends BaseCourseWagtailPage { };
readonly content_type: "competence.CompetenceProfilePage";
readonly course: Course;
readonly circles: CircleLight[];
readonly children: CompetencePage[];
}
export interface CompetenceCertificateAssignment extends BaseCourseWagtailPage { export interface CompetenceCertificateAssignment extends BaseCourseWagtailPage {
assignment_type: "CASEWORK" | "EDONIQ_TEST"; 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 { export interface CourseSession {
id: string; id: string;
created_at: string; created_at: string;
@ -523,46 +435,15 @@ export interface CourseSession {
due_dates: DueDate[]; due_dates: DueDate[];
} }
export type Role = "MEMBER" | "EXPERT" | "TUTOR"; export type Role = CourseCourseSessionUserRoleChoices;
export interface CourseSessionUser { export type CourseSessionUser = CourseSessionUserObjectsType;
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 interface ExpertSessionUser extends CourseSessionUser { export interface ExpertSessionUser extends CourseSessionUser {
role: "EXPERT"; role: "EXPERT";
circles: {
id: string;
title: string;
slug: string;
translation_key: string;
}[];
} }
export interface CourseSessionDetail { export type CourseSessionDetail = CourseSessionObjectType;
id: string;
title: string;
course: {
id: string;
title: string;
slug: string;
};
assignments: CourseSessionAssignment[];
attendance_courses: CourseSessionAttendanceCourse[];
edoniq_tests: CourseSessionEdoniqTest[];
users: CourseSessionUser[];
}
// document upload // document upload
export interface DocumentUploadData { export interface DocumentUploadData {
@ -641,37 +522,11 @@ export interface AssignmentTaskCompletionData {
}; };
} }
export interface AssignmentCompletion { export type AssignmentCompletion = AssignmentCompletionObjectType & {
id: string; // assignment_user: Pick<UserObjectType, "id" | "__typename">;
created_at: string; // evaluation_user: Pick<UserObjectType, "id" | "__typename">;
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;
completion_data: AssignmentCompletionData; completion_data: AssignmentCompletionData;
task_completion_data: AssignmentTaskCompletionData; 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 { 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); 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 { function createCourseUrl(courseSlug: string | undefined, specificSub: string): string {
if (!courseSlug) { if (!courseSlug) {
return "/"; return "/";

View File

@ -43,6 +43,7 @@ describe("circle.cy.js", () => {
"Verschaffe dir einen Überblick" "Verschaffe dir einen Überblick"
); );
cy.get('[data-cy="complete-and-continue"]').click({ force: true }); 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="ls-continue-button"]').click();
cy.get('[data-cy="lc-title"]').should( cy.get('[data-cy="lc-title"]').should(
@ -50,6 +51,7 @@ describe("circle.cy.js", () => {
"Handlungsfeld «Fahrzeug»" "Handlungsfeld «Fahrzeug»"
); );
cy.get('[data-cy="complete-and-continue"]').click({ force: true }); cy.get('[data-cy="complete-and-continue"]').click({ force: true });
cy.get('[data-cy="circle-title"]').should("contain", "Fahrzeug");
cy.get( cy.get(
'[data-cy="test-lehrgang-lp-circle-fahrzeug-lc-verschaffe-dir-einen-überblick-checkbox"]' '[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="close-learning-content"]').click();
cy.get('[data-cy="circle-title"]').should("contain", "Fahrzeug"); 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(); .click();
cy.get('[data-cy="circle-title"]').should("contain", "Reisen"); 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 import List
from graphene_django import DjangoObjectType from graphene_django import DjangoObjectType
from vbv_lernwelt.assignment.graphql.types import AssignmentObjectType from vbv_lernwelt.assignment.graphql.types import AssignmentObjectType
from vbv_lernwelt.competence.models import ( from vbv_lernwelt.competence.models import (
ActionCompetence,
CompetenceCertificate, CompetenceCertificate,
CompetenceCertificateList, CompetenceCertificateList,
PerformanceCriteria,
) )
from vbv_lernwelt.course.graphql.interfaces import CoursePageInterface from vbv_lernwelt.course.graphql.interfaces import CoursePageInterface
from vbv_lernwelt.learnpath.graphql.types import LearningUnitObjectType
class CompetenceCertificateObjectType(DjangoObjectType): class CompetenceCertificateObjectType(DjangoObjectType):
assignments = List(AssignmentObjectType) assignments = List(graphene.NonNull(AssignmentObjectType), required=True)
class Meta: class Meta:
model = CompetenceCertificate model = CompetenceCertificate
@ -22,11 +26,40 @@ class CompetenceCertificateObjectType(DjangoObjectType):
class CompetenceCertificateListObjectType(DjangoObjectType): class CompetenceCertificateListObjectType(DjangoObjectType):
competence_certificates = List(CompetenceCertificateObjectType) competence_certificates = List(
graphene.NonNull(CompetenceCertificateObjectType), required=True
)
class Meta: class Meta:
model = CompetenceCertificateList model = CompetenceCertificateList
interfaces = (CoursePageInterface,) interfaces = (CoursePageInterface,)
fields = []
def resolve_competence_certificates(self, info): def resolve_competence_certificates(self, info):
return CompetenceCertificate.objects.child_of(self) return CompetenceCertificate.objects.child_of(self)
class PerformanceCriteriaObjectType(DjangoObjectType):
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, use_json_field=True,
) )
def get_frontend_url(self):
return f""
content_panels = [ content_panels = [
FieldPanel("title"), FieldPanel("title"),
FieldPanel("competence_id"), FieldPanel("competence_id"),
@ -141,6 +144,9 @@ class PerformanceCriteria(CourseBasePage):
FieldPanel("learning_unit"), FieldPanel("learning_unit"),
] ]
def get_frontend_url(self):
return ""
def save(self, clean=True, user=None, log_action=False, **kwargs): def save(self, clean=True, user=None, log_action=False, **kwargs):
profile_parent = ( profile_parent = (
self.get_ancestors().exact_type(ActionCompetenceListPage).last() self.get_ancestors().exact_type(ActionCompetenceListPage).last()

View File

@ -6,7 +6,7 @@ from graphene_django import DjangoObjectType
from vbv_lernwelt.core.models import User from vbv_lernwelt.core.models import User
class UserType(DjangoObjectType): class UserObjectType(DjangoObjectType):
class Meta: class Meta:
model = User model = User
fields = ( 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) LearningUnitFactory(title="Kompetenznachweis", title_hidden=True, parent=circle)
LearningContentEdoniqTestFactory( # LearningContentEdoniqTestFactory(
title="Wissens- und Verständnisfragen", # title="Wissens- und Verständnisfragen",
parent=circle, # parent=circle,
description=RichText( # 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>" # "<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.", # 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", # 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) LearningUnitFactory(title="Feedback", title_hidden=True, parent=circle)
LearningContentFeedbackFactory( LearningContentFeedbackFactory(
parent=circle, parent=circle,

View File

@ -5,14 +5,13 @@ from vbv_lernwelt.learnpath.models import Circle
class CoursePageInterface(graphene.Interface): class CoursePageInterface(graphene.Interface):
id = graphene.ID() id = graphene.ID(required=True)
title = graphene.String() title = graphene.String(required=True)
slug = graphene.String() slug = graphene.String(required=True)
content_type = graphene.String() content_type = graphene.String(required=True)
live = graphene.Boolean() live = graphene.Boolean(required=True)
translation_key = graphene.String() translation_key = graphene.String(required=True)
frontend_url = graphene.String() frontend_url = graphene.String(required=True)
circle = graphene.Field("vbv_lernwelt.learnpath.graphql.types.CircleObjectType")
course = graphene.Field("vbv_lernwelt.course.graphql.types.CourseObjectType") course = graphene.Field("vbv_lernwelt.course.graphql.types.CourseObjectType")
def resolve_frontend_url(self, info): def resolve_frontend_url(self, info):
@ -21,11 +20,5 @@ class CoursePageInterface(graphene.Interface):
def resolve_content_type(self, info): def resolve_content_type(self, info):
return get_django_content_type(self) 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): def resolve_course(self, info):
return self.get_course() return self.get_course()

View File

@ -1,22 +1,62 @@
import graphene 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.graphql.types import CourseObjectType, CourseSessionObjectType
from vbv_lernwelt.course.models import Course, CourseSession from vbv_lernwelt.course.models import Course, CourseSession
from vbv_lernwelt.course.permissions import has_course_access 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): 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()) course_session = graphene.Field(CourseSessionObjectType, id=graphene.ID())
def resolve_course(root, info, id): def resolve_course(self, info, id=None, slug=None):
course = Course.objects.get(pk=id) 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): if has_course_access(info.context.user, course):
return course return course
raise PermissionError("You do not have access to this 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) course_session = CourseSession.objects.get(pk=id)
if has_course_access(info.context.user, course_session.course): if has_course_access(info.context.user, course_session.course):
return course_session return course_session
raise PermissionError("You do not have access to this 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 graphql import GraphQLError
from rest_framework.exceptions import PermissionDenied from rest_framework.exceptions import PermissionDenied
from vbv_lernwelt.competence.graphql.types import ActionCompetenceObjectType
from vbv_lernwelt.course.models import ( from vbv_lernwelt.course.models import (
CircleDocument, CircleDocument,
Course, Course,
@ -84,30 +85,40 @@ def resolve_course_page(
class CourseObjectType(DjangoObjectType): 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: class Meta:
model = Course model = Course
fields = ("id", "title", "category_name", "slug", "learning_path") fields = ("id", "title", "category_name", "slug")
def resolve_learning_path(self, info): @staticmethod
return self.get_learning_path() 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): class CourseSessionUserExpertCircleType(ObjectType):
id = graphene.ID() id = graphene.ID(required=True)
title = graphene.String() title = graphene.String(required=True)
slug = graphene.String() slug = graphene.String(required=True)
class CourseSessionUserObjectsType(DjangoObjectType): class CourseSessionUserObjectsType(DjangoObjectType):
user_id = graphene.UUID() user_id = graphene.UUID(required=True)
first_name = graphene.String() first_name = graphene.String(required=True)
last_name = graphene.String() last_name = graphene.String(required=True)
email = graphene.String() email = graphene.String(required=True)
avatar_url = graphene.String() avatar_url = graphene.String(required=True)
role = graphene.String() # role = graphene.String(required=True)
circles = graphene.List(CourseSessionUserExpertCircleType) circles = graphene.List(
graphene.NonNull(CourseSessionUserExpertCircleType), required=True
)
class Meta: class Meta:
model = CourseSessionUser model = CourseSessionUser
@ -166,12 +177,16 @@ class CircleDocumentObjectType(DjangoObjectType):
class CourseSessionObjectType(DjangoObjectType): class CourseSessionObjectType(DjangoObjectType):
attendance_courses = graphene.List(CourseSessionAttendanceCourseObjectType) attendance_courses = graphene.List(
assignments = graphene.List(CourseSessionAssignmentObjectType) graphene.NonNull(CourseSessionAttendanceCourseObjectType), required=True
edoniq_tests = graphene.List(CourseSessionEdoniqTestObjectType) )
documents = graphene.List(CircleDocumentObjectType) assignments = graphene.List(
graphene.NonNull(CourseSessionAssignmentObjectType), required=True
users = graphene.List(CourseSessionUserObjectsType) )
edoniq_tests = graphene.List(
graphene.NonNull(CourseSessionEdoniqTestObjectType), required=True
)
users = graphene.List(graphene.NonNull(CourseSessionUserObjectsType), required=True)
class Meta: class Meta:
model = CourseSession model = CourseSession
@ -195,10 +210,5 @@ class CourseSessionObjectType(DjangoObjectType):
def resolve_edoniq_tests(self, info): def resolve_edoniq_tests(self, info):
return CourseSessionEdoniqTest.objects.filter(course_session=self) 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): def resolve_users(self, info):
return CourseSessionUser.objects.filter(course_session_id=self.id).distinct() 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): def get_course_url(self):
return f"/course/{self.slug}" return f"/course/{self.slug}"
def get_cockpit_url(self):
return f"/{self.get_course_url()}/cockpit"
def get_learning_path(self): def get_learning_path(self):
from vbv_lernwelt.learnpath.models import LearningPath from vbv_lernwelt.learnpath.models import LearningPath
return self.coursepage.get_children().exact_type(LearningPath).first().specific return self.coursepage.get_children().exact_type(LearningPath).first().specific
def get_learning_path_url(self): def get_action_competences(self):
return self.get_learning_path().get_frontend_url() from vbv_lernwelt.competence.models import ActionCompetence
def get_cockpit_url(self): return self.coursepage.get_descendants().exact_type(ActionCompetence).specific()
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()
def get_media_library_url(self): def get_media_library_url(self):
from vbv_lernwelt.media_library.models import MediaLibraryPage from vbv_lernwelt.media_library.models import MediaLibraryPage

View File

@ -33,13 +33,14 @@ class CourseSessionAttendanceCourseObjectType(DjangoObjectType):
AttendanceUserObjectType, source="attendance_user_list" AttendanceUserObjectType, source="attendance_user_list"
) )
due_date = graphene.Field(DueDateObjectType) due_date = graphene.Field(DueDateObjectType)
learning_content = graphene.Field(LearningContentAttendanceCourseObjectType) learning_content = graphene.Field(
LearningContentAttendanceCourseObjectType, required=True
)
class Meta: class Meta:
model = CourseSessionAttendanceCourse model = CourseSessionAttendanceCourse
fields = ( fields = (
"id", "id",
"course_session_id",
"learning_content", "learning_content",
"location", "location",
"trainer", "trainer",
@ -53,17 +54,18 @@ class CourseSessionAttendanceCourseObjectType(DjangoObjectType):
class CourseSessionAssignmentObjectType(DjangoObjectType): class CourseSessionAssignmentObjectType(DjangoObjectType):
course_session_id = graphene.ID(source="course_session_id") course_session_id = graphene.ID(source="course_session_id", required=True)
learning_content_id = graphene.ID(source="learning_content_id") learning_content_id = graphene.ID(source="learning_content_id", required=True)
submission_deadline = graphene.Field(DueDateObjectType) submission_deadline = graphene.Field(DueDateObjectType)
evaluation_deadline = graphene.Field(DueDateObjectType) evaluation_deadline = graphene.Field(DueDateObjectType)
learning_content = graphene.Field(LearningContentAssignmentObjectType) learning_content = graphene.Field(
LearningContentAssignmentObjectType, required=True
)
class Meta: class Meta:
model = CourseSessionAssignment model = CourseSessionAssignment
fields = ( fields = (
"id", "id",
"course_session_id",
"learning_content", "learning_content",
"submission_deadline", "submission_deadline",
"evaluation_deadline", "evaluation_deadline",
@ -71,16 +73,18 @@ class CourseSessionAssignmentObjectType(DjangoObjectType):
class CourseSessionEdoniqTestObjectType(DjangoObjectType): class CourseSessionEdoniqTestObjectType(DjangoObjectType):
course_session_id = graphene.ID(source="course_session_id") course_session_id = graphene.ID(source="course_session_id", required=True)
learning_content_id = graphene.ID(source="learning_content_id") learning_content_id = graphene.ID(source="learning_content_id", required=True)
deadline = graphene.Field(DueDateObjectType) deadline = graphene.Field(DueDateObjectType)
learning_content = graphene.Field(LearningContentEdoniqTestObjectType) learning_content = graphene.Field(
LearningContentEdoniqTestObjectType, required=True
)
class Meta: class Meta:
model = CourseSessionEdoniqTest model = CourseSessionEdoniqTest
fields = ( fields = (
"id", "id",
"course_session_id",
"learning_content", "learning_content",
"deadline", "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.create_default_users import create_default_users
from vbv_lernwelt.core.models import User from vbv_lernwelt.core.models import User
from vbv_lernwelt.course.creators.test_course import create_test_course from vbv_lernwelt.course.creators.test_course import create_test_course
from vbv_lernwelt.course.models import CourseSession, CourseSessionUser from vbv_lernwelt.course.models import CourseSession
from vbv_lernwelt.feedback.factories import FeedbackResponseFactory from vbv_lernwelt.feedback.factories import FeedbackResponseFactory
from vbv_lernwelt.feedback.models import FeedbackResponse from vbv_lernwelt.feedback.models import FeedbackResponse
from vbv_lernwelt.learnpath.models import Circle from vbv_lernwelt.learnpath.models import Circle

View File

@ -13,7 +13,6 @@ from vbv_lernwelt.course.models import CourseCategory, CoursePage
from vbv_lernwelt.learnpath.tests.learning_path_factories import ( from vbv_lernwelt.learnpath.tests.learning_path_factories import (
CircleFactory, CircleFactory,
LearningContentAssignmentFactory, LearningContentAssignmentFactory,
LearningContentEdoniqTestFactory,
LearningContentFeedbackFactory, LearningContentFeedbackFactory,
LearningContentLearningModuleFactory, LearningContentLearningModuleFactory,
LearningContentMediaLibraryFactory, 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.", # 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#/", # content_url="https://s3.eu-central-1.amazonaws.com/myvbv-wbt.iterativ.ch/fachcheck-fahrzeug-xapi-LFv8YiyM/index.html#/",
# ) # )
LearningContentAssignmentFactory( # LearningContentAssignmentFactory(
title="Reflexion", # title="Reflexion",
assignment_type="REFLECTION", # assignment_type="REFLECTION",
parent=circle, # content_assignment=Assignment.objects.get(
content_assignment=Assignment.objects.get( # slug__startswith=f"{circle.get_course().slug}-assignment-reflexion"
slug__startswith=f"{circle.get_course().slug}-assignment-reflexion" # ),
), # ),
),
LearningContentFeedbackFactory( LearningContentFeedbackFactory(
parent=circle, parent=circle,
) )

View File

@ -1,21 +1,8 @@
import graphene import graphene
from vbv_lernwelt.course.graphql.types import resolve_course_page from vbv_lernwelt.course.graphql.types import resolve_course_page
from vbv_lernwelt.learnpath.graphql.types import ( from vbv_lernwelt.learnpath.graphql.types import LearningPathObjectType
CircleObjectType, from vbv_lernwelt.learnpath.models import LearningPath
LearningContentAssignmentObjectType,
LearningContentAttendanceCourseObjectType,
LearningContentDocumentListObjectType,
LearningContentEdoniqTestObjectType,
LearningContentFeedbackObjectType,
LearningContentLearningModuleObjectType,
LearningContentMediaLibraryObjectType,
LearningContentPlaceholderObjectType,
LearningContentRichTextObjectType,
LearningContentVideoObjectType,
LearningPathObjectType,
)
from vbv_lernwelt.learnpath.models import Circle, LearningPath
class LearningPathQuery: class LearningPathQuery:
@ -26,10 +13,6 @@ class LearningPathQuery:
course_id=graphene.ID(), course_id=graphene.ID(),
course_slug=graphene.String(), 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( def resolve_learning_path(
root, info, id=None, slug=None, course_id=None, course_slug=None root, info, id=None, slug=None, course_id=None, course_slug=None
@ -43,53 +26,3 @@ class LearningPathQuery:
course_id=course_id, course_id=course_id,
course_slug=course_slug, 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__) 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): class LearningContentInterface(CoursePageInterface):
minutes = graphene.Int() minutes = graphene.Int()
description = graphene.String() description = graphene.String(required=True)
content = graphene.String() content_url = graphene.String(required=True)
can_user_self_toggle_course_completion = graphene.Boolean(required=True)
circle = graphene.Field(CircleLightObjectType)
@staticmethod
def resolve_circle(root, info):
return root.get_parent_circle()
@classmethod @classmethod
def resolve_type(cls, instance, info): def resolve_type(cls, instance, info):
@ -63,96 +75,156 @@ class LearningContentInterface(CoursePageInterface):
class LearningContentAttendanceCourseObjectType(DjangoObjectType): class LearningContentAttendanceCourseObjectType(DjangoObjectType):
class Meta: class Meta:
model = LearningContentAttendanceCourse model = LearningContentAttendanceCourse
interfaces = (LearningContentInterface,) interfaces = (
CoursePageInterface,
LearningContentInterface,
)
fields = [] fields = []
class LearningContentVideoObjectType(DjangoObjectType): class LearningContentVideoObjectType(DjangoObjectType):
class Meta: class Meta:
model = LearningContentVideo model = LearningContentVideo
interfaces = (LearningContentInterface,) interfaces = (
CoursePageInterface,
LearningContentInterface,
)
fields = [] fields = []
class LearningContentPlaceholderObjectType(DjangoObjectType): class LearningContentPlaceholderObjectType(DjangoObjectType):
class Meta: class Meta:
model = LearningContentPlaceholder model = LearningContentPlaceholder
interfaces = (LearningContentInterface,) interfaces = (
CoursePageInterface,
LearningContentInterface,
)
fields = [] fields = []
class LearningContentFeedbackObjectType(DjangoObjectType): class LearningContentFeedbackObjectType(DjangoObjectType):
class Meta: class Meta:
model = LearningContentFeedback model = LearningContentFeedback
interfaces = (LearningContentInterface,) interfaces = (
CoursePageInterface,
LearningContentInterface,
)
fields = [] fields = []
class LearningContentLearningModuleObjectType(DjangoObjectType): class LearningContentLearningModuleObjectType(DjangoObjectType):
class Meta: class Meta:
model = LearningContentLearningModule model = LearningContentLearningModule
interfaces = (LearningContentInterface,) interfaces = (
CoursePageInterface,
LearningContentInterface,
)
fields = [] fields = []
class LearningContentMediaLibraryObjectType(DjangoObjectType): class LearningContentMediaLibraryObjectType(DjangoObjectType):
class Meta: class Meta:
model = LearningContentMediaLibrary model = LearningContentMediaLibrary
interfaces = (LearningContentInterface,) interfaces = (
CoursePageInterface,
LearningContentInterface,
)
fields = [] fields = []
class LearningContentEdoniqTestObjectType(DjangoObjectType): class LearningContentEdoniqTestObjectType(DjangoObjectType):
competence_certificate = graphene.Field(
"vbv_lernwelt.competence.graphql.types.CompetenceCertificateObjectType",
)
has_extended_time_test = graphene.Boolean(required=True)
class Meta: class Meta:
model = LearningContentEdoniqTest model = LearningContentEdoniqTest
interfaces = (LearningContentInterface,) interfaces = (
fields = [ CoursePageInterface,
"content_assignment", 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 LearningContentRichTextObjectType(DjangoObjectType):
class Meta: class Meta:
model = LearningContentRichText model = LearningContentRichText
interfaces = (LearningContentInterface,) interfaces = (
fields = [] CoursePageInterface,
LearningContentInterface,
)
fields = ["text"]
class LearningContentAssignmentObjectType(DjangoObjectType): class LearningContentAssignmentObjectType(DjangoObjectType):
competence_certificate = graphene.Field(
"vbv_lernwelt.competence.graphql.types.CompetenceCertificateObjectType",
)
class Meta: class Meta:
model = LearningContentAssignment model = LearningContentAssignment
interfaces = (LearningContentInterface,) interfaces = (
CoursePageInterface,
LearningContentInterface,
)
fields = [ fields = [
"content_assignment", "content_assignment",
"assignment_type", "assignment_type",
] ]
def resolve_competence_certificate(self: LearningContentAssignment, info):
return self.content_assignment.competence_certificate
class LearningContentDocumentListObjectType(DjangoObjectType): class LearningContentDocumentListObjectType(DjangoObjectType):
class Meta: class Meta:
model = LearningContentDocumentList model = LearningContentDocumentList
interfaces = (LearningContentInterface,) interfaces = (
CoursePageInterface,
LearningContentInterface,
)
fields = [] fields = []
class LearningUnitObjectType(DjangoObjectType): 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: class Meta:
model = LearningUnit model = LearningUnit
interfaces = (CoursePageInterface,) interfaces = (CoursePageInterface,)
fields = [] fields = ["evaluate_url", "title_hidden"]
@staticmethod def resolve_evaluate_url(self: LearningUnit, info, **kwargs):
def resolve_learning_contents(root: 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 siblings = None
if hasattr(info.context, "circle_descendants"): if hasattr(info.context, "circle_descendants"):
circle_descendants = 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 :] siblings = circle_descendants[index + 1 :]
if not siblings: if not siblings:
siblings = root.get_siblings().live().specific() siblings = self.get_siblings().live().specific()
learning_contents = [] learning_contents = []
for sibling in siblings: for sibling in siblings:
@ -167,7 +239,9 @@ class LearningUnitObjectType(DjangoObjectType):
class LearningSequenceObjectType(DjangoObjectType): class LearningSequenceObjectType(DjangoObjectType):
learning_units = graphene.List(LearningUnitObjectType) learning_units = graphene.List(
graphene.NonNull(LearningUnitObjectType), required=True
)
class Meta: class Meta:
model = LearningSequence model = LearningSequence
@ -196,7 +270,9 @@ class LearningSequenceObjectType(DjangoObjectType):
class CircleObjectType(DjangoObjectType): class CircleObjectType(DjangoObjectType):
learning_sequences = graphene.List(LearningSequenceObjectType) learning_sequences = graphene.List(
graphene.NonNull(LearningSequenceObjectType), required=True
)
class Meta: class Meta:
model = Circle model = Circle
@ -206,21 +282,23 @@ class CircleObjectType(DjangoObjectType):
"goals", "goals",
] ]
@staticmethod def resolve_learning_sequences(self: Circle, info, **kwargs):
def resolve_learning_sequences(root: Circle, info, **kwargs):
circle_descendants = None circle_descendants = None
if hasattr(info.context, "learning_path_descendants"): if hasattr(info.context, "learning_path_descendants"):
children = 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( next_circle_index = find_first_index(
children[circle_start_index + 1 :], next_children,
pred=lambda child: child.specific_class == Circle, 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: 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) # store flattened descendents to improve performance (no need for db queries)
info.context.circle_descendants = list(circle_descendants) info.context.circle_descendants = list(circle_descendants)
@ -233,7 +311,7 @@ class CircleObjectType(DjangoObjectType):
class TopicObjectType(DjangoObjectType): class TopicObjectType(DjangoObjectType):
circles = graphene.List(CircleObjectType) circles = graphene.List(graphene.NonNull(CircleObjectType), required=True)
class Meta: class Meta:
model = Topic model = Topic
@ -265,11 +343,12 @@ class TopicObjectType(DjangoObjectType):
class LearningPathObjectType(DjangoObjectType): class LearningPathObjectType(DjangoObjectType):
topics = graphene.List(TopicObjectType) topics = graphene.List(graphene.NonNull(TopicObjectType), required=True)
class Meta: class Meta:
model = LearningPath model = LearningPath
interfaces = (CoursePageInterface,) interfaces = (CoursePageInterface,)
fields = []
@staticmethod @staticmethod
def resolve_topics(root: LearningPath, info, **kwargs): 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): def get_frontend_url(self):
return f"/course/{self.slug.replace('-lp', '')}/learn" return f"/course/{self.slug.replace('-lp', '')}/learn"
def get_cockpit_url(self):
return f"/course/{self.slug.replace('-lp', '')}/cockpit"
class Topic(CourseBasePage): class Topic(CourseBasePage):
serialize_field_names = ["is_visible"] serialize_field_names = ["is_visible"]
@ -58,6 +55,9 @@ class Topic(CourseBasePage):
def get_admin_display_title(self): def get_admin_display_title(self):
return f"Thema: {self.draft_title}" return f"Thema: {self.draft_title}"
def get_frontend_url(self):
return ""
class Meta: class Meta:
verbose_name = "Topic" verbose_name = "Topic"
@ -372,10 +372,7 @@ class LearningContentEdoniqTest(LearningContent):
content_assignment = models.ForeignKey( content_assignment = models.ForeignKey(
"assignment.Assignment", "assignment.Assignment",
# `null=True` is only set because of existing data... on_delete=models.PROTECT,
null=True,
blank=True,
on_delete=models.SET_NULL,
) )
@property @property