Merged in feature/VBV-465-learningpath-graphql-client-composable (pull request #220)
Feature/VBV-465 learningpath graphql client composable
This commit is contained in:
commit
7cbe15ce50
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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: [],
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -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));
|
||||||
|
|
|
||||||
|
|
@ -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",
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
|
|
||||||
|
|
@ -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>
|
|
||||||
|
|
@ -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(() => {
|
||||||
|
|
|
||||||
|
|
@ -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>
|
|
||||||
|
|
@ -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,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -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)]";
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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
|
|
@ -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";
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`);
|
||||||
|
|
|
||||||
|
|
@ -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];
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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"
|
||||||
>
|
>
|
||||||
|
|
|
||||||
|
|
@ -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(
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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"
|
||||||
>
|
>
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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">
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
@ -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(
|
||||||
|
|
|
||||||
|
|
@ -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"
|
||||||
|
|
|
||||||
|
|
@ -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"
|
||||||
>
|
>
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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`"
|
||||||
>
|
>
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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>(), {
|
||||||
|
|
|
||||||
|
|
@ -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"
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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>();
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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"
|
||||||
>
|
>
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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 [];
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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}`
|
||||||
|
|
|
||||||
|
|
@ -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" });
|
||||||
|
|
|
||||||
|
|
@ -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 {
|
||||||
|
|
|
||||||
|
|
@ -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"
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
@ -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);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
@ -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"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
@ -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,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -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);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -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 [];
|
||||||
|
|
|
||||||
|
|
@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
@ -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
|
||||||
) {
|
) {
|
||||||
|
|
|
||||||
|
|
@ -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,
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
@ -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 {
|
||||||
|
|
|
||||||
|
|
@ -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 "/";
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -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");
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -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()
|
||||||
|
|
|
||||||
|
|
@ -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()
|
||||||
|
|
|
||||||
|
|
@ -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 = (
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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()
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
)
|
||||||
|
|
|
||||||
|
|
@ -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()
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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",
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -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
|
|
||||||
|
|
|
||||||
|
|
@ -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):
|
||||||
|
|
|
||||||
|
|
@ -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),
|
||||||
|
]
|
||||||
|
|
@ -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"
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue