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/templates/vue/index.html
|
||||
/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
|
||||
- pwd
|
||||
- npm install
|
||||
- npm run codegen
|
||||
- npm test
|
||||
|
||||
js-linting: &js-linting
|
||||
|
|
|
|||
|
|
@ -11,7 +11,13 @@ const config: CodegenConfig = {
|
|||
"./src/gql/": {
|
||||
preset: "client",
|
||||
config: {
|
||||
// avoidOptionals: true,
|
||||
useTypeImports: true,
|
||||
scalars: {
|
||||
ID: "string",
|
||||
UUID: "string",
|
||||
DateTime: "string",
|
||||
},
|
||||
},
|
||||
plugins: [],
|
||||
},
|
||||
|
|
|
|||
|
|
@ -8,4 +8,4 @@ const schema = readFileSync("./src/gql/schema.graphql", "utf-8");
|
|||
const minifiedSchema = minifyIntrospectionQuery(getIntrospectedSchema(schema));
|
||||
|
||||
// Write the minified schema to a new file
|
||||
writeFileSync("./src/gql/minifiedSchema.json", JSON.stringify(minifiedSchema));
|
||||
writeFileSync("./src/gql/dist/minifiedSchema.json", JSON.stringify(minifiedSchema));
|
||||
|
|
|
|||
|
|
@ -16,7 +16,8 @@
|
|||
"storybook": "storybook dev -p 6006",
|
||||
"tailwind": "tailwindcss -i tailwind.css -o ../server/vbv_lernwelt/static/css/tailwind.css --watch",
|
||||
"test": "vitest run",
|
||||
"typecheck": "npm run codegen && vue-tsc --noEmit -p tsconfig.app.json --composite false"
|
||||
"typecheck": "npm run codegen && vue-tsc --noEmit -p tsconfig.app.json --composite false",
|
||||
"typecheck-only": "vue-tsc --noEmit -p tsconfig.app.json --composite false"
|
||||
},
|
||||
"dependencies": {
|
||||
"@headlessui/tailwindcss": "^0.1.3",
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ import dayjs from "dayjs";
|
|||
import LocalizedFormat from "dayjs/plugin/localizedFormat";
|
||||
import i18next from "i18next";
|
||||
|
||||
export const formatDueDate = (start: string, end?: string) => {
|
||||
export const formatDueDate = (start: string, end?: string | null) => {
|
||||
dayjs.extend(LocalizedFormat);
|
||||
const startDayjs = dayjs(start);
|
||||
const startDateString = getDateString(startDayjs);
|
||||
|
|
|
|||
|
|
@ -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">
|
||||
import LearningPathCircle from "@/pages/learningPath/learningPathPage/LearningPathCircle.vue";
|
||||
import { calculateCircleSectorData } from "@/pages/learningPath/learningPathPage/utils";
|
||||
import type { LearningPath } from "@/services/learningPath";
|
||||
import { computed } from "vue";
|
||||
import { useCourseDataWithCompletion } from "@/composables";
|
||||
|
||||
export type DiagramType = "horizontal" | "horizontalSmall" | "singleSmall";
|
||||
|
||||
export interface Props {
|
||||
diagramType?: DiagramType;
|
||||
learningPath: LearningPath;
|
||||
// set to undefined (default) to show all circles
|
||||
showCircleSlugs?: string[];
|
||||
courseSlug: string;
|
||||
courseSessionId: string;
|
||||
userId?: string;
|
||||
diagramType?: DiagramType;
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
diagramType: "horizontal",
|
||||
showCircleSlugs: undefined,
|
||||
userId: undefined,
|
||||
});
|
||||
|
||||
const lpQueryResult = useCourseDataWithCompletion(
|
||||
props.courseSlug,
|
||||
props.userId,
|
||||
props.courseSessionId
|
||||
);
|
||||
|
||||
const circles = computed(() => {
|
||||
if (props.showCircleSlugs?.length) {
|
||||
return props.learningPath.circles.filter(
|
||||
return (lpQueryResult.circles.value ?? []).filter(
|
||||
(c) => props.showCircleSlugs?.includes(c.slug) ?? true
|
||||
);
|
||||
}
|
||||
return props.learningPath.circles;
|
||||
return lpQueryResult.circles.value ?? [];
|
||||
});
|
||||
|
||||
const wrapperClasses = computed(() => {
|
||||
|
|
|
|||
|
|
@ -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 type { CourseSession, CourseSessionDetail } from "@/types";
|
||||
import { useQuery } from "@urql/vue";
|
||||
|
||||
import { COURSE_SESSION_DETAIL_QUERY } from "@/graphql/queries";
|
||||
import { useUserStore } from "@/stores/user";
|
||||
import type {
|
||||
ActionCompetence,
|
||||
Course,
|
||||
CourseCompletion,
|
||||
CourseCompletionStatus,
|
||||
CourseSession,
|
||||
CourseSessionDetail,
|
||||
LearningContentWithCompletion,
|
||||
LearningPathType,
|
||||
LearningUnitPerformanceCriteria,
|
||||
PerformanceCriteria,
|
||||
} from "@/types";
|
||||
import { useQuery } from "@urql/vue";
|
||||
import orderBy from "lodash/orderBy";
|
||||
import log from "loglevel";
|
||||
import type { ComputedRef } from "vue";
|
||||
import { computed, ref, watchEffect } from "vue";
|
||||
|
|
@ -56,19 +74,19 @@ export function useCourseSessionDetailQuery(courSessionId?: string) {
|
|||
|
||||
function findAssignment(learningContentId: string) {
|
||||
return (courseSessionDetail.value?.assignments ?? []).find((a) => {
|
||||
return a.learning_content.id === learningContentId;
|
||||
return a.learning_content?.id === learningContentId;
|
||||
});
|
||||
}
|
||||
|
||||
function findEdoniqTest(learningContentId: string) {
|
||||
return (courseSessionDetail.value?.edoniq_tests ?? []).find((e) => {
|
||||
return e.learning_content.id === learningContentId;
|
||||
return e.learning_content?.id === learningContentId;
|
||||
});
|
||||
}
|
||||
|
||||
function findAttendanceCourse(learningContentId: string) {
|
||||
return (courseSessionDetail.value?.attendance_courses ?? []).find((e) => {
|
||||
return e.learning_content.id === learningContentId;
|
||||
return e.learning_content?.id === learningContentId;
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -123,3 +141,273 @@ export function useCourseSessionDetailQuery(courSessionId?: string) {
|
|||
filterCircleExperts,
|
||||
};
|
||||
}
|
||||
|
||||
export function flatCircles(learningPath: LearningPathType) {
|
||||
return learningPath.topics.flatMap((t) => t.circles);
|
||||
}
|
||||
|
||||
export function useCourseData(courseSlug: string) {
|
||||
const learningPath = ref<LearningPathType | undefined>(undefined);
|
||||
const actionCompetences = ref<ActionCompetence[]>([]);
|
||||
const course = ref<Course | undefined>(undefined);
|
||||
|
||||
// urql.useQuery is not meant to be used programmatically, so we use graphqlClient.query instead
|
||||
const resultPromise = graphqlClient
|
||||
.query(COURSE_QUERY, { slug: `${courseSlug}` })
|
||||
.toPromise();
|
||||
|
||||
resultPromise.then((result) => {
|
||||
if (result.error) {
|
||||
log.error(result.error);
|
||||
}
|
||||
|
||||
course.value = result.data?.course as Course;
|
||||
actionCompetences.value = result.data?.course
|
||||
?.action_competences as ActionCompetence[];
|
||||
learningPath.value = result.data?.course?.learning_path as LearningPathType;
|
||||
|
||||
// attach circle information to learning contents
|
||||
if (learningPath.value) {
|
||||
flatCircles(learningPath.value).forEach((circle) => {
|
||||
circle.learning_sequences.forEach((ls, lsIndex) => {
|
||||
const circleData = {
|
||||
id: circle.id,
|
||||
slug: circle.slug,
|
||||
title: circle.title,
|
||||
};
|
||||
return ls.learning_units.forEach((lu, luIndex) => {
|
||||
lu.circle = Object.assign({}, circleData);
|
||||
lu.learning_contents.forEach((lc, lcIndex) => {
|
||||
lc.circle = Object.assign({}, circleData);
|
||||
lc.continueUrl = ls.frontend_url || circle.frontend_url;
|
||||
lc.firstInCircle = lcIndex === 0 && luIndex === 0 && lsIndex === 0;
|
||||
lc.parentLearningUnit = {
|
||||
id: lu.id,
|
||||
slug: lu.slug,
|
||||
title: lu.title,
|
||||
};
|
||||
});
|
||||
|
||||
lu.performance_criteria.forEach((luPc) => {
|
||||
luPc.circle = Object.assign({}, circleData);
|
||||
const pc = findPerformanceCriterion(luPc.id);
|
||||
if (pc) {
|
||||
pc.circle = Object.assign({}, circleData);
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
const circles = computed(() => {
|
||||
if (learningPath.value) {
|
||||
return flatCircles(learningPath.value);
|
||||
}
|
||||
return undefined;
|
||||
});
|
||||
|
||||
function findCircle(idOrSlug: string) {
|
||||
return (circles.value ?? []).find((c) => {
|
||||
return c.id === idOrSlug || c.slug.endsWith(idOrSlug);
|
||||
});
|
||||
}
|
||||
|
||||
function findPerformanceCriterion(id: string) {
|
||||
return (actionCompetences.value ?? [])
|
||||
.flatMap((ac) => {
|
||||
return ac.performance_criteria;
|
||||
})
|
||||
.find((pc) => {
|
||||
return pc.id === id;
|
||||
}) as PerformanceCriteria | undefined;
|
||||
}
|
||||
|
||||
function findLearningContent(
|
||||
learningContentIdOrSlug: string,
|
||||
circleIdOrSlug?: string
|
||||
) {
|
||||
let filteredCircles = circles.value ?? [];
|
||||
if (circleIdOrSlug) {
|
||||
filteredCircles = filteredCircles.filter((c) => {
|
||||
return c.id === circleIdOrSlug || c.slug.endsWith(circleIdOrSlug);
|
||||
});
|
||||
}
|
||||
|
||||
return filteredCircles
|
||||
.flatMap((c) => {
|
||||
return circleFlatLearningContents(c);
|
||||
})
|
||||
.find((lc) => {
|
||||
return (
|
||||
lc.id === learningContentIdOrSlug || lc.slug.endsWith(learningContentIdOrSlug)
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
function findLearningUnit(learningUnitIdOrSlug: string, circleIdOrSlug?: string) {
|
||||
let filteredCircles = circles.value ?? [];
|
||||
if (circleIdOrSlug) {
|
||||
filteredCircles = filteredCircles.filter((c) => {
|
||||
return c.id === circleIdOrSlug || c.slug.endsWith(circleIdOrSlug);
|
||||
});
|
||||
}
|
||||
|
||||
return filteredCircles
|
||||
.flatMap((c) => {
|
||||
return circleFlatLearningUnits(c);
|
||||
})
|
||||
.find((lu) => {
|
||||
return lu.id === learningUnitIdOrSlug || lu.slug.endsWith(learningUnitIdOrSlug);
|
||||
});
|
||||
}
|
||||
|
||||
const flatPerformanceCriteria = computed(() => {
|
||||
return (actionCompetences.value ?? []).flatMap((ac) => {
|
||||
return ac.performance_criteria;
|
||||
}) as PerformanceCriteria[];
|
||||
});
|
||||
|
||||
return {
|
||||
resultPromise,
|
||||
course,
|
||||
learningPath,
|
||||
actionCompetences,
|
||||
circles,
|
||||
findCircle,
|
||||
findLearningContent,
|
||||
findLearningUnit,
|
||||
flatPerformanceCriteria,
|
||||
};
|
||||
}
|
||||
|
||||
export function useCourseDataWithCompletion(
|
||||
courseSlug?: string,
|
||||
userId?: string,
|
||||
courseSessionId?: string
|
||||
) {
|
||||
if (!courseSlug) {
|
||||
courseSlug = useCurrentCourseSession().value.course.slug;
|
||||
}
|
||||
if (!userId) {
|
||||
userId = useUserStore().id;
|
||||
}
|
||||
if (!courseSessionId) {
|
||||
courseSessionId = useCurrentCourseSession().value.id;
|
||||
}
|
||||
|
||||
const courseResult = useCourseData(courseSlug);
|
||||
const completionStore = useCompletionStore();
|
||||
const nextLearningContent = ref<LearningContentWithCompletion | undefined>(undefined);
|
||||
const loaded = ref(false);
|
||||
|
||||
function updateCompletionData() {
|
||||
if (userId && courseSessionId) {
|
||||
return completionStore.loadCourseSessionCompletionData(courseSessionId, userId);
|
||||
}
|
||||
return Promise.resolve([]);
|
||||
}
|
||||
|
||||
function _parseCompletionData(completionData: CourseCompletion[]) {
|
||||
if (courseResult.circles.value) {
|
||||
courseResult.circles.value.forEach((circle) => {
|
||||
circleFlatChildren(circle).forEach((lc) => {
|
||||
const pageIndex = completionData.findIndex((e) => {
|
||||
return e.page_id === lc.id;
|
||||
});
|
||||
if (pageIndex >= 0) {
|
||||
lc.completion_status = completionData[pageIndex].completion_status;
|
||||
} else {
|
||||
lc.completion_status = "UNKNOWN";
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
if (courseResult.actionCompetences.value) {
|
||||
courseResult.actionCompetences.value.forEach((ac) => {
|
||||
ac.performance_criteria.forEach((pc) => {
|
||||
const pageIndex = completionData.findIndex((e) => {
|
||||
return e.page_id === pc.id;
|
||||
});
|
||||
if (pageIndex >= 0) {
|
||||
pc.completion_status = completionData[pageIndex].completion_status;
|
||||
} else {
|
||||
pc.completion_status = "UNKNOWN";
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
calcNextLearningContent(completionData);
|
||||
}
|
||||
|
||||
function calcNextLearningContent(completionData: CourseCompletion[]) {
|
||||
const flatLearningContents = (courseResult.circles.value ?? []).flatMap((c) => {
|
||||
return circleFlatLearningContents(c);
|
||||
});
|
||||
const lastCompleted = findLastCompletedLearningContent(completionData);
|
||||
if (lastCompleted) {
|
||||
const lastCompletedIndex = flatLearningContents.findIndex((lc) => {
|
||||
return lc.id === lastCompleted.id;
|
||||
});
|
||||
if (flatLearningContents[lastCompletedIndex + 1]) {
|
||||
nextLearningContent.value = flatLearningContents[lastCompletedIndex + 1];
|
||||
} else {
|
||||
nextLearningContent.value = undefined;
|
||||
}
|
||||
} else {
|
||||
nextLearningContent.value = flatLearningContents[0];
|
||||
}
|
||||
}
|
||||
|
||||
function findLastCompletedLearningContent(completionData: CourseCompletion[]) {
|
||||
const latestCompletion = orderBy(completionData ?? [], ["updated_at"], "desc").find(
|
||||
(c: CourseCompletion) => {
|
||||
return (
|
||||
c.completion_status === "SUCCESS" &&
|
||||
c.page_type.startsWith("learnpath.LearningContent")
|
||||
);
|
||||
}
|
||||
);
|
||||
if (latestCompletion) {
|
||||
return courseResult.findLearningContent(latestCompletion.page_id);
|
||||
}
|
||||
}
|
||||
|
||||
async function markCompletion(
|
||||
page: LearningContentWithCompletion | LearningUnitPerformanceCriteria,
|
||||
completion_status: CourseCompletionStatus = "SUCCESS"
|
||||
) {
|
||||
if (userId && courseSessionId) {
|
||||
page.completion_status = completion_status;
|
||||
const completionData = await completionStore.markPage(
|
||||
page,
|
||||
userId,
|
||||
courseSessionId
|
||||
);
|
||||
_parseCompletionData(completionData);
|
||||
}
|
||||
}
|
||||
|
||||
async function _start() {
|
||||
return Promise.all([courseResult.resultPromise, updateCompletionData()]).then(
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
([_queryResults, completionData]) => {
|
||||
_parseCompletionData(completionData);
|
||||
loaded.value = true;
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
const resultPromise = _start();
|
||||
|
||||
return {
|
||||
...courseResult,
|
||||
loaded,
|
||||
resultPromise,
|
||||
markCompletion,
|
||||
nextLearningContent,
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,9 +1,3 @@
|
|||
import type { CourseCompletionStatus } from "@/types";
|
||||
|
||||
export const COMPLETION_SUCCESS: CourseCompletionStatus = "SUCCESS";
|
||||
export const COMPLETION_FAILURE: CourseCompletionStatus = "FAIL";
|
||||
export const COMPLETION_UNKNOWN: CourseCompletionStatus = "UNKNOWN";
|
||||
|
||||
export const itCheckboxDefaultIconCheckedTailwindClass =
|
||||
"bg-[url(/static/icons/icon-checkbox-checked.svg)] hover:bg-[url(/static/icons/icon-checkbox-checked-hover.svg)]";
|
||||
|
||||
|
|
|
|||
|
|
@ -18,9 +18,9 @@ const documents = {
|
|||
"\n fragment CoursePageFields on CoursePageInterface {\n title\n id\n slug\n content_type\n frontend_url\n }\n": types.CoursePageFieldsFragmentDoc,
|
||||
"\n query attendanceCheckQuery($courseSessionId: ID!) {\n course_session_attendance_course(id: $courseSessionId) {\n id\n attendance_user_list {\n user_id\n status\n }\n }\n }\n": types.AttendanceCheckQueryDocument,
|
||||
"\n query assignmentCompletionQuery(\n $assignmentId: ID!\n $courseSessionId: ID!\n $learningContentId: ID\n $assignmentUserId: UUID\n ) {\n assignment(id: $assignmentId) {\n assignment_type\n needs_expert_evaluation\n max_points\n content_type\n effort_required\n evaluation_description\n evaluation_document_url\n evaluation_tasks\n id\n intro_text\n performance_objectives\n slug\n tasks\n title\n translation_key\n competence_certificate {\n ...CoursePageFields\n }\n }\n assignment_completion(\n assignment_id: $assignmentId\n course_session_id: $courseSessionId\n assignment_user_id: $assignmentUserId\n learning_content_page_id: $learningContentId\n ) {\n id\n completion_status\n submitted_at\n evaluation_submitted_at\n evaluation_user {\n id\n }\n assignment_user {\n id\n }\n evaluation_points\n evaluation_max_points\n evaluation_passed\n edoniq_extended_time_flag\n completion_data\n task_completion_data\n }\n }\n": types.AssignmentCompletionQueryDocument,
|
||||
"\n query courseQuery($courseId: ID!) {\n course(id: $courseId) {\n id\n slug\n title\n category_name\n learning_path {\n id\n }\n }\n }\n": types.CourseQueryDocument,
|
||||
"\n query competenceCertificateQuery($courseSlug: String!, $courseSessionId: ID!) {\n competence_certificate_list(course_slug: $courseSlug) {\n ...CoursePageFields\n competence_certificates {\n ...CoursePageFields\n assignments {\n ...CoursePageFields\n assignment_type\n max_points\n completion(course_session_id: $courseSessionId) {\n id\n completion_status\n submitted_at\n evaluation_points\n evaluation_max_points\n evaluation_passed\n }\n learning_content {\n title\n id\n slug\n content_type\n frontend_url\n circle {\n ...CoursePageFields\n }\n }\n }\n }\n }\n }\n": types.CompetenceCertificateQueryDocument,
|
||||
"\n query competenceCertificateQuery($courseSlug: String!, $courseSessionId: ID!) {\n competence_certificate_list(course_slug: $courseSlug) {\n ...CoursePageFields\n competence_certificates {\n ...CoursePageFields\n assignments {\n ...CoursePageFields\n assignment_type\n max_points\n completion(course_session_id: $courseSessionId) {\n id\n completion_status\n submitted_at\n evaluation_points\n evaluation_max_points\n evaluation_passed\n }\n learning_content {\n ...CoursePageFields\n circle {\n id\n title\n slug\n }\n }\n }\n }\n }\n }\n": types.CompetenceCertificateQueryDocument,
|
||||
"\n query courseSessionDetail($courseSessionId: ID!) {\n course_session(id: $courseSessionId) {\n id\n title\n course {\n id\n title\n slug\n }\n users {\n id\n user_id\n first_name\n last_name\n email\n avatar_url\n role\n circles {\n id\n title\n slug\n }\n }\n attendance_courses {\n id\n location\n trainer\n due_date {\n id\n start\n end\n }\n learning_content_id\n learning_content {\n id\n title\n circle {\n id\n title\n slug\n }\n }\n }\n assignments {\n id\n submission_deadline {\n id\n start\n }\n evaluation_deadline {\n id\n start\n }\n learning_content {\n id\n title\n content_assignment {\n id\n title\n assignment_type\n }\n }\n }\n edoniq_tests {\n id\n deadline {\n id\n start\n end\n }\n learning_content {\n id\n title\n content_assignment {\n id\n title\n assignment_type\n }\n }\n }\n }\n }\n": types.CourseSessionDetailDocument,
|
||||
"\n query courseQuery($slug: String!) {\n course(slug: $slug) {\n id\n title\n slug\n category_name\n action_competences {\n competence_id\n ...CoursePageFields\n performance_criteria {\n competence_id\n learning_unit {\n id\n slug\n evaluate_url\n }\n ...CoursePageFields\n }\n }\n learning_path {\n ...CoursePageFields\n topics {\n is_visible\n ...CoursePageFields\n circles {\n description\n goals\n ...CoursePageFields\n learning_sequences {\n icon\n ...CoursePageFields\n learning_units {\n evaluate_url\n ...CoursePageFields\n performance_criteria {\n ...CoursePageFields\n }\n learning_contents {\n can_user_self_toggle_course_completion\n content_url\n minutes\n description\n ...CoursePageFields\n ... on LearningContentAssignmentObjectType {\n assignment_type\n content_assignment {\n id\n }\n competence_certificate {\n ...CoursePageFields\n }\n }\n ... on LearningContentEdoniqTestObjectType {\n checkbox_text\n has_extended_time_test\n content_assignment {\n id\n }\n competence_certificate {\n ...CoursePageFields\n }\n }\n ... on LearningContentRichTextObjectType {\n text\n }\n }\n }\n }\n }\n }\n }\n }\n }\n": types.CourseQueryDocument,
|
||||
"\n mutation SendFeedbackMutation(\n $courseSessionId: ID!\n $learningContentId: ID!\n $data: GenericScalar!\n $submitted: Boolean\n ) {\n send_feedback(\n course_session_id: $courseSessionId\n learning_content_page_id: $learningContentId\n data: $data\n submitted: $submitted\n ) {\n feedback_response {\n id\n data\n submitted\n }\n errors {\n field\n messages\n }\n }\n }\n": types.SendFeedbackMutationDocument,
|
||||
};
|
||||
|
||||
|
|
@ -61,15 +61,15 @@ export function graphql(source: "\n query assignmentCompletionQuery(\n $assi
|
|||
/**
|
||||
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
|
||||
*/
|
||||
export function graphql(source: "\n query courseQuery($courseId: ID!) {\n course(id: $courseId) {\n id\n slug\n title\n category_name\n learning_path {\n id\n }\n }\n }\n"): (typeof documents)["\n query courseQuery($courseId: ID!) {\n course(id: $courseId) {\n id\n slug\n title\n category_name\n learning_path {\n id\n }\n }\n }\n"];
|
||||
/**
|
||||
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
|
||||
*/
|
||||
export function graphql(source: "\n query competenceCertificateQuery($courseSlug: String!, $courseSessionId: ID!) {\n competence_certificate_list(course_slug: $courseSlug) {\n ...CoursePageFields\n competence_certificates {\n ...CoursePageFields\n assignments {\n ...CoursePageFields\n assignment_type\n max_points\n completion(course_session_id: $courseSessionId) {\n id\n completion_status\n submitted_at\n evaluation_points\n evaluation_max_points\n evaluation_passed\n }\n learning_content {\n title\n id\n slug\n content_type\n frontend_url\n circle {\n ...CoursePageFields\n }\n }\n }\n }\n }\n }\n"): (typeof documents)["\n query competenceCertificateQuery($courseSlug: String!, $courseSessionId: ID!) {\n competence_certificate_list(course_slug: $courseSlug) {\n ...CoursePageFields\n competence_certificates {\n ...CoursePageFields\n assignments {\n ...CoursePageFields\n assignment_type\n max_points\n completion(course_session_id: $courseSessionId) {\n id\n completion_status\n submitted_at\n evaluation_points\n evaluation_max_points\n evaluation_passed\n }\n learning_content {\n title\n id\n slug\n content_type\n frontend_url\n circle {\n ...CoursePageFields\n }\n }\n }\n }\n }\n }\n"];
|
||||
export function graphql(source: "\n query competenceCertificateQuery($courseSlug: String!, $courseSessionId: ID!) {\n competence_certificate_list(course_slug: $courseSlug) {\n ...CoursePageFields\n competence_certificates {\n ...CoursePageFields\n assignments {\n ...CoursePageFields\n assignment_type\n max_points\n completion(course_session_id: $courseSessionId) {\n id\n completion_status\n submitted_at\n evaluation_points\n evaluation_max_points\n evaluation_passed\n }\n learning_content {\n ...CoursePageFields\n circle {\n id\n title\n slug\n }\n }\n }\n }\n }\n }\n"): (typeof documents)["\n query competenceCertificateQuery($courseSlug: String!, $courseSessionId: ID!) {\n competence_certificate_list(course_slug: $courseSlug) {\n ...CoursePageFields\n competence_certificates {\n ...CoursePageFields\n assignments {\n ...CoursePageFields\n assignment_type\n max_points\n completion(course_session_id: $courseSessionId) {\n id\n completion_status\n submitted_at\n evaluation_points\n evaluation_max_points\n evaluation_passed\n }\n learning_content {\n ...CoursePageFields\n circle {\n id\n title\n slug\n }\n }\n }\n }\n }\n }\n"];
|
||||
/**
|
||||
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
|
||||
*/
|
||||
export function graphql(source: "\n query courseSessionDetail($courseSessionId: ID!) {\n course_session(id: $courseSessionId) {\n id\n title\n course {\n id\n title\n slug\n }\n users {\n id\n user_id\n first_name\n last_name\n email\n avatar_url\n role\n circles {\n id\n title\n slug\n }\n }\n attendance_courses {\n id\n location\n trainer\n due_date {\n id\n start\n end\n }\n learning_content_id\n learning_content {\n id\n title\n circle {\n id\n title\n slug\n }\n }\n }\n assignments {\n id\n submission_deadline {\n id\n start\n }\n evaluation_deadline {\n id\n start\n }\n learning_content {\n id\n title\n content_assignment {\n id\n title\n assignment_type\n }\n }\n }\n edoniq_tests {\n id\n deadline {\n id\n start\n end\n }\n learning_content {\n id\n title\n content_assignment {\n id\n title\n assignment_type\n }\n }\n }\n }\n }\n"): (typeof documents)["\n query courseSessionDetail($courseSessionId: ID!) {\n course_session(id: $courseSessionId) {\n id\n title\n course {\n id\n title\n slug\n }\n users {\n id\n user_id\n first_name\n last_name\n email\n avatar_url\n role\n circles {\n id\n title\n slug\n }\n }\n attendance_courses {\n id\n location\n trainer\n due_date {\n id\n start\n end\n }\n learning_content_id\n learning_content {\n id\n title\n circle {\n id\n title\n slug\n }\n }\n }\n assignments {\n id\n submission_deadline {\n id\n start\n }\n evaluation_deadline {\n id\n start\n }\n learning_content {\n id\n title\n content_assignment {\n id\n title\n assignment_type\n }\n }\n }\n edoniq_tests {\n id\n deadline {\n id\n start\n end\n }\n learning_content {\n id\n title\n content_assignment {\n id\n title\n assignment_type\n }\n }\n }\n }\n }\n"];
|
||||
/**
|
||||
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
|
||||
*/
|
||||
export function graphql(source: "\n query courseQuery($slug: String!) {\n course(slug: $slug) {\n id\n title\n slug\n category_name\n action_competences {\n competence_id\n ...CoursePageFields\n performance_criteria {\n competence_id\n learning_unit {\n id\n slug\n evaluate_url\n }\n ...CoursePageFields\n }\n }\n learning_path {\n ...CoursePageFields\n topics {\n is_visible\n ...CoursePageFields\n circles {\n description\n goals\n ...CoursePageFields\n learning_sequences {\n icon\n ...CoursePageFields\n learning_units {\n evaluate_url\n ...CoursePageFields\n performance_criteria {\n ...CoursePageFields\n }\n learning_contents {\n can_user_self_toggle_course_completion\n content_url\n minutes\n description\n ...CoursePageFields\n ... on LearningContentAssignmentObjectType {\n assignment_type\n content_assignment {\n id\n }\n competence_certificate {\n ...CoursePageFields\n }\n }\n ... on LearningContentEdoniqTestObjectType {\n checkbox_text\n has_extended_time_test\n content_assignment {\n id\n }\n competence_certificate {\n ...CoursePageFields\n }\n }\n ... on LearningContentRichTextObjectType {\n text\n }\n }\n }\n }\n }\n }\n }\n }\n }\n"): (typeof documents)["\n query courseQuery($slug: String!) {\n course(slug: $slug) {\n id\n title\n slug\n category_name\n action_competences {\n competence_id\n ...CoursePageFields\n performance_criteria {\n competence_id\n learning_unit {\n id\n slug\n evaluate_url\n }\n ...CoursePageFields\n }\n }\n learning_path {\n ...CoursePageFields\n topics {\n is_visible\n ...CoursePageFields\n circles {\n description\n goals\n ...CoursePageFields\n learning_sequences {\n icon\n ...CoursePageFields\n learning_units {\n evaluate_url\n ...CoursePageFields\n performance_criteria {\n ...CoursePageFields\n }\n learning_contents {\n can_user_self_toggle_course_completion\n content_url\n minutes\n description\n ...CoursePageFields\n ... on LearningContentAssignmentObjectType {\n assignment_type\n content_assignment {\n id\n }\n competence_certificate {\n ...CoursePageFields\n }\n }\n ... on LearningContentEdoniqTestObjectType {\n checkbox_text\n has_extended_time_test\n content_assignment {\n id\n }\n competence_certificate {\n ...CoursePageFields\n }\n }\n ... on LearningContentRichTextObjectType {\n text\n }\n }\n }\n }\n }\n }\n }\n }\n }\n"];
|
||||
/**
|
||||
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
|
||||
*/
|
||||
|
|
|
|||
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large
Load Diff
|
|
@ -1,3 +1,4 @@
|
|||
export const ActionCompetenceObjectType = "ActionCompetenceObjectType";
|
||||
export const AssignmentAssignmentAssignmentTypeChoices = "AssignmentAssignmentAssignmentTypeChoices";
|
||||
export const AssignmentAssignmentCompletionCompletionStatusChoices = "AssignmentAssignmentCompletionCompletionStatusChoices";
|
||||
export const AssignmentCompletionMutation = "AssignmentCompletionMutation";
|
||||
|
|
@ -9,11 +10,12 @@ export const AttendanceUserInputType = "AttendanceUserInputType";
|
|||
export const AttendanceUserObjectType = "AttendanceUserObjectType";
|
||||
export const AttendanceUserStatus = "AttendanceUserStatus";
|
||||
export const Boolean = "Boolean";
|
||||
export const CircleDocumentObjectType = "CircleDocumentObjectType";
|
||||
export const CircleLightObjectType = "CircleLightObjectType";
|
||||
export const CircleObjectType = "CircleObjectType";
|
||||
export const CompetenceCertificateListObjectType = "CompetenceCertificateListObjectType";
|
||||
export const CompetenceCertificateObjectType = "CompetenceCertificateObjectType";
|
||||
export const CoreUserLanguageChoices = "CoreUserLanguageChoices";
|
||||
export const CourseCourseSessionUserRoleChoices = "CourseCourseSessionUserRoleChoices";
|
||||
export const CourseObjectType = "CourseObjectType";
|
||||
export const CoursePageInterface = "CoursePageInterface";
|
||||
export const CourseSessionAssignmentObjectType = "CourseSessionAssignmentObjectType";
|
||||
|
|
@ -49,9 +51,10 @@ export const LearningSequenceObjectType = "LearningSequenceObjectType";
|
|||
export const LearningUnitObjectType = "LearningUnitObjectType";
|
||||
export const LearnpathLearningContentAssignmentAssignmentTypeChoices = "LearnpathLearningContentAssignmentAssignmentTypeChoices";
|
||||
export const Mutation = "Mutation";
|
||||
export const PerformanceCriteriaObjectType = "PerformanceCriteriaObjectType";
|
||||
export const Query = "Query";
|
||||
export const SendFeedbackMutation = "SendFeedbackMutation";
|
||||
export const String = "String";
|
||||
export const TopicObjectType = "TopicObjectType";
|
||||
export const UUID = "UUID";
|
||||
export const UserType = "UserType";
|
||||
export const UserObjectType = "UserObjectType";
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
// @ts-ignore
|
||||
import { cacheExchange } from "@urql/exchange-graphcache";
|
||||
import { Client, fetchExchange } from "@urql/vue";
|
||||
import schema from "../gql/minifiedSchema.json";
|
||||
import schema from "../gql/dist/minifiedSchema.json";
|
||||
import {
|
||||
AssignmentCompletionMutation,
|
||||
AssignmentCompletionObjectType,
|
||||
|
|
|
|||
|
|
@ -76,20 +76,6 @@ export const ASSIGNMENT_COMPLETION_QUERY = graphql(`
|
|||
}
|
||||
`);
|
||||
|
||||
export const COURSE_QUERY = graphql(`
|
||||
query courseQuery($courseId: ID!) {
|
||||
course(id: $courseId) {
|
||||
id
|
||||
slug
|
||||
title
|
||||
category_name
|
||||
learning_path {
|
||||
id
|
||||
}
|
||||
}
|
||||
}
|
||||
`);
|
||||
|
||||
export const COMPETENCE_NAVI_CERTIFICATE_QUERY = graphql(`
|
||||
query competenceCertificateQuery($courseSlug: String!, $courseSessionId: ID!) {
|
||||
competence_certificate_list(course_slug: $courseSlug) {
|
||||
|
|
@ -109,13 +95,11 @@ export const COMPETENCE_NAVI_CERTIFICATE_QUERY = graphql(`
|
|||
evaluation_passed
|
||||
}
|
||||
learning_content {
|
||||
title
|
||||
id
|
||||
slug
|
||||
content_type
|
||||
frontend_url
|
||||
...CoursePageFields
|
||||
circle {
|
||||
...CoursePageFields
|
||||
id
|
||||
title
|
||||
slug
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -208,3 +192,79 @@ export const COURSE_SESSION_DETAIL_QUERY = graphql(`
|
|||
}
|
||||
}
|
||||
`);
|
||||
|
||||
export const COURSE_QUERY = graphql(`
|
||||
query courseQuery($slug: String!) {
|
||||
course(slug: $slug) {
|
||||
id
|
||||
title
|
||||
slug
|
||||
category_name
|
||||
action_competences {
|
||||
competence_id
|
||||
...CoursePageFields
|
||||
performance_criteria {
|
||||
competence_id
|
||||
learning_unit {
|
||||
id
|
||||
slug
|
||||
evaluate_url
|
||||
}
|
||||
...CoursePageFields
|
||||
}
|
||||
}
|
||||
learning_path {
|
||||
...CoursePageFields
|
||||
topics {
|
||||
is_visible
|
||||
...CoursePageFields
|
||||
circles {
|
||||
description
|
||||
goals
|
||||
...CoursePageFields
|
||||
learning_sequences {
|
||||
icon
|
||||
...CoursePageFields
|
||||
learning_units {
|
||||
evaluate_url
|
||||
...CoursePageFields
|
||||
performance_criteria {
|
||||
...CoursePageFields
|
||||
}
|
||||
learning_contents {
|
||||
can_user_self_toggle_course_completion
|
||||
content_url
|
||||
minutes
|
||||
description
|
||||
...CoursePageFields
|
||||
... on LearningContentAssignmentObjectType {
|
||||
assignment_type
|
||||
content_assignment {
|
||||
id
|
||||
}
|
||||
competence_certificate {
|
||||
...CoursePageFields
|
||||
}
|
||||
}
|
||||
... on LearningContentEdoniqTestObjectType {
|
||||
checkbox_text
|
||||
has_extended_time_test
|
||||
content_assignment {
|
||||
id
|
||||
}
|
||||
competence_certificate {
|
||||
...CoursePageFields
|
||||
}
|
||||
}
|
||||
... on LearningContentRichTextObjectType {
|
||||
text
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`);
|
||||
|
|
|
|||
|
|
@ -1,17 +1,16 @@
|
|||
<script setup lang="ts">
|
||||
import { computed, onMounted, ref, watch } from "vue";
|
||||
import { useCourseSessionsStore } from "@/stores/courseSessions";
|
||||
import { useLearningPathStore } from "@/stores/learningPath";
|
||||
import { useTranslation } from "i18next-vue";
|
||||
import ItDropdownSelect from "@/components/ui/ItDropdownSelect.vue";
|
||||
import type { DueDate } from "@/types";
|
||||
import DueDatesList from "@/components/dueDates/DueDatesList.vue";
|
||||
import { useCourseData } from "@/composables";
|
||||
|
||||
const { t } = useTranslation();
|
||||
|
||||
const UNFILTERED = Number.MAX_SAFE_INTEGER.toString();
|
||||
const courseSessionsStore = useCourseSessionsStore();
|
||||
const learningPathStore = useLearningPathStore();
|
||||
|
||||
type Item = {
|
||||
id: string;
|
||||
|
|
@ -61,16 +60,15 @@ const circles = ref<Item[]>([initialItemCircle]);
|
|||
const selectedCircle = ref<Item>(circles.value[0]);
|
||||
|
||||
async function loadCircleValues() {
|
||||
const data = await learningPathStore.loadLearningPath(
|
||||
`${selectedCourse.value.slug}-lp`,
|
||||
undefined,
|
||||
false,
|
||||
false
|
||||
);
|
||||
if (data) {
|
||||
if (selectedCourse.value) {
|
||||
const learningPathQuery = useCourseData(selectedCourse.value.slug);
|
||||
await learningPathQuery.resultPromise;
|
||||
circles.value = [
|
||||
initialItemCircle,
|
||||
...data.circles.map((circle) => ({ id: circle.id, name: circle.title })),
|
||||
...(learningPathQuery.circles.value ?? []).map((circle) => ({
|
||||
id: circle.id,
|
||||
name: circle.title,
|
||||
})),
|
||||
];
|
||||
} else {
|
||||
circles.value = [initialItemCircle];
|
||||
|
|
|
|||
|
|
@ -1,12 +1,12 @@
|
|||
<script setup lang="ts">
|
||||
import DueDatesList from "@/components/dueDates/DueDatesList.vue";
|
||||
import LearningPathDiagramSmall from "@/components/learningPath/LearningPathDiagramSmall.vue";
|
||||
import { useCourseSessionsStore } from "@/stores/courseSessions";
|
||||
import { useUserStore } from "@/stores/user";
|
||||
import type { CourseSession } from "@/types";
|
||||
import log from "loglevel";
|
||||
import { computed, onMounted } from "vue";
|
||||
import { getCockpitUrl, getLearningPathUrl } from "@/utils/utils";
|
||||
import LearningPathDiagram from "@/components/learningPath/LearningPathDiagram.vue";
|
||||
|
||||
log.debug("DashboardPage created");
|
||||
|
||||
|
|
@ -47,10 +47,12 @@ const getNextStepLink = (courseSession: CourseSession) => {
|
|||
<div class="bg-white p-6 md:h-full">
|
||||
<h3 class="mb-4">{{ courseSession.course.title }}</h3>
|
||||
<div>
|
||||
<LearningPathDiagramSmall
|
||||
<LearningPathDiagram
|
||||
class="mb-4"
|
||||
:course-slug="courseSession.course.slug"
|
||||
></LearningPathDiagramSmall>
|
||||
:course-session-id="courseSession.id"
|
||||
diagram-type="horizontalSmall"
|
||||
></LearningPathDiagram>
|
||||
</div>
|
||||
<div>
|
||||
<router-link
|
||||
|
|
|
|||
|
|
@ -1,11 +1,11 @@
|
|||
<script setup lang="ts">
|
||||
import { useCourseSessionDetailQuery } from "@/composables";
|
||||
import {
|
||||
useCourseDataWithCompletion,
|
||||
useCourseSessionDetailQuery,
|
||||
} from "@/composables";
|
||||
import { useCockpitStore } from "@/stores/cockpit";
|
||||
import { useCompetenceStore } from "@/stores/competence";
|
||||
import { useLearningPathStore } from "@/stores/learningPath";
|
||||
import { useUserStore } from "@/stores/user";
|
||||
import * as log from "loglevel";
|
||||
import { onMounted } from "vue";
|
||||
import { onMounted, ref } from "vue";
|
||||
|
||||
log.debug("CockpitParentPage created");
|
||||
|
||||
|
|
@ -14,33 +14,28 @@ const props = defineProps<{
|
|||
}>();
|
||||
|
||||
const cockpitStore = useCockpitStore();
|
||||
const competenceStore = useCompetenceStore();
|
||||
const learningPathStore = useLearningPathStore();
|
||||
|
||||
const courseSessionDetailResult = useCourseSessionDetailQuery();
|
||||
|
||||
const loaded = ref(false);
|
||||
|
||||
onMounted(async () => {
|
||||
log.debug("CockpitParentPage mounted", props.courseSlug);
|
||||
|
||||
try {
|
||||
await courseSessionDetailResult.waitForData();
|
||||
const members = courseSessionDetailResult.filterMembers();
|
||||
members.forEach((csu) => {
|
||||
competenceStore.loadCompetenceProfilePage(
|
||||
props.courseSlug + "-competencenavi-competences",
|
||||
csu.user_id
|
||||
);
|
||||
|
||||
learningPathStore.loadLearningPath(props.courseSlug + "-lp", csu.user_id);
|
||||
});
|
||||
await learningPathStore.loadLearningPath(
|
||||
props.courseSlug + "-lp",
|
||||
useUserStore().id
|
||||
);
|
||||
await cockpitStore.loadCircles(
|
||||
props.courseSlug,
|
||||
courseSessionDetailResult.findCurrentUser()
|
||||
);
|
||||
|
||||
// workaround so that the completion data is loaded before display
|
||||
const userDataPromises = courseSessionDetailResult.filterMembers().map((m) => {
|
||||
const completionData = useCourseDataWithCompletion(props.courseSlug, m.id);
|
||||
return completionData.resultPromise;
|
||||
});
|
||||
await Promise.all(userDataPromises);
|
||||
|
||||
loaded.value = true;
|
||||
} catch (error) {
|
||||
log.error(error);
|
||||
}
|
||||
|
|
@ -50,7 +45,9 @@ onMounted(async () => {
|
|||
<template>
|
||||
<div class="bg-gray-200">
|
||||
<main>
|
||||
<router-view></router-view>
|
||||
<div v-if="loaded">
|
||||
<router-view></router-view>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
</template>
|
||||
|
|
|
|||
|
|
@ -1,12 +1,13 @@
|
|||
<script setup lang="ts">
|
||||
import { useCompetenceStore } from "@/stores/competence";
|
||||
import { useLearningPathStore } from "@/stores/learningPath";
|
||||
import * as log from "loglevel";
|
||||
import { computed, onMounted } from "vue";
|
||||
|
||||
import CompetenceDetail from "@/pages/competence/ActionCompetenceDetail.vue";
|
||||
import LearningPathPathView from "@/pages/learningPath/learningPathPage/LearningPathPathView.vue";
|
||||
import { useCourseSessionDetailQuery } from "@/composables";
|
||||
import {
|
||||
useCourseSessionDetailQuery,
|
||||
useCourseDataWithCompletion,
|
||||
} from "@/composables";
|
||||
|
||||
const props = defineProps<{
|
||||
userId: string;
|
||||
|
|
@ -15,15 +16,19 @@ const props = defineProps<{
|
|||
|
||||
log.debug("CockpitUserProfilePage created", props.userId);
|
||||
|
||||
const competenceStore = useCompetenceStore();
|
||||
const learningPathStore = useLearningPathStore();
|
||||
const courseCompletionData = useCourseDataWithCompletion(
|
||||
props.courseSlug,
|
||||
props.userId
|
||||
);
|
||||
|
||||
onMounted(async () => {
|
||||
log.debug("CockpitUserProfilePage mounted");
|
||||
});
|
||||
|
||||
const lpQueryResult = useCourseDataWithCompletion(props.courseSlug, props.userId);
|
||||
|
||||
const learningPath = computed(() => {
|
||||
return learningPathStore.learningPathForUser(props.courseSlug, props.userId);
|
||||
return lpQueryResult.learningPath.value;
|
||||
});
|
||||
|
||||
const { findUser } = useCourseSessionDetailQuery();
|
||||
|
|
@ -66,6 +71,7 @@ function setActiveClasses(isActive: boolean) {
|
|||
:use-mobile-layout="false"
|
||||
:hide-buttons="true"
|
||||
:learning-path="learningPath"
|
||||
:next-learning-content="undefined"
|
||||
:override-circle-url-base="`/course/${props.courseSlug}/cockpit/profile/${props.userId}`"
|
||||
></LearningPathPathView>
|
||||
</div>
|
||||
|
|
@ -84,12 +90,9 @@ function setActiveClasses(isActive: boolean) {
|
|||
</li>
|
||||
</ul>
|
||||
<div>
|
||||
<ul
|
||||
v-if="competenceStore.competenceProfilePage(user.user_id)"
|
||||
class="bg-white px-8"
|
||||
>
|
||||
<ul class="bg-white px-8">
|
||||
<li
|
||||
v-for="competence in competenceStore.competences(user.user_id)"
|
||||
v-for="competence in courseCompletionData.actionCompetences.value ?? []"
|
||||
:key="competence.id"
|
||||
class="border-b border-gray-500 p-8 last:border-0"
|
||||
>
|
||||
|
|
|
|||
|
|
@ -65,7 +65,7 @@ const assignmentDetail = computed(() => {
|
|||
});
|
||||
|
||||
const dueDate = computed(() =>
|
||||
dayjs(assignmentDetail.value?.evaluation_deadline.start)
|
||||
dayjs(assignmentDetail.value?.evaluation_deadline?.start)
|
||||
);
|
||||
|
||||
const inEvaluationTask = computed(
|
||||
|
|
|
|||
|
|
@ -81,7 +81,7 @@ const courseSessionDetailResult = useCourseSessionDetailQuery();
|
|||
const evaluationUser = computed(() => {
|
||||
if (props.assignmentCompletion.evaluation_user) {
|
||||
return courseSessionDetailResult.findUser(
|
||||
props.assignmentCompletion.evaluation_user
|
||||
props.assignmentCompletion.evaluation_user?.id
|
||||
);
|
||||
}
|
||||
return undefined;
|
||||
|
|
@ -113,7 +113,12 @@ const evaluationUser = computed(() => {
|
|||
</div>
|
||||
</section>
|
||||
|
||||
<div v-if="!props.assignmentCompletion.evaluation_passed">
|
||||
<div
|
||||
v-if="
|
||||
props.assignmentCompletion.completion_status === 'EVALUATION_SUBMITTED' &&
|
||||
!props.assignmentCompletion.evaluation_passed
|
||||
"
|
||||
>
|
||||
<span class="my-2 rounded-md bg-error-red-200 px-2.5 py-0.5">
|
||||
{{ $t("a.Nicht Bestanden") }}
|
||||
</span>
|
||||
|
|
|
|||
|
|
@ -13,16 +13,14 @@ import { computed, onMounted, reactive } from "vue";
|
|||
import AssignmentSubmissionProgress from "@/pages/cockpit/cockpitPage/AssignmentSubmissionProgress.vue";
|
||||
import { useCourseSessionDetailQuery } from "@/composables";
|
||||
import { formatDueDate } from "../../../components/dueDates/dueDatesUtils";
|
||||
import { stringifyParse } from "@/utils/utils";
|
||||
|
||||
const props = defineProps<{
|
||||
courseSession: CourseSession;
|
||||
learningContentAssignment: LearningContentAssignment;
|
||||
}>();
|
||||
|
||||
log.debug(
|
||||
"AssignmentDetails created",
|
||||
props.learningContentAssignment.content_assignment_id
|
||||
);
|
||||
log.debug("AssignmentDetails created", stringifyParse(props));
|
||||
|
||||
const courseSessionDetailResult = useCourseSessionDetailQuery();
|
||||
|
||||
|
|
@ -39,7 +37,7 @@ const assignmentDetail = computed(() => {
|
|||
onMounted(async () => {
|
||||
const { gradedUsers, assignmentSubmittedUsers } =
|
||||
await loadAssignmentCompletionStatusData(
|
||||
props.learningContentAssignment.content_assignment_id,
|
||||
props.learningContentAssignment.content_assignment.id,
|
||||
props.courseSession.id,
|
||||
props.learningContentAssignment.id
|
||||
);
|
||||
|
|
@ -54,17 +52,17 @@ onMounted(async () => {
|
|||
{{ learningContentAssignment.title }}
|
||||
</h2>
|
||||
<div class="pt-1 underline">
|
||||
Circle «{{ learningContentAssignment.parentCircle.title }}»
|
||||
{{ $t("a.Circle") }} «{{ learningContentAssignment.circle?.title }}»
|
||||
</div>
|
||||
<div v-if="assignmentDetail">
|
||||
<span v-if="assignmentDetail.submission_deadline?.start">
|
||||
{{ $t("Abgabetermin Ergebnisse:") }}
|
||||
{{ formatDueDate(assignmentDetail.submission_deadline.start) }}
|
||||
{{ formatDueDate(assignmentDetail.submission_deadline?.start) }}
|
||||
</span>
|
||||
<template v-if="assignmentDetail.evaluation_deadline?.start">
|
||||
<br />
|
||||
{{ $t("Freigabetermin Bewertungen:") }}
|
||||
{{ formatDueDate(assignmentDetail.evaluation_deadline.start) }}
|
||||
{{ formatDueDate(assignmentDetail.evaluation_deadline?.start) }}
|
||||
</template>
|
||||
</div>
|
||||
<div v-else>
|
||||
|
|
@ -131,7 +129,7 @@ onMounted(async () => {
|
|||
<template #link>
|
||||
<router-link
|
||||
v-if="state.assignmentSubmittedUsers.includes(csu)"
|
||||
:to="`/course/${props.courseSession.course.slug}/cockpit/assignment/${learningContentAssignment.content_assignment_id}/${csu.user_id}`"
|
||||
:to="`/course/${props.courseSession.course.slug}/cockpit/assignment/${learningContentAssignment.content_assignment.id}/${csu.user_id}`"
|
||||
class="link lg:w-full lg:text-right"
|
||||
data-cy="show-results"
|
||||
>
|
||||
|
|
|
|||
|
|
@ -1,11 +1,9 @@
|
|||
<script setup lang="ts">
|
||||
import { useCurrentCourseSession } from "@/composables";
|
||||
import { useCurrentCourseSession, useCourseData } from "@/composables";
|
||||
import AssignmentDetails from "@/pages/cockpit/assignmentsPage/AssignmentDetails.vue";
|
||||
import * as log from "loglevel";
|
||||
import { computed, onMounted } from "vue";
|
||||
import { useUserStore } from "@/stores/user";
|
||||
import { useLearningPathStore } from "@/stores/learningPath";
|
||||
import { calcLearningContentAssignments } from "@/services/assignmentService";
|
||||
import type { LearningContentAssignment } from "@/types";
|
||||
|
||||
const props = defineProps<{
|
||||
courseSlug: string;
|
||||
|
|
@ -15,17 +13,15 @@ const props = defineProps<{
|
|||
log.debug("AssignmentsPage created", props.courseSlug);
|
||||
|
||||
const courseSession = useCurrentCourseSession();
|
||||
const userStore = useUserStore();
|
||||
const learningPathStore = useLearningPathStore();
|
||||
|
||||
onMounted(async () => {
|
||||
log.debug("AssignmentsPage mounted");
|
||||
});
|
||||
|
||||
const lpQueryResult = useCourseData(props.courseSlug);
|
||||
|
||||
const learningContentAssignment = computed(() => {
|
||||
return calcLearningContentAssignments(
|
||||
learningPathStore.learningPathForUser(courseSession.value.course.slug, userStore.id)
|
||||
).filter((lc) => lc.id === props.assignmentId)[0];
|
||||
return lpQueryResult.findLearningContent(props.assignmentId);
|
||||
});
|
||||
</script>
|
||||
|
||||
|
|
@ -46,7 +42,7 @@ const learningContentAssignment = computed(() => {
|
|||
<AssignmentDetails
|
||||
v-if="learningContentAssignment"
|
||||
:course-session="courseSession"
|
||||
:learning-content-assignment="learningContentAssignment"
|
||||
:learning-content-assignment="learningContentAssignment as LearningContentAssignment"
|
||||
/>
|
||||
</div>
|
||||
</main>
|
||||
|
|
|
|||
|
|
@ -32,8 +32,8 @@ const presenceCoursesDropdownOptions = computed(() => {
|
|||
({
|
||||
id: attendanceCourse.id,
|
||||
name: `${t("Präsenzkurs")} ${
|
||||
attendanceCourse.learning_content.circle.title
|
||||
} ${dayjs(attendanceCourse.due_date.start).format("DD.MM.YYYY")}`,
|
||||
attendanceCourse.learning_content.circle?.title
|
||||
} ${dayjs(attendanceCourse.due_date?.start).format("DD.MM.YYYY")}`,
|
||||
} as DropdownSelectable)
|
||||
);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ import type {
|
|||
} from "@/types";
|
||||
import log from "loglevel";
|
||||
import { onMounted, reactive } from "vue";
|
||||
import { stringifyParse } from "@/utils/utils";
|
||||
|
||||
const props = defineProps<{
|
||||
courseSession: CourseSession;
|
||||
|
|
@ -16,10 +17,7 @@ const props = defineProps<{
|
|||
showTitle: boolean;
|
||||
}>();
|
||||
|
||||
log.debug(
|
||||
"AssignmentSubmissionProgress created",
|
||||
props.learningContentAssignment.content_assignment_id
|
||||
);
|
||||
log.debug("AssignmentSubmissionProgress created", stringifyParse(props));
|
||||
|
||||
const state = reactive({
|
||||
statusByUser: [] as {
|
||||
|
|
@ -34,7 +32,7 @@ const state = reactive({
|
|||
onMounted(async () => {
|
||||
const { assignmentSubmittedUsers, gradedUsers, total } =
|
||||
await loadAssignmentCompletionStatusData(
|
||||
props.learningContentAssignment.content_assignment_id,
|
||||
props.learningContentAssignment.content_assignment.id,
|
||||
props.courseSession.id,
|
||||
props.learningContentAssignment.id
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1,16 +1,14 @@
|
|||
<script setup lang="ts">
|
||||
import LearningPathDiagram from "@/components/learningPath/LearningPathDiagram.vue";
|
||||
import ItPersonRow from "@/components/ui/ItPersonRow.vue";
|
||||
import type { LearningPath } from "@/services/learningPath";
|
||||
|
||||
import { useCourseSessionDetailQuery, useCurrentCourseSession } from "@/composables";
|
||||
import SubmissionsOverview from "@/pages/cockpit/cockpitPage/SubmissionsOverview.vue";
|
||||
import { useCockpitStore } from "@/stores/cockpit";
|
||||
import { useCompetenceStore } from "@/stores/competence";
|
||||
import { useLearningPathStore } from "@/stores/learningPath";
|
||||
import log from "loglevel";
|
||||
import CockpitDates from "@/pages/cockpit/cockpitPage/CockpitDates.vue";
|
||||
import ItDropdownSelect from "@/components/ui/ItDropdownSelect.vue";
|
||||
import UserStatusCount from "@/pages/cockpit/cockpitPage/UserStatusCount.vue";
|
||||
|
||||
const props = defineProps<{
|
||||
courseSlug: string;
|
||||
|
|
@ -19,19 +17,8 @@ const props = defineProps<{
|
|||
log.debug("CockpitIndexPage created", props.courseSlug);
|
||||
|
||||
const cockpitStore = useCockpitStore();
|
||||
const competenceStore = useCompetenceStore();
|
||||
const learningPathStore = useLearningPathStore();
|
||||
const courseSession = useCurrentCourseSession();
|
||||
const courseSessionDetailResult = useCourseSessionDetailQuery();
|
||||
|
||||
function userCountStatusForCircle(userId: string) {
|
||||
if (!cockpitStore.currentCircle) return { FAIL: 0, SUCCESS: 0, UNKNOWN: 0 };
|
||||
const criteria = competenceStore.flatPerformanceCriteria(
|
||||
userId,
|
||||
cockpitStore.currentCircle.id
|
||||
);
|
||||
return competenceStore.calcStatusCount(criteria);
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
|
@ -143,18 +130,9 @@ function userCountStatusForCircle(userId: string) {
|
|||
class="mt-2 flex w-full flex-col items-center justify-between lg:mt-0 lg:flex-row"
|
||||
>
|
||||
<LearningPathDiagram
|
||||
v-if="
|
||||
learningPathStore.learningPathForUser(
|
||||
props.courseSlug,
|
||||
csu.user_id
|
||||
)
|
||||
"
|
||||
:learning-path="
|
||||
learningPathStore.learningPathForUser(
|
||||
props.courseSlug,
|
||||
csu.user_id
|
||||
) as LearningPath
|
||||
"
|
||||
:course-session-id="courseSession.id"
|
||||
:course-slug="props.courseSlug"
|
||||
:user-id="csu.user_id"
|
||||
:show-circle-slugs="[cockpitStore.currentCircle.slug]"
|
||||
diagram-type="singleSmall"
|
||||
class="mr-4"
|
||||
|
|
@ -162,32 +140,10 @@ function userCountStatusForCircle(userId: string) {
|
|||
<p class="lg:min-w-[150px]">
|
||||
{{ cockpitStore.currentCircle.title }}
|
||||
</p>
|
||||
<div class="ml-4 flex flex-row items-center">
|
||||
<div class="mr-6 flex flex-row items-center">
|
||||
<it-icon-smiley-thinking
|
||||
class="mr-2 inline-block h-8 w-8"
|
||||
></it-icon-smiley-thinking>
|
||||
<p class="text-bold inline-block w-6">
|
||||
{{ userCountStatusForCircle(csu.user_id).FAIL }}
|
||||
</p>
|
||||
</div>
|
||||
<li class="mr-6 flex flex-row items-center">
|
||||
<it-icon-smiley-happy
|
||||
class="mr-2 inline-block h-8 w-8"
|
||||
></it-icon-smiley-happy>
|
||||
<p class="text-bold inline-block w-6">
|
||||
{{ userCountStatusForCircle(csu.user_id).SUCCESS }}
|
||||
</p>
|
||||
</li>
|
||||
<li class="flex flex-row items-center">
|
||||
<it-icon-smiley-neutral
|
||||
class="mr-2 inline-block h-8 w-8"
|
||||
></it-icon-smiley-neutral>
|
||||
<p class="text-bold inline-block w-6">
|
||||
{{ userCountStatusForCircle(csu.user_id).UNKNOWN }}
|
||||
</p>
|
||||
</li>
|
||||
</div>
|
||||
<UserStatusCount
|
||||
:course-slug="props.courseSlug"
|
||||
:user-id="csu.user_id"
|
||||
></UserStatusCount>
|
||||
</div>
|
||||
</template>
|
||||
<template #link>
|
||||
|
|
|
|||
|
|
@ -1,8 +1,6 @@
|
|||
2
|
||||
<script setup lang="ts">
|
||||
import AssignmentSubmissionProgress from "@/pages/cockpit/cockpitPage/AssignmentSubmissionProgress.vue";
|
||||
import { useLearningPathStore } from "@/stores/learningPath";
|
||||
import { useUserStore } from "@/stores/user";
|
||||
import type {
|
||||
CourseSession,
|
||||
LearningContent,
|
||||
|
|
@ -13,11 +11,16 @@ import { computed } from "vue";
|
|||
import { useTranslation } from "i18next-vue";
|
||||
import FeedbackSubmissionProgress from "@/pages/cockpit/cockpitPage/FeedbackSubmissionProgress.vue";
|
||||
import { learningContentTypeData } from "@/utils/typeMaps";
|
||||
import { useCourseSessionDetailQuery } from "@/composables";
|
||||
import {
|
||||
useCourseSessionDetailQuery,
|
||||
useCourseDataWithCompletion,
|
||||
} from "@/composables";
|
||||
import { circleFlatLearningContents } from "@/services/circle";
|
||||
|
||||
interface Submittable {
|
||||
id: string;
|
||||
circleName: string;
|
||||
circleId: string;
|
||||
frontendUrl: string;
|
||||
title: string;
|
||||
showDetailsText: string;
|
||||
|
|
@ -32,24 +35,20 @@ const props = defineProps<{
|
|||
|
||||
log.debug("SubmissionsOverview created", props.courseSession.id);
|
||||
|
||||
const userStore = useUserStore();
|
||||
const learningPathStore = useLearningPathStore();
|
||||
const courseSessionDetailResult = useCourseSessionDetailQuery();
|
||||
|
||||
const { t } = useTranslation();
|
||||
|
||||
const lpQueryResult = useCourseDataWithCompletion();
|
||||
|
||||
const submittables = computed(() => {
|
||||
const learningPath = learningPathStore.learningPathForUser(
|
||||
props.courseSession.course.slug,
|
||||
userStore.id
|
||||
);
|
||||
if (!learningPath) {
|
||||
if (!lpQueryResult.circles.value?.length) {
|
||||
return [];
|
||||
}
|
||||
return learningPath.circles
|
||||
return lpQueryResult.circles.value
|
||||
.filter((circle) => props.selectedCircle == circle.id)
|
||||
.flatMap((circle) => {
|
||||
const learningContents = circle.flatLearningContents.filter(
|
||||
const learningContents = circleFlatLearningContents(circle).filter(
|
||||
(lc) =>
|
||||
lc.content_type === "learnpath.LearningContentAssignment" ||
|
||||
lc.content_type === "learnpath.LearningContentFeedback"
|
||||
|
|
@ -59,10 +58,11 @@ const submittables = computed(() => {
|
|||
return {
|
||||
id: lc.id,
|
||||
circleName: circle.title,
|
||||
circleId: circle.id,
|
||||
frontendUrl: lc.frontend_url,
|
||||
title: getLearningContentType(lc),
|
||||
showDetailsText: getShowDetailsText(lc),
|
||||
detailsLink: getDetailsLink(lc),
|
||||
detailsLink: getDetailsLink(lc, circle.id),
|
||||
content: lc,
|
||||
};
|
||||
});
|
||||
|
|
@ -103,9 +103,9 @@ const getShowDetailsText = (lc: LearningContent) => {
|
|||
return t("Feedback anschauen");
|
||||
};
|
||||
|
||||
const getDetailsLink = (lc: LearningContent) => {
|
||||
const getDetailsLink = (lc: LearningContent, circleId: string) => {
|
||||
if (isFeedback(lc)) {
|
||||
return `cockpit/feedback/${lc.parentCircle.id}`;
|
||||
return `cockpit/feedback/${circleId}`;
|
||||
}
|
||||
return `cockpit/assignment/${lc.id}`;
|
||||
};
|
||||
|
|
@ -147,7 +147,9 @@ const getIconName = (lc: LearningContent) => {
|
|||
</div>
|
||||
<div class="flex flex-col">
|
||||
<h3 class="text-bold flex items-center gap-2">{{ submittable.title }}</h3>
|
||||
<p class="text-gray-800">Circle «{{ submittable.circleName }}»</p>
|
||||
<p class="text-gray-800">
|
||||
{{ $t("a.Circle") }} «{{ submittable.circleName }}»
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<AssignmentSubmissionProgress
|
||||
|
|
@ -160,7 +162,7 @@ const getIconName = (lc: LearningContent) => {
|
|||
<FeedbackSubmissionProgress
|
||||
v-if="isFeedback(submittable.content)"
|
||||
:course-session="props.courseSession"
|
||||
:circle-id="submittable.content.parentCircle.id"
|
||||
:circle-id="submittable.circleId"
|
||||
class="grow pr-8"
|
||||
></FeedbackSubmissionProgress>
|
||||
<div class="flex items-center lg:w-1/4 lg:justify-end">
|
||||
|
|
|
|||
|
|
@ -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">
|
||||
import { useCurrentCourseSession } from "@/composables";
|
||||
import { useCurrentCourseSession, useCourseData } from "@/composables";
|
||||
import ItDropdownSelect from "@/components/ui/ItDropdownSelect.vue";
|
||||
import { useCockpitStore } from "@/stores/cockpit";
|
||||
import ItModal from "@/components/ui/ItModal.vue";
|
||||
|
|
@ -16,13 +16,11 @@ import {
|
|||
} from "@/services/files";
|
||||
import { useCourseSessionsStore } from "@/stores/courseSessions";
|
||||
import DocumentListItem from "@/components/circle/DocumentListItem.vue";
|
||||
import { useCircleStore } from "@/stores/circle";
|
||||
|
||||
const cockpitStore = useCockpitStore();
|
||||
const courseSession = useCurrentCourseSession();
|
||||
const circleStore = useCircleStore();
|
||||
|
||||
const courseSessionsStore = useCourseSessionsStore();
|
||||
const courseData = useCourseData(courseSession.value?.course.slug);
|
||||
|
||||
const { t } = useTranslation();
|
||||
|
||||
|
|
@ -51,26 +49,18 @@ onMounted(async () => {
|
|||
await fetchDocuments();
|
||||
});
|
||||
|
||||
watch(
|
||||
// workaround to load learning sequences when circle changes
|
||||
() => cockpitStore.currentCircle,
|
||||
async () => {
|
||||
if (cockpitStore.currentCircle) {
|
||||
await circleStore.loadCircle(
|
||||
courseSession.value?.course.slug,
|
||||
cockpitStore.currentCircle?.slug
|
||||
);
|
||||
const dropdownLearningSequences = computed(() => {
|
||||
if (cockpitStore.currentCircle?.slug) {
|
||||
const circle = courseData.findCircle(cockpitStore.currentCircle?.slug);
|
||||
if (circle) {
|
||||
return circle.learning_sequences.map((sequence) => ({
|
||||
id: sequence.id,
|
||||
name: `${sequence.title}`,
|
||||
}));
|
||||
}
|
||||
},
|
||||
{ immediate: true }
|
||||
);
|
||||
|
||||
const dropdownLearningSequences = computed(() =>
|
||||
circleStore.circle?.learningSequences.map((sequence) => ({
|
||||
id: sequence.id,
|
||||
name: `${sequence.title}`,
|
||||
}))
|
||||
);
|
||||
}
|
||||
return [];
|
||||
});
|
||||
|
||||
const circleDocuments = computed(() => {
|
||||
return circleDocumentsResultData.value.filter(
|
||||
|
|
|
|||
|
|
@ -1,15 +1,12 @@
|
|||
<script setup lang="ts">
|
||||
import ItToggleArrow from "@/components/ui/ItToggleArrow.vue";
|
||||
import { useCompetenceStore } from "@/stores/competence";
|
||||
import type { CompetencePage } from "@/types";
|
||||
import log from "loglevel";
|
||||
import { ref, watch } from "vue";
|
||||
import SinglePerformanceCriteriaRow from "@/pages/competence/SinglePerformanceCriteriaRow.vue";
|
||||
|
||||
const competenceStore = useCompetenceStore();
|
||||
import type { ActionCompetence } from "@/types";
|
||||
|
||||
interface Props {
|
||||
competence: CompetencePage;
|
||||
competence: ActionCompetence;
|
||||
courseSlug: string;
|
||||
showAssessAgain?: boolean;
|
||||
isInline?: boolean;
|
||||
|
|
@ -60,12 +57,12 @@ const togglePerformanceCriteria = () => {
|
|||
</li>
|
||||
|
||||
<li
|
||||
v-for="performanceCriteria in competenceStore.criteriaByCompetence(competence)"
|
||||
v-for="performanceCriteria in competence.performance_criteria"
|
||||
:key="performanceCriteria.id"
|
||||
class="my-4 border-b pb-4 last:border-0"
|
||||
>
|
||||
<SinglePerformanceCriteriaRow
|
||||
:criteria="performanceCriteria"
|
||||
:criterion="performanceCriteria"
|
||||
:show-state="false"
|
||||
:course-slug="props.courseSlug"
|
||||
:show-assess-again="props.showAssessAgain"
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
<script setup lang="ts">
|
||||
import CompetenceDetail from "@/pages/competence/ActionCompetenceDetail.vue";
|
||||
import { useCompetenceStore } from "@/stores/competence";
|
||||
import * as log from "loglevel";
|
||||
import { ref } from "vue";
|
||||
import { useCourseDataWithCompletion } from "@/composables";
|
||||
|
||||
log.debug("CompetenceListPage created");
|
||||
|
||||
|
|
@ -10,7 +10,7 @@ const props = defineProps<{
|
|||
courseSlug: string;
|
||||
}>();
|
||||
|
||||
const competenceStore = useCompetenceStore();
|
||||
const courseData = useCourseDataWithCompletion(props.courseSlug);
|
||||
|
||||
const isOpenAll = ref(false);
|
||||
|
||||
|
|
@ -40,9 +40,9 @@ function toggleOpen() {
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<ul v-if="competenceStore.competenceProfilePage()">
|
||||
<ul v-if="courseData.actionCompetences.value?.length">
|
||||
<li
|
||||
v-for="competence in competenceStore.competences()"
|
||||
v-for="competence in courseData.actionCompetences.value"
|
||||
:key="competence.id"
|
||||
class="mb-8 bg-white px-8 pt-6"
|
||||
>
|
||||
|
|
|
|||
|
|
@ -4,14 +4,14 @@ import { COMPETENCE_NAVI_CERTIFICATE_QUERY } from "@/graphql/queries";
|
|||
import { useQuery } from "@urql/vue";
|
||||
import { computed } from "vue";
|
||||
import type { CompetenceCertificate } from "@/types";
|
||||
import { useCurrentCourseSession } from "@/composables";
|
||||
import { useCurrentCourseSession, useCourseDataWithCompletion } from "@/composables";
|
||||
import {
|
||||
assignmentsMaxEvaluationPoints,
|
||||
assignmentsUserPoints,
|
||||
competenceCertificateProgressStatusCount,
|
||||
} from "@/pages/competence/utils";
|
||||
import ItProgress from "@/components/ui/ItProgress.vue";
|
||||
import { useCompetenceStore } from "@/stores/competence";
|
||||
import { calcPerformanceCriteriaStatusCount } from "@/services/competence";
|
||||
|
||||
const props = defineProps<{
|
||||
courseSlug: string;
|
||||
|
|
@ -20,8 +20,7 @@ const props = defineProps<{
|
|||
log.debug("CompetenceIndexPage setup", props);
|
||||
|
||||
const courseSession = useCurrentCourseSession();
|
||||
|
||||
const competenceStore = useCompetenceStore();
|
||||
const courseData = useCourseDataWithCompletion(props.courseSlug);
|
||||
|
||||
const certificatesQuery = useQuery({
|
||||
query: COMPETENCE_NAVI_CERTIFICATE_QUERY,
|
||||
|
|
@ -51,7 +50,7 @@ const userPointsEvaluatedAssignments = computed(() => {
|
|||
});
|
||||
|
||||
const performanceCriteriaStatusCount = computed(() => {
|
||||
return competenceStore.calcStatusCount(competenceStore.flatPerformanceCriteria());
|
||||
return calcPerformanceCriteriaStatusCount(courseData.flatPerformanceCriteria.value);
|
||||
});
|
||||
</script>
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,4 @@
|
|||
<script setup lang="ts">
|
||||
import { useCompetenceStore } from "@/stores/competence";
|
||||
import * as log from "loglevel";
|
||||
import { onMounted } from "vue";
|
||||
import { useRoute } from "vue-router";
|
||||
|
|
@ -10,8 +9,6 @@ const props = defineProps<{
|
|||
courseSlug: string;
|
||||
}>();
|
||||
|
||||
const competenceStore = useCompetenceStore();
|
||||
|
||||
const route = useRoute();
|
||||
|
||||
function routeInOverview() {
|
||||
|
|
@ -32,13 +29,6 @@ function routeInActionCompetences() {
|
|||
|
||||
onMounted(async () => {
|
||||
log.debug("CompetenceParentPage mounted", props.courseSlug);
|
||||
|
||||
try {
|
||||
const competencePageSlug = props.courseSlug + "-competencenavi-competences";
|
||||
await competenceStore.loadCompetenceProfilePage(competencePageSlug);
|
||||
} catch (error) {
|
||||
log.error(error);
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
<script setup lang="ts">
|
||||
import { useCompetenceStore } from "@/stores/competence";
|
||||
import * as log from "loglevel";
|
||||
import { computed } from "vue";
|
||||
import _ from "lodash";
|
||||
import { useCourseDataWithCompletion } from "@/composables";
|
||||
|
||||
const props = defineProps<{
|
||||
courseSlug: string;
|
||||
|
|
@ -10,28 +10,30 @@ const props = defineProps<{
|
|||
|
||||
log.debug("PerformanceCriteriaPage created", props);
|
||||
|
||||
const competenceStore = useCompetenceStore();
|
||||
const courseCompletionData = useCourseDataWithCompletion(props.courseSlug);
|
||||
|
||||
const uniqueLearningUnits = computed(() => {
|
||||
// FIXME: this complex calculation can go away,
|
||||
// once the criteria are in its own learning content
|
||||
// get the learningUnits sorted by circle order in the course
|
||||
const circles = competenceStore.circles.map((c, index) => {
|
||||
const circles = (courseCompletionData.circles.value ?? []).map((c, index) => {
|
||||
return { ...c, sortKey: index };
|
||||
});
|
||||
return _.orderBy(
|
||||
_.uniqBy(
|
||||
competenceStore.flatPerformanceCriteria().map((pc) => {
|
||||
return {
|
||||
luId: pc.learning_unit.id,
|
||||
luTitle: pc.learning_unit.title,
|
||||
luSlug: pc.learning_unit.slug,
|
||||
circleId: pc.circle.id,
|
||||
circleTitle: pc.circle.title,
|
||||
url: pc.learning_unit.evaluate_url,
|
||||
sortKey: circles.find((c) => c.id === pc.circle.id)?.sortKey,
|
||||
};
|
||||
}),
|
||||
(courseCompletionData.flatPerformanceCriteria.value ?? [])
|
||||
.filter((pc) => Boolean(pc.learning_unit))
|
||||
.map((pc) => {
|
||||
return {
|
||||
luId: pc.learning_unit?.id,
|
||||
luTitle: pc.learning_unit?.title,
|
||||
luSlug: pc.learning_unit?.slug,
|
||||
circleId: pc.circle.id,
|
||||
circleTitle: pc.circle.title,
|
||||
url: pc.learning_unit?.evaluate_url,
|
||||
sortKey: circles.find((c) => c.id === pc.circle.id)?.sortKey,
|
||||
};
|
||||
}),
|
||||
"luId"
|
||||
),
|
||||
"sortKey"
|
||||
|
|
@ -40,9 +42,9 @@ const uniqueLearningUnits = computed(() => {
|
|||
|
||||
const criteriaByLearningUnit = computed(() => {
|
||||
return uniqueLearningUnits.value.map((lu) => {
|
||||
const criteria = competenceStore
|
||||
.flatPerformanceCriteria()
|
||||
.filter((pc) => pc.learning_unit.id === lu.luId);
|
||||
const criteria = (courseCompletionData.flatPerformanceCriteria.value ?? []).filter(
|
||||
(pc) => pc.learning_unit?.id === lu.luId
|
||||
);
|
||||
return {
|
||||
...lu,
|
||||
countSuccess: criteria.filter((c) => c.completion_status === "SUCCESS").length,
|
||||
|
|
@ -98,7 +100,7 @@ const criteriaByLearningUnit = computed(() => {
|
|||
|
||||
<div>
|
||||
<router-link
|
||||
:to="selfEvaluation.url"
|
||||
:to="selfEvaluation.url ?? '/'"
|
||||
class="link"
|
||||
:data-cy="`${selfEvaluation.luSlug}-open`"
|
||||
>
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
import type { PerformanceCriteria } from "@/types";
|
||||
|
||||
interface Props {
|
||||
criteria: PerformanceCriteria;
|
||||
criterion: PerformanceCriteria;
|
||||
courseSlug: string;
|
||||
showState?: boolean;
|
||||
showAssessAgain?: boolean;
|
||||
|
|
@ -19,24 +19,24 @@ const props = withDefaults(defineProps<Props>(), {
|
|||
<div class="flex flex-row items-center">
|
||||
<div v-if="showState" class="mr-4 h-8 w-8">
|
||||
<it-icon-smiley-happy
|
||||
v-if="criteria.completion_status === 'SUCCESS'"
|
||||
v-if="criterion.completion_status === 'SUCCESS'"
|
||||
></it-icon-smiley-happy>
|
||||
<it-icon-smiley-thinking
|
||||
v-else-if="criteria.completion_status === 'FAIL'"
|
||||
v-else-if="criterion.completion_status === 'FAIL'"
|
||||
></it-icon-smiley-thinking>
|
||||
<it-icon-smiley-neutral v-else></it-icon-smiley-neutral>
|
||||
</div>
|
||||
<div class="mb-4 pr-4 lg:mb-0 lg:mr-8">
|
||||
{{ criteria.title }}
|
||||
{{ criterion.title }}
|
||||
</div>
|
||||
</div>
|
||||
<span class="lg:whitespace-nowrap">
|
||||
<router-link
|
||||
v-if="props.showAssessAgain"
|
||||
v-if="props.showAssessAgain && criterion.learning_unit?.evaluate_url"
|
||||
class="link"
|
||||
:to="criteria.learning_unit.evaluate_url"
|
||||
:to="criterion.learning_unit.evaluate_url"
|
||||
>
|
||||
{{ $t("competences.assessAgain", { x: criteria.circle.title }) }}
|
||||
{{ $t("competences.assessAgain", { x: criterion.circle.title }) }}
|
||||
</router-link>
|
||||
</span>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
<script setup lang="ts">
|
||||
import { showIcon } from "@/pages/learningPath/circlePage/learningSequenceUtils";
|
||||
import { useCircleStore } from "@/stores/circle";
|
||||
import type { DefaultArcObject } from "d3";
|
||||
import * as d3 from "d3";
|
||||
import pick from "lodash/pick";
|
||||
|
|
@ -9,26 +8,23 @@ import { computed, onMounted } from "vue";
|
|||
|
||||
// @ts-ignore
|
||||
import colors from "@/colors.json";
|
||||
import type { LearningSequence } from "@/types";
|
||||
|
||||
const circleStore = useCircleStore();
|
||||
import type { CircleType, LearningSequence } from "@/types";
|
||||
import {
|
||||
allFinishedInLearningSequence,
|
||||
someFinishedInLearningSequence,
|
||||
} from "@/services/circle";
|
||||
|
||||
function someFinished(learningSequence: LearningSequence) {
|
||||
if (circleStore.circle) {
|
||||
return circleStore.circle.someFinishedInLearningSequence(
|
||||
learningSequence.translation_key
|
||||
);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
const props = defineProps<{
|
||||
circle: CircleType;
|
||||
}>();
|
||||
|
||||
function allFinished(learningSequence: LearningSequence) {
|
||||
if (circleStore.circle) {
|
||||
return circleStore.circle.allFinishedInLearningSequence(
|
||||
learningSequence.translation_key
|
||||
);
|
||||
}
|
||||
return false;
|
||||
return allFinishedInLearningSequence(learningSequence);
|
||||
}
|
||||
|
||||
function someFinished(learningSequence: LearningSequence) {
|
||||
return someFinishedInLearningSequence(learningSequence);
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
|
|
@ -48,14 +44,14 @@ interface CirclePie extends d3.PieArcDatum<number> {
|
|||
}
|
||||
|
||||
const pieData = computed(() => {
|
||||
const circle = circleStore.circle;
|
||||
const circle = props.circle;
|
||||
|
||||
if (circle) {
|
||||
const pieWeights = new Array(Math.max(circle.learningSequences.length, 1)).fill(1);
|
||||
const pieWeights = new Array(Math.max(circle.learning_sequences.length, 1)).fill(1);
|
||||
const pieGenerator = d3.pie();
|
||||
const angles = pieGenerator(pieWeights);
|
||||
let result = angles.map((angle) => {
|
||||
const thisLearningSequence = circle.learningSequences[angle.index];
|
||||
const thisLearningSequence = circle.learning_sequences[angle.index];
|
||||
|
||||
// Rotate the cirlce by PI (180 degrees) normally 0 = 12'o clock, now start at 6 o clock
|
||||
angle.startAngle += Math.PI;
|
||||
|
|
|
|||
|
|
@ -1,9 +1,9 @@
|
|||
<script setup lang="ts">
|
||||
import ItFullScreenModal from "@/components/ui/ItFullScreenModal.vue";
|
||||
import type { Circle } from "@/services/circle";
|
||||
import type { CircleType } from "@/types";
|
||||
|
||||
defineProps<{
|
||||
circle: Circle | undefined;
|
||||
circle: CircleType;
|
||||
show: boolean;
|
||||
}>();
|
||||
|
||||
|
|
|
|||
|
|
@ -1,16 +1,18 @@
|
|||
<script setup lang="ts">
|
||||
import { useCircleStore } from "@/stores/circle";
|
||||
import type { CourseSessionUser } from "@/types";
|
||||
import { humanizeDuration } from "@/utils/humanizeDuration";
|
||||
import sumBy from "lodash/sumBy";
|
||||
import log from "loglevel";
|
||||
import { computed, onMounted } from "vue";
|
||||
import { computed, watch } from "vue";
|
||||
import { useRoute } from "vue-router";
|
||||
import CircleDiagram from "./CircleDiagram.vue";
|
||||
import CircleOverview from "./CircleOverview.vue";
|
||||
import DocumentSection from "./DocumentSection.vue";
|
||||
import LearningSequence from "./LearningSequence.vue";
|
||||
import { useCourseSessionDetailQuery } from "@/composables";
|
||||
import {
|
||||
useCourseSessionDetailQuery,
|
||||
useCourseDataWithCompletion,
|
||||
} from "@/composables";
|
||||
import { stringifyParse } from "@/utils/utils";
|
||||
import { useCircleStore } from "@/stores/circle";
|
||||
import LearningSequence from "@/pages/learningPath/circlePage/LearningSequence.vue";
|
||||
|
||||
export interface Props {
|
||||
courseSlug: string;
|
||||
|
|
@ -28,84 +30,85 @@ const props = withDefaults(defineProps<Props>(), {
|
|||
profileUser: undefined,
|
||||
});
|
||||
|
||||
log.debug("CirclePage created", props.readonly, props.profileUser);
|
||||
log.debug("CirclePage created", stringifyParse(props));
|
||||
|
||||
const lpQueryResult = useCourseDataWithCompletion(
|
||||
props.courseSlug,
|
||||
props.profileUser?.user_id
|
||||
);
|
||||
|
||||
const circle = computed(() => {
|
||||
return lpQueryResult.findCircle(props.circleSlug);
|
||||
});
|
||||
|
||||
const circleExperts = computed(() => {
|
||||
if (circleStore.circle) {
|
||||
return courseSessionDetailResult.filterCircleExperts(circleStore.circle.slug);
|
||||
if (circle.value) {
|
||||
return courseSessionDetailResult.filterCircleExperts(circle.value.slug);
|
||||
}
|
||||
return [];
|
||||
});
|
||||
|
||||
const duration = computed(() => {
|
||||
if (circleStore.circle) {
|
||||
const minutes = sumBy(circleStore.circle.learningSequences, "minutes");
|
||||
return humanizeDuration(minutes);
|
||||
}
|
||||
|
||||
// if (circleStore.circle) {
|
||||
// const minutes = sumBy(circleStore.circle.learningSequences, "minutes");
|
||||
// return humanizeDuration(minutes);
|
||||
// }
|
||||
return "";
|
||||
});
|
||||
|
||||
const showDuration = computed(() => {
|
||||
return (
|
||||
circleStore.circle && sumBy(circleStore.circle.learningSequences, "minutes") > 0
|
||||
);
|
||||
// return (
|
||||
// circleStore.circle && sumBy(circleStore.circle.learningSequences, "minutes") > 0
|
||||
// );
|
||||
return false;
|
||||
});
|
||||
|
||||
onMounted(async () => {
|
||||
log.debug(
|
||||
"CirclePage mounted",
|
||||
props.courseSlug,
|
||||
props.circleSlug,
|
||||
props.profileUser
|
||||
);
|
||||
watch(
|
||||
() => circle.value,
|
||||
() => {
|
||||
if (circle.value) {
|
||||
log.debug("circle loaded", circle);
|
||||
|
||||
try {
|
||||
if (props.profileUser) {
|
||||
await circleStore.loadCircle(
|
||||
props.courseSlug,
|
||||
props.circleSlug,
|
||||
props.profileUser.user_id
|
||||
);
|
||||
} else {
|
||||
await circleStore.loadCircle(props.courseSlug, props.circleSlug);
|
||||
}
|
||||
try {
|
||||
if (route.hash.startsWith("#ls-") || route.hash.startsWith("#lu-")) {
|
||||
const slugEnd = route.hash.replace("#", "");
|
||||
|
||||
if (route.hash.startsWith("#ls-") || route.hash.startsWith("#lu-")) {
|
||||
const slugEnd = route.hash.replace("#", "");
|
||||
let wagtailPage = null;
|
||||
|
||||
if (slugEnd.startsWith("ls-")) {
|
||||
wagtailPage = circleStore.circle?.learningSequences.find((ls) => {
|
||||
return ls.slug.endsWith(slugEnd);
|
||||
});
|
||||
} else if (slugEnd.startsWith("lu-")) {
|
||||
const learningUnits = circleStore.circle?.learningSequences.flatMap(
|
||||
(ls) => ls.learningUnits
|
||||
);
|
||||
if (learningUnits) {
|
||||
wagtailPage = learningUnits.find((lu) => {
|
||||
return lu.slug.endsWith(slugEnd);
|
||||
});
|
||||
if (circle.value) {
|
||||
let wagtailPage = null;
|
||||
if (slugEnd.startsWith("ls-")) {
|
||||
wagtailPage = circle.value.learning_sequences.find((ls) => {
|
||||
return ls.slug.endsWith(slugEnd);
|
||||
});
|
||||
} else if (slugEnd.startsWith("lu-")) {
|
||||
const learningUnits = circle.value.learning_sequences.flatMap(
|
||||
(ls) => ls.learning_units
|
||||
);
|
||||
if (learningUnits) {
|
||||
wagtailPage = learningUnits.find((lu) => {
|
||||
return lu.slug.endsWith(slugEnd);
|
||||
});
|
||||
}
|
||||
}
|
||||
if (wagtailPage) {
|
||||
document
|
||||
.getElementById(wagtailPage.slug)
|
||||
?.scrollIntoView({ behavior: "smooth" });
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (wagtailPage) {
|
||||
document
|
||||
.getElementById(wagtailPage.slug)
|
||||
?.scrollIntoView({ behavior: "smooth" });
|
||||
} catch (error) {
|
||||
log.error(error);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
log.error(error);
|
||||
}
|
||||
});
|
||||
);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<div v-if="circle">
|
||||
<Teleport to="body">
|
||||
<CircleOverview
|
||||
:circle="circleStore.circle"
|
||||
:circle="circle"
|
||||
:show="circleStore.page === 'OVERVIEW'"
|
||||
@closemodal="circleStore.page = 'INDEX'"
|
||||
/>
|
||||
|
|
@ -145,7 +148,7 @@ onMounted(async () => {
|
|||
</router-link>
|
||||
|
||||
<h1 class="text-blue-dark text-4xl lg:text-6xl" data-cy="circle-title">
|
||||
{{ circleStore.circle?.title }}
|
||||
{{ circle?.title }}
|
||||
</h1>
|
||||
|
||||
<div v-if="showDuration" class="mt-2">
|
||||
|
|
@ -154,7 +157,7 @@ onMounted(async () => {
|
|||
</div>
|
||||
|
||||
<div class="mt-8 w-full">
|
||||
<CircleDiagram></CircleDiagram>
|
||||
<CircleDiagram v-if="circle" :circle="circle"></CircleDiagram>
|
||||
</div>
|
||||
<div v-if="!props.readonly" class="mt-4 border-t-2 lg:hidden">
|
||||
<div
|
||||
|
|
@ -176,7 +179,7 @@ onMounted(async () => {
|
|||
{{ $t("circlePage.circleContentBoxTitle") }}
|
||||
</h3>
|
||||
<div class="mt-4 leading-relaxed">
|
||||
{{ circleStore.circle?.description }}
|
||||
{{ circle?.description }}
|
||||
</div>
|
||||
|
||||
<button
|
||||
|
|
@ -186,13 +189,13 @@ onMounted(async () => {
|
|||
{{ $t("circlePage.learnMore") }}
|
||||
</button>
|
||||
</div>
|
||||
<DocumentSection v-if="!readonly" />
|
||||
<DocumentSection v-if="!readonly" :circle="circle" />
|
||||
<div v-if="!props.readonly" class="expert mt-8 border p-6">
|
||||
<h3 class="text-blue-dark">{{ $t("circlePage.gotQuestions") }}</h3>
|
||||
<div class="mt-4 leading-relaxed">
|
||||
{{
|
||||
$t("circlePage.contactExpertDescription", {
|
||||
circleName: circleStore.circle?.title,
|
||||
circleName: circle?.title,
|
||||
})
|
||||
}}
|
||||
</div>
|
||||
|
|
@ -220,11 +223,12 @@ onMounted(async () => {
|
|||
|
||||
<ol class="flex-auto bg-gray-200 px-4 py-8 lg:px-24">
|
||||
<li
|
||||
v-for="learningSequence in circleStore.circle?.learningSequences ||
|
||||
[]"
|
||||
:key="learningSequence.translation_key"
|
||||
v-for="learningSequence in circle?.learning_sequences ?? []"
|
||||
:key="learningSequence.id"
|
||||
>
|
||||
<LearningSequence
|
||||
:course-slug="props.courseSlug"
|
||||
:circle="circle"
|
||||
:learning-sequence="learningSequence"
|
||||
:readonly="props.readonly"
|
||||
></LearningSequence>
|
||||
|
|
@ -239,15 +243,6 @@ onMounted(async () => {
|
|||
</template>
|
||||
|
||||
<style lang="postcss" scoped>
|
||||
.circle-container {
|
||||
/*background: linear-gradient(to right, white 0%, white 50%, theme(colors.gray.200) 50%, theme(colors.gray.200) 100%);*/
|
||||
}
|
||||
|
||||
.circle {
|
||||
/*max-width: 1440px;*/
|
||||
/*margin: 0 auto;*/
|
||||
}
|
||||
|
||||
.v-enter-active,
|
||||
.v-leave-active {
|
||||
transition: opacity 0.3s ease;
|
||||
|
|
|
|||
|
|
@ -23,12 +23,14 @@
|
|||
import DocumentListItem from "@/components/circle/DocumentListItem.vue";
|
||||
import { useCurrentCourseSession } from "@/composables";
|
||||
import { computed, onMounted, ref } from "vue";
|
||||
import { useCircleStore } from "@/stores/circle";
|
||||
import type { CircleDocument } from "@/types";
|
||||
import type { CircleDocument, CircleType } from "@/types";
|
||||
import { fetchCourseSessionDocuments } from "@/services/files";
|
||||
|
||||
const props = defineProps<{
|
||||
circle: CircleType;
|
||||
}>();
|
||||
|
||||
const courseSession = useCurrentCourseSession();
|
||||
const circleStore = useCircleStore();
|
||||
|
||||
const circleDocumentsResultData = ref<CircleDocument[]>([]);
|
||||
|
||||
|
|
@ -43,7 +45,7 @@ async function fetchDocuments() {
|
|||
|
||||
const circleDocuments = computed(() => {
|
||||
return circleDocumentsResultData.value.filter(
|
||||
(d) => d.learning_sequence.circle.slug === circleStore.circle?.slug
|
||||
(d) => d.learning_sequence.circle.slug === props.circle?.slug
|
||||
);
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -1,26 +1,36 @@
|
|||
<script setup lang="ts">
|
||||
import ItCheckbox from "@/components/ui/ItCheckbox.vue";
|
||||
import LearningContentBadge from "@/pages/learningPath/LearningContentTypeBadge.vue";
|
||||
import { showIcon } from "@/pages/learningPath/circlePage/learningSequenceUtils";
|
||||
import { useCircleStore } from "@/stores/circle";
|
||||
import type {
|
||||
CircleType,
|
||||
CourseCompletionStatus,
|
||||
LearningContent,
|
||||
LearningContentAssignment,
|
||||
LearningContentEdoniqTest,
|
||||
LearningContentInterface,
|
||||
LearningContentWithCompletion,
|
||||
LearningSequence,
|
||||
} from "@/types";
|
||||
import findLast from "lodash/findLast";
|
||||
import type { Ref } from "vue";
|
||||
import { computed } from "vue";
|
||||
import { humanizeDuration } from "../../../utils/humanizeDuration";
|
||||
import {
|
||||
itCheckboxDefaultIconCheckedTailwindClass,
|
||||
itCheckboxDefaultIconUncheckedTailwindClass,
|
||||
} from "@/constants";
|
||||
import ItCheckbox from "@/components/ui/ItCheckbox.vue";
|
||||
import {
|
||||
allFinishedInLearningSequence,
|
||||
calcSelfEvaluationStatus,
|
||||
circleFlatLearningContents,
|
||||
someFinishedInLearningSequence,
|
||||
} from "@/services/circle";
|
||||
import { useCourseDataWithCompletion } from "@/composables";
|
||||
import { findLastIndex } from "lodash";
|
||||
|
||||
type Props = {
|
||||
courseSlug: string;
|
||||
learningSequence: LearningSequence;
|
||||
circle: CircleType;
|
||||
readonly?: boolean;
|
||||
};
|
||||
|
||||
|
|
@ -30,65 +40,64 @@ const props = withDefaults(defineProps<Props>(), {
|
|||
|
||||
const circleStore = useCircleStore();
|
||||
|
||||
function toggleCompleted(learningContent: LearningContentInterface) {
|
||||
const lpQueryResult = useCourseDataWithCompletion(props.courseSlug);
|
||||
|
||||
function toggleCompleted(learningContent: LearningContentWithCompletion) {
|
||||
let completionStatus: CourseCompletionStatus = "SUCCESS";
|
||||
if (learningContent.completion_status === "SUCCESS") {
|
||||
completionStatus = "FAIL";
|
||||
}
|
||||
circleStore.markCompletion(learningContent, completionStatus);
|
||||
lpQueryResult.markCompletion(learningContent, completionStatus);
|
||||
}
|
||||
|
||||
const someFinished = computed(() => {
|
||||
if (props.learningSequence && circleStore.circle) {
|
||||
return circleStore.circle.someFinishedInLearningSequence(
|
||||
props.learningSequence.translation_key
|
||||
);
|
||||
if (props.learningSequence) {
|
||||
return someFinishedInLearningSequence(props.learningSequence);
|
||||
}
|
||||
|
||||
return false;
|
||||
});
|
||||
|
||||
const allFinished = computed(() => {
|
||||
if (props.learningSequence && circleStore.circle) {
|
||||
return circleStore.circle.allFinishedInLearningSequence(
|
||||
props.learningSequence.translation_key
|
||||
);
|
||||
if (props.learningSequence) {
|
||||
return allFinishedInLearningSequence(props.learningSequence);
|
||||
}
|
||||
|
||||
return false;
|
||||
});
|
||||
|
||||
const continueTranslationKeyTuple = computed(() => {
|
||||
if (props.learningSequence && circleStore.circle) {
|
||||
const lastFinished = findLast(
|
||||
circleStore.circle.flatLearningContents,
|
||||
const continueTranslationKeyTuple: Ref<[string | undefined, boolean]> = computed(() => {
|
||||
if (props.learningSequence) {
|
||||
const flatLearningContents = circleFlatLearningContents(props.circle);
|
||||
const lastFinishedIndex = findLastIndex(
|
||||
circleFlatLearningContents(props.circle),
|
||||
(learningContent) => {
|
||||
return learningContent.completion_status === "SUCCESS";
|
||||
}
|
||||
);
|
||||
|
||||
if (!lastFinished) {
|
||||
if (lastFinishedIndex === -1) {
|
||||
// must be the first
|
||||
return [circleStore.circle.flatLearningContents[0].translation_key, true];
|
||||
return [flatLearningContents[0].id, true];
|
||||
}
|
||||
|
||||
if (lastFinished && lastFinished.nextLearningContent) {
|
||||
return [lastFinished.nextLearningContent.translation_key, false];
|
||||
if (flatLearningContents[lastFinishedIndex + 1]) {
|
||||
return [flatLearningContents[lastFinishedIndex + 1].id, false];
|
||||
}
|
||||
}
|
||||
|
||||
return "";
|
||||
return [undefined, false];
|
||||
});
|
||||
|
||||
const learningSequenceBorderClass = computed(() => {
|
||||
let result: string[] = [];
|
||||
if (props.learningSequence && circleStore.circle) {
|
||||
if (props.learningSequence) {
|
||||
if (allFinished.value) {
|
||||
result = ["border-l-4", "border-l-green-500"];
|
||||
} else if (someFinished.value) {
|
||||
result = ["border-l-4", "border-l-sky-500"];
|
||||
} else {
|
||||
result = ["border-l-gray-500"];
|
||||
result = ["border-l", "border-l-gray-500"];
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -123,22 +132,27 @@ function checkboxIconUncheckedTailwindClass(lc: LearningContent) {
|
|||
</script>
|
||||
|
||||
<template>
|
||||
<div :id="learningSequence.slug" class="learning-sequence mb-8">
|
||||
<div
|
||||
:id="learningSequence.slug"
|
||||
class="learning-sequence mb-8"
|
||||
data-cy="lp-learning-sequence"
|
||||
>
|
||||
<div class="mb-2 flex items-center gap-4 text-blue-900">
|
||||
<component :is="learningSequence.icon" v-if="showIcon(learningSequence.icon)" />
|
||||
<h3 class="text-large font-semibold">
|
||||
{{ learningSequence.title }}
|
||||
</h3>
|
||||
<div v-if="learningSequence.minutes > 0">
|
||||
{{ humanizeDuration(learningSequence.minutes) }}
|
||||
</div>
|
||||
<!-- <div v-if="learningSequence.minutes > 0">-->
|
||||
<!-- {{ humanizeDuration(learningSequence.minutes) }}-->
|
||||
<!-- </div>-->
|
||||
</div>
|
||||
|
||||
<ol class="border bg-white px-4 lg:px-6" :class="learningSequenceBorderClass">
|
||||
<li
|
||||
v-for="learningUnit in learningSequence.learningUnits"
|
||||
v-for="learningUnit in learningSequence.learning_units"
|
||||
:id="learningUnit.slug"
|
||||
:key="learningUnit.id"
|
||||
data-cy="lp-learning-unit"
|
||||
class="pt-3 lg:pt-6"
|
||||
>
|
||||
<div
|
||||
|
|
@ -148,14 +162,15 @@ function checkboxIconUncheckedTailwindClass(lc: LearningContent) {
|
|||
<div class="font-semibold">
|
||||
{{ learningUnit.title }}
|
||||
</div>
|
||||
<div v-if="learningUnit.minutes > 0" class="whitespace-nowrap">
|
||||
{{ humanizeDuration(learningUnit.minutes) }}
|
||||
</div>
|
||||
<!-- <div v-if="learningUnit.minutes > 0" class="whitespace-nowrap">-->
|
||||
<!-- {{ humanizeDuration(learningUnit.minutes) }}-->
|
||||
<!-- </div>-->
|
||||
</div>
|
||||
<ol>
|
||||
<li
|
||||
v-for="learningContent in learningUnit.learningContents"
|
||||
v-for="learningContent in learningUnit.learning_contents"
|
||||
:key="learningContent.id"
|
||||
data-cy="lp-learning-content"
|
||||
>
|
||||
<div class="pb-6">
|
||||
<div class="flex items-center gap-4">
|
||||
|
|
@ -181,15 +196,15 @@ function checkboxIconUncheckedTailwindClass(lc: LearningContent) {
|
|||
"
|
||||
@toggle="toggleCompleted(learningContent)"
|
||||
@click="
|
||||
(event: MouseEvent) => {
|
||||
// when disabled open the learning content directly
|
||||
if (!learningContent.can_user_self_toggle_course_completion) {
|
||||
circleStore.openLearningContent(learningContent);
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
}
|
||||
}
|
||||
"
|
||||
(event: MouseEvent) => {
|
||||
// when disabled open the learning content directly
|
||||
if (!learningContent.can_user_self_toggle_course_completion) {
|
||||
circleStore.openLearningContent(learningContent);
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
}
|
||||
}
|
||||
"
|
||||
/>
|
||||
<div
|
||||
class="flex flex-auto flex-col gap-4 xl:flex-row xl:justify-between"
|
||||
|
|
@ -236,7 +251,7 @@ function checkboxIconUncheckedTailwindClass(lc: LearningContent) {
|
|||
|
||||
<div
|
||||
v-if="
|
||||
learningContent.translation_key === continueTranslationKeyTuple[0] &&
|
||||
learningContent.id === continueTranslationKeyTuple[0] &&
|
||||
!props.readonly
|
||||
"
|
||||
class="my-4"
|
||||
|
|
@ -259,20 +274,20 @@ function checkboxIconUncheckedTailwindClass(lc: LearningContent) {
|
|||
</ol>
|
||||
|
||||
<div
|
||||
v-if="learningUnit.children.length"
|
||||
v-if="learningUnit.performance_criteria.length"
|
||||
:class="{ 'cursor-pointer': !props.readonly }"
|
||||
:data-cy="`${learningUnit.slug}`"
|
||||
@click="!props.readonly && circleStore.openSelfEvaluation(learningUnit)"
|
||||
>
|
||||
<div
|
||||
v-if="circleStore.calcSelfEvaluationStatus(learningUnit) === 'SUCCESS'"
|
||||
v-if="calcSelfEvaluationStatus(learningUnit) === 'SUCCESS'"
|
||||
class="self-evaluation-success flex items-center gap-4 pb-6"
|
||||
>
|
||||
<it-icon-smiley-happy class="mr-4 h-8 w-8 flex-none" data-cy="success" />
|
||||
<div>{{ $t("selfEvaluation.selfEvaluationYes") }}</div>
|
||||
</div>
|
||||
<div
|
||||
v-else-if="circleStore.calcSelfEvaluationStatus(learningUnit) === 'FAIL'"
|
||||
v-else-if="calcSelfEvaluationStatus(learningUnit) === 'FAIL'"
|
||||
class="self-evaluation-fail flex items-center gap-4 pb-6"
|
||||
>
|
||||
<it-icon-smiley-thinking class="mr-4 h-8 w-8 flex-none" data-cy="fail" />
|
||||
|
|
@ -284,7 +299,7 @@ function checkboxIconUncheckedTailwindClass(lc: LearningContent) {
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<hr v-if="!learningUnit.last" class="-mx-4 text-gray-500" />
|
||||
<!-- <hr v-if="!learningUnit.last" class="-mx-4 text-gray-500" />-->
|
||||
</li>
|
||||
</ol>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,12 +1,9 @@
|
|||
<script setup lang="ts">
|
||||
import LearningContentParent from "@/pages/learningPath/learningContentPage/LearningContentParent.vue";
|
||||
import { useCircleStore } from "@/stores/circle";
|
||||
import type { LearningContent } from "@/types";
|
||||
import * as log from "loglevel";
|
||||
import type { Ref } from "vue";
|
||||
import { getCurrentInstance, onMounted, onUpdated, ref, watch } from "vue";
|
||||
|
||||
log.debug("LearningContentView created");
|
||||
import { computed, getCurrentInstance, onUpdated } from "vue";
|
||||
import { useCourseDataWithCompletion } from "@/composables";
|
||||
import { stringifyParse } from "@/utils/utils";
|
||||
|
||||
const props = defineProps<{
|
||||
courseSlug: string;
|
||||
|
|
@ -14,43 +11,14 @@ const props = defineProps<{
|
|||
contentSlug: string;
|
||||
}>();
|
||||
|
||||
const learningContent: Ref<LearningContent | undefined> = ref(undefined);
|
||||
log.debug("LearningContentView created", stringifyParse(props));
|
||||
|
||||
const circleStore = useCircleStore();
|
||||
|
||||
const loadLearningContent = async () => {
|
||||
try {
|
||||
learningContent.value = await circleStore.loadLearningContent(
|
||||
props.courseSlug,
|
||||
props.circleSlug,
|
||||
props.contentSlug
|
||||
);
|
||||
} catch (error) {
|
||||
log.error(error);
|
||||
}
|
||||
};
|
||||
|
||||
watch(
|
||||
() => props.contentSlug,
|
||||
async () => {
|
||||
log.debug(
|
||||
"LearningContentView props.contentSlug changed",
|
||||
props.courseSlug,
|
||||
props.circleSlug,
|
||||
props.contentSlug
|
||||
);
|
||||
await loadLearningContent();
|
||||
}
|
||||
const courseData = useCourseDataWithCompletion(props.courseSlug);
|
||||
const learningContent = computed(() =>
|
||||
courseData.findLearningContent(props.contentSlug, props.circleSlug)
|
||||
);
|
||||
|
||||
onMounted(async () => {
|
||||
log.debug(
|
||||
"LearningContentView mounted",
|
||||
props.courseSlug,
|
||||
props.circleSlug,
|
||||
props.contentSlug
|
||||
);
|
||||
await loadLearningContent();
|
||||
const circle = computed(() => {
|
||||
return courseData.findCircle(props.circleSlug);
|
||||
});
|
||||
|
||||
onUpdated(() => {
|
||||
|
|
@ -73,7 +41,11 @@ onUpdated(() => {
|
|||
</script>
|
||||
|
||||
<template>
|
||||
<LearningContentParent v-if="learningContent" :learning-content="learningContent" />
|
||||
<LearningContentParent
|
||||
v-if="learningContent && circle"
|
||||
:learning-content="learningContent"
|
||||
:circle="circle"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<style lang="postcss" scoped></style>
|
||||
|
|
|
|||
|
|
@ -2,8 +2,12 @@
|
|||
import LearningContentContainer from "@/pages/learningPath/learningContentPage/LearningContentContainer.vue";
|
||||
import DocumentListBlock from "@/pages/learningPath/learningContentPage/blocks/DocumentListBlock.vue";
|
||||
import EdoniqTestBlock from "@/pages/learningPath/learningContentPage/blocks/EdoniqTestBlock.vue";
|
||||
import { useCircleStore } from "@/stores/circle";
|
||||
import type { LearningContent, LearningContentType } from "@/types";
|
||||
import type {
|
||||
CircleType,
|
||||
LearningContent,
|
||||
LearningContentContentType,
|
||||
LearningContentWithCompletion,
|
||||
} from "@/types";
|
||||
import eventBus from "@/utils/eventBus";
|
||||
import log from "loglevel";
|
||||
import type { Component } from "vue";
|
||||
|
|
@ -17,19 +21,24 @@ import PlaceholderBlock from "./blocks/PlaceholderBlock.vue";
|
|||
import RichTextBlock from "./blocks/RichTextBlock.vue";
|
||||
import VideoBlock from "./blocks/VideoBlock.vue";
|
||||
import { getPreviousRoute } from "@/router/history";
|
||||
|
||||
const circleStore = useCircleStore();
|
||||
import { stringifyParse } from "@/utils/utils";
|
||||
import { useCourseDataWithCompletion } from "@/composables";
|
||||
import { useCircleStore } from "@/stores/circle";
|
||||
|
||||
const props = defineProps<{
|
||||
learningContent: LearningContent;
|
||||
circle: CircleType;
|
||||
}>();
|
||||
|
||||
log.debug("LearningContentParent setup", props.learningContent);
|
||||
log.debug("LearningContentParent setup", stringifyParse(props));
|
||||
|
||||
const courseCompletionData = useCourseDataWithCompletion();
|
||||
const circleStore = useCircleStore();
|
||||
|
||||
const previousRoute = getPreviousRoute();
|
||||
|
||||
// can't use the type as component name, as some are reserved HTML components, e.g. video
|
||||
const COMPONENTS: Record<LearningContentType, Component> = {
|
||||
const COMPONENTS: Record<LearningContentContentType, Component> = {
|
||||
"learnpath.LearningContentAssignment": AssignmentBlock,
|
||||
"learnpath.LearningContentAttendanceCourse": AttendanceCourseBlock,
|
||||
"learnpath.LearningContentDocumentList": DocumentListBlock,
|
||||
|
|
@ -48,7 +57,14 @@ const component = computed(() => {
|
|||
});
|
||||
|
||||
function handleFinishedLearningContent() {
|
||||
circleStore.continueFromLearningContent(props.learningContent, previousRoute);
|
||||
circleStore.continueFromLearningContent(
|
||||
props.learningContent,
|
||||
props.circle,
|
||||
previousRoute,
|
||||
(lc: LearningContentWithCompletion) => {
|
||||
courseCompletionData.markCompletion(lc, "SUCCESS");
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
eventBus.on("finishedLearningContent", handleFinishedLearningContent);
|
||||
|
|
@ -60,7 +76,9 @@ onUnmounted(() => {
|
|||
|
||||
<template>
|
||||
<LearningContentContainer
|
||||
@exit="circleStore.closeLearningContent(props.learningContent, previousRoute)"
|
||||
@exit="
|
||||
circleStore.closeLearningContent(props.learningContent, circle, previousRoute)
|
||||
"
|
||||
>
|
||||
<div>
|
||||
<component :is="component" :content="learningContent"></component>
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ import dayjs from "dayjs";
|
|||
|
||||
interface Props {
|
||||
assignment: Assignment;
|
||||
submissionDeadlineStart?: string;
|
||||
submissionDeadlineStart?: string | null;
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
|
|
|
|||
|
|
@ -3,7 +3,11 @@ import DateEmbedding from "@/components/dueDates/DateEmbedding.vue";
|
|||
import ItButton from "@/components/ui/ItButton.vue";
|
||||
import ItCheckbox from "@/components/ui/ItCheckbox.vue";
|
||||
import ItSuccessAlert from "@/components/ui/ItSuccessAlert.vue";
|
||||
import { useCourseSessionDetailQuery, useCurrentCourseSession } from "@/composables";
|
||||
import {
|
||||
useCourseSessionDetailQuery,
|
||||
useCurrentCourseSession,
|
||||
useCourseData,
|
||||
} from "@/composables";
|
||||
import { bustItGetCache } from "@/fetchHelpers";
|
||||
import { UPSERT_ASSIGNMENT_COMPLETION_MUTATION } from "@/graphql/mutations";
|
||||
import AssignmentSubmissionResponses from "@/pages/learningPath/learningContentPage/assignment/AssignmentSubmissionResponses.vue";
|
||||
|
|
@ -15,14 +19,13 @@ import { computed, reactive } from "vue";
|
|||
import { useTranslation } from "i18next-vue";
|
||||
import eventBus from "@/utils/eventBus";
|
||||
import dayjs from "dayjs";
|
||||
import { useCircleStore } from "@/stores/circle";
|
||||
|
||||
const props = defineProps<{
|
||||
assignment: Assignment;
|
||||
learningContentId: string;
|
||||
assignmentCompletion?: AssignmentCompletion;
|
||||
courseSessionId: string;
|
||||
submissionDeadlineStart?: string;
|
||||
submissionDeadlineStart?: string | null;
|
||||
}>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
|
|
@ -31,7 +34,7 @@ const emit = defineEmits<{
|
|||
|
||||
const courseSession = useCurrentCourseSession();
|
||||
const courseSessionDetailResult = useCourseSessionDetailQuery();
|
||||
const circleStore = useCircleStore();
|
||||
const courseData = useCourseData(courseSession.value.course.slug);
|
||||
|
||||
const { t } = useTranslation();
|
||||
|
||||
|
|
@ -40,9 +43,15 @@ const state = reactive({
|
|||
confirmPerson: false,
|
||||
});
|
||||
|
||||
const learningContent = computed(() => {
|
||||
return courseData.findLearningContent(props.learningContentId);
|
||||
});
|
||||
|
||||
const circleExperts = computed(() => {
|
||||
if (circleStore.circle) {
|
||||
return courseSessionDetailResult.filterCircleExperts(circleStore.circle.slug);
|
||||
if (learningContent.value?.circle) {
|
||||
return courseSessionDetailResult.filterCircleExperts(
|
||||
learningContent.value?.circle.slug
|
||||
);
|
||||
}
|
||||
return [];
|
||||
});
|
||||
|
|
@ -133,7 +142,7 @@ const onSubmit = async () => {
|
|||
data-cy="confirm-submit-person"
|
||||
@toggle="state.confirmPerson = !state.confirmPerson"
|
||||
></ItCheckbox>
|
||||
<div class="flex flex-row items-center pb-6 pl-[49px]">
|
||||
<div v-if="circleExpert" class="flex flex-row items-center pb-6 pl-[49px]">
|
||||
<img
|
||||
alt="Notification icon"
|
||||
class="mr-2 h-[45px] min-w-[45px] rounded-full"
|
||||
|
|
|
|||
|
|
@ -34,7 +34,7 @@ const queryResult = useQuery({
|
|||
query: ASSIGNMENT_COMPLETION_QUERY,
|
||||
variables: {
|
||||
courseSessionId: courseSession.value.id,
|
||||
assignmentId: props.learningContent.content_assignment_id,
|
||||
assignmentId: props.learningContent.content_assignment.id,
|
||||
learningContentId: props.learningContent.id,
|
||||
},
|
||||
pause: true,
|
||||
|
|
@ -95,7 +95,7 @@ watchEffect(() => {
|
|||
onMounted(async () => {
|
||||
log.debug(
|
||||
"AssignmentView mounted",
|
||||
props.learningContent.content_assignment_id,
|
||||
props.learningContent.content_assignment.id,
|
||||
props.learningContent
|
||||
);
|
||||
|
||||
|
|
@ -122,7 +122,7 @@ const currentTask = computed(() => {
|
|||
const initUpsertAssignmentCompletion = async () => {
|
||||
try {
|
||||
await upsertAssignmentCompletionMutation.executeMutation({
|
||||
assignmentId: props.learningContent.content_assignment_id,
|
||||
assignmentId: props.learningContent.content_assignment.id,
|
||||
courseSessionId: courseSession.value.id,
|
||||
learningContentId: props.learningContent.id,
|
||||
completionDataString: JSON.stringify({}),
|
||||
|
|
@ -234,7 +234,7 @@ const assignmentUser = computed(() => {
|
|||
<AssignmentTaskView
|
||||
v-else-if="currentTask"
|
||||
:task="currentTask"
|
||||
:assignment-id="props.learningContent.content_assignment_id"
|
||||
:assignment-id="props.learningContent.content_assignment.id"
|
||||
:assignment-completion="assignmentCompletion"
|
||||
:learning-content-id="props.learningContent.id"
|
||||
></AssignmentTaskView>
|
||||
|
|
|
|||
|
|
@ -5,8 +5,8 @@
|
|||
<p class="grid-in-value">
|
||||
{{
|
||||
formatDueDate(
|
||||
props.attendanceCourse.due_date.start,
|
||||
props.attendanceCourse.due_date.end
|
||||
props.attendanceCourse.due_date?.start ?? "",
|
||||
props.attendanceCourse.due_date?.end
|
||||
)
|
||||
}}
|
||||
</p>
|
||||
|
|
@ -28,11 +28,11 @@
|
|||
|
||||
<script setup lang="ts">
|
||||
import { formatDueDate } from "@/components/dueDates/dueDatesUtils";
|
||||
import type { CourseSessionAttendanceCourse } from "@/types";
|
||||
import { computed } from "vue";
|
||||
import type { CourseSessionAttendanceCourseObjectType } from "@/gql/graphql";
|
||||
|
||||
export interface Props {
|
||||
attendanceCourse: CourseSessionAttendanceCourse;
|
||||
attendanceCourse: CourseSessionAttendanceCourseObjectType;
|
||||
}
|
||||
|
||||
const props = defineProps<Props>();
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ const props = defineProps<{
|
|||
|
||||
<template>
|
||||
<AssignmentView
|
||||
:assignment-id="props.content.content_assignment_id"
|
||||
:assignment-id="props.content.content_assignment.id"
|
||||
:learning-content="props.content"
|
||||
/>
|
||||
</template>
|
||||
|
|
|
|||
|
|
@ -1,11 +1,30 @@
|
|||
<script setup lang="ts">
|
||||
import MediaLink from "@/components/mediaLibrary/MediaLink.vue";
|
||||
import type { LearningContentDocumentList } from "@/types";
|
||||
import type {
|
||||
LearningContentDocumentList,
|
||||
MediaLibraryContentBlockValue,
|
||||
} from "@/types";
|
||||
import LearningContentSimpleLayout from "../layouts/LearningContentSimpleLayout.vue";
|
||||
import { onMounted, ref } from "vue";
|
||||
import log from "loglevel";
|
||||
import { itGetCached } from "@/fetchHelpers";
|
||||
|
||||
const props = defineProps<{
|
||||
content: LearningContentDocumentList;
|
||||
}>();
|
||||
|
||||
type BlockDocument = {
|
||||
id: string;
|
||||
value: MediaLibraryContentBlockValue;
|
||||
};
|
||||
|
||||
const documents = ref<BlockDocument[]>([]);
|
||||
|
||||
onMounted(async () => {
|
||||
log.debug("DocumentListBlock mounted");
|
||||
const response = await itGetCached(`/api/course/page/${props.content.slug}/`);
|
||||
documents.value = response.documents;
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
|
@ -19,7 +38,7 @@ const props = defineProps<{
|
|||
<div>
|
||||
<ul class="border-t">
|
||||
<li
|
||||
v-for="item in content.documents"
|
||||
v-for="item in documents"
|
||||
:key="item.id"
|
||||
class="flex items-center justify-between border-b py-4"
|
||||
>
|
||||
|
|
|
|||
|
|
@ -11,10 +11,7 @@ import { ASSIGNMENT_COMPLETION_QUERY } from "@/graphql/queries";
|
|||
import { useCourseSessionDetailQuery, useCurrentCourseSession } from "@/composables";
|
||||
import dayjs from "dayjs";
|
||||
import ItSuccessAlert from "@/components/ui/ItSuccessAlert.vue";
|
||||
import {
|
||||
formatDueDate,
|
||||
getDateString,
|
||||
} from "../../../../components/dueDates/dueDatesUtils";
|
||||
import { formatDueDate, getDateString } from "@/components/dueDates/dueDatesUtils";
|
||||
|
||||
const { t } = useTranslation();
|
||||
|
||||
|
|
@ -33,7 +30,7 @@ const queryResult = useQuery({
|
|||
query: ASSIGNMENT_COMPLETION_QUERY,
|
||||
variables: {
|
||||
courseSessionId: courseSession.value.id,
|
||||
assignmentId: props.content.content_assignment_id,
|
||||
assignmentId: props.content.content_assignment.id,
|
||||
learningContentId: props.content.id,
|
||||
},
|
||||
});
|
||||
|
|
@ -52,9 +49,12 @@ const extendedTimeTest = ref(false);
|
|||
|
||||
const deadlineInPast = computed(() => {
|
||||
// with 16 minutes buffer
|
||||
return dayjs(courseSessionEdoniqTest.value?.deadline.start)
|
||||
.add(16, "minute")
|
||||
.isBefore(dayjs());
|
||||
if (courseSessionEdoniqTest.value?.deadline?.start) {
|
||||
return dayjs(courseSessionEdoniqTest.value?.deadline.start)
|
||||
.add(16, "minute")
|
||||
.isBefore(dayjs());
|
||||
}
|
||||
return false;
|
||||
});
|
||||
|
||||
async function startTest() {
|
||||
|
|
@ -89,7 +89,7 @@ async function startTest() {
|
|||
<p class="mt-2 text-lg">
|
||||
{{
|
||||
$t("edoniqTest.submitDateDescription", {
|
||||
x: formatDueDate(courseSessionEdoniqTest.deadline.start),
|
||||
x: formatDueDate(courseSessionEdoniqTest?.deadline?.start ?? ""),
|
||||
})
|
||||
}}
|
||||
</p>
|
||||
|
|
@ -157,7 +157,7 @@ async function startTest() {
|
|||
</div>
|
||||
<div v-else>
|
||||
{{ $t("a.Abgabetermin") }}:
|
||||
{{ getDateString(dayjs(courseSessionEdoniqTest?.deadline.start)) }}
|
||||
{{ getDateString(dayjs(courseSessionEdoniqTest?.deadline?.start)) }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -9,7 +9,6 @@ import {
|
|||
YES_NO,
|
||||
} from "@/pages/learningPath/learningContentPage/feedback/feedback.constants";
|
||||
import LearningContentMultiLayout from "@/pages/learningPath/learningContentPage/layouts/LearningContentMultiLayout.vue";
|
||||
import { useCircleStore } from "@/stores/circle";
|
||||
import type { LearningContentFeedback } from "@/types";
|
||||
import { useMutation } from "@urql/vue";
|
||||
import { useRouteQuery } from "@vueuse/router";
|
||||
|
|
@ -22,7 +21,6 @@ const props = defineProps<{
|
|||
content: LearningContentFeedback;
|
||||
}>();
|
||||
const courseSession = useCurrentCourseSession();
|
||||
const circleStore = useCircleStore();
|
||||
const courseSessionDetailResult = useCourseSessionDetailQuery();
|
||||
|
||||
const { t } = useTranslation();
|
||||
|
|
@ -30,12 +28,12 @@ const { t } = useTranslation();
|
|||
const stepNo = useRouteQuery("step", "0", { transform: Number, mode: "push" });
|
||||
|
||||
const title = computed(
|
||||
() => `«${circleStore.circle?.title}»: ${t("feedback.areYouSatisfied")}`
|
||||
() => `«${props.content.circle?.title}»: ${t("feedback.areYouSatisfied")}`
|
||||
);
|
||||
|
||||
const circleExperts = computed(() => {
|
||||
if (circleStore.circle) {
|
||||
return courseSessionDetailResult.filterCircleExperts(circleStore.circle.slug);
|
||||
if (props.content?.circle?.slug) {
|
||||
return courseSessionDetailResult.filterCircleExperts(props.content.circle.slug);
|
||||
}
|
||||
return [];
|
||||
});
|
||||
|
|
|
|||
|
|
@ -2,41 +2,51 @@
|
|||
import LearningPathCircle from "@/pages/learningPath/learningPathPage/LearningPathCircle.vue";
|
||||
import LearningPathContinueButton from "@/pages/learningPath/learningPathPage/LearningPathContinueButton.vue";
|
||||
import { calculateCircleSectorData } from "@/pages/learningPath/learningPathPage/utils";
|
||||
import type { Circle } from "@/services/circle";
|
||||
import type { LearningPath } from "@/services/learningPath";
|
||||
import type { Topic } from "@/types";
|
||||
import { onMounted, ref } from "vue";
|
||||
import type { CircleType, LearningContentWithCompletion } from "@/types";
|
||||
import { computed, ref, watch } from "vue";
|
||||
|
||||
const props = defineProps<{
|
||||
learningPath: LearningPath | undefined;
|
||||
circle: Circle;
|
||||
topic: Topic;
|
||||
circle: CircleType;
|
||||
nextLearningContent: LearningContentWithCompletion | undefined;
|
||||
isFirstCircle: boolean;
|
||||
isLastCircle: boolean;
|
||||
isCurrentCircle: boolean;
|
||||
overrideCircleUrl?: string;
|
||||
}>();
|
||||
|
||||
const circleElement = ref<HTMLElement | null>(null);
|
||||
|
||||
onMounted(() => {
|
||||
if (props.isCurrentCircle) {
|
||||
const isCurrentCircle = computed(() => {
|
||||
return props.nextLearningContent?.circle?.id === props.circle.id;
|
||||
});
|
||||
|
||||
function scrollToCircle() {
|
||||
if (isCurrentCircle.value) {
|
||||
setTimeout(() => {
|
||||
circleElement?.value?.scrollIntoView({
|
||||
behavior: "smooth",
|
||||
inline: "center",
|
||||
block: "nearest",
|
||||
});
|
||||
}, 400);
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
watch(
|
||||
() => isCurrentCircle.value,
|
||||
(isCurrent) => {
|
||||
if (isCurrent) {
|
||||
scrollToCircle();
|
||||
}
|
||||
},
|
||||
{ immediate: true }
|
||||
);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<router-link
|
||||
:to="overrideCircleUrl ? overrideCircleUrl : props.circle.frontend_url"
|
||||
:data-cy="`circle-${props.circle.title}`"
|
||||
class="flex flex-col items-center pb-6"
|
||||
class="cy-lp-circle flex flex-col items-center pb-6"
|
||||
>
|
||||
<div ref="circleElement" class="flex flex-row items-center pb-2">
|
||||
<div class="w-12">
|
||||
|
|
@ -55,10 +65,9 @@ onMounted(() => {
|
|||
{{ props.circle.title }}
|
||||
</div>
|
||||
|
||||
<div v-if="props.isCurrentCircle" class="whitespace-nowrap">
|
||||
<div v-if="isCurrentCircle" class="whitespace-nowrap">
|
||||
<LearningPathContinueButton
|
||||
:has-progress="!props.learningPath?.continueData?.has_no_progress"
|
||||
:url="props.learningPath?.continueData?.url"
|
||||
:next-learning-content="props.nextLearningContent"
|
||||
></LearningPathContinueButton>
|
||||
</div>
|
||||
</router-link>
|
||||
|
|
|
|||
|
|
@ -2,29 +2,35 @@
|
|||
import LearningPathCircle from "@/pages/learningPath/learningPathPage/LearningPathCircle.vue";
|
||||
import LearningPathContinueButton from "@/pages/learningPath/learningPathPage/LearningPathContinueButton.vue";
|
||||
import { calculateCircleSectorData } from "@/pages/learningPath/learningPathPage/utils";
|
||||
import type { Circle } from "@/services/circle";
|
||||
import type { LearningPath } from "@/services/learningPath";
|
||||
import { onMounted, ref } from "vue";
|
||||
import { computed, ref, watch } from "vue";
|
||||
import type { CircleType, LearningContentWithCompletion } from "@/types";
|
||||
|
||||
const props = defineProps<{
|
||||
learningPath: LearningPath | undefined;
|
||||
circle: Circle;
|
||||
isCurrentCircle: boolean;
|
||||
circle: CircleType;
|
||||
nextLearningContent: LearningContentWithCompletion | undefined;
|
||||
}>();
|
||||
|
||||
const circleElement = ref<HTMLElement | null>(null);
|
||||
|
||||
onMounted(() => {
|
||||
if (props.isCurrentCircle) {
|
||||
setTimeout(() => {
|
||||
circleElement?.value?.scrollIntoView({
|
||||
behavior: "smooth",
|
||||
inline: "nearest",
|
||||
block: "center",
|
||||
});
|
||||
}, 400);
|
||||
}
|
||||
const isCurrentCircle = computed(() => {
|
||||
return props.nextLearningContent?.circle?.id === props.circle.id;
|
||||
});
|
||||
|
||||
watch(
|
||||
() => isCurrentCircle.value,
|
||||
(isCurrent) => {
|
||||
if (isCurrent) {
|
||||
setTimeout(() => {
|
||||
circleElement?.value?.scrollIntoView({
|
||||
behavior: "smooth",
|
||||
inline: "center",
|
||||
block: "nearest",
|
||||
});
|
||||
});
|
||||
}
|
||||
},
|
||||
{ immediate: true }
|
||||
);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
|
@ -43,8 +49,7 @@ onMounted(() => {
|
|||
|
||||
<div v-if="isCurrentCircle" class="whitespace-nowrap pl-4">
|
||||
<LearningPathContinueButton
|
||||
:has-progress="!props.learningPath?.continueData?.has_no_progress"
|
||||
:url="props.learningPath?.continueData?.url"
|
||||
:next-learning-content="props.nextLearningContent"
|
||||
></LearningPathContinueButton>
|
||||
</div>
|
||||
</router-link>
|
||||
|
|
|
|||
|
|
@ -1,24 +1,24 @@
|
|||
<script setup lang="ts">
|
||||
interface Props {
|
||||
hasProgress?: boolean;
|
||||
url?: string;
|
||||
}
|
||||
import type { LearningContentWithCompletion } from "@/types";
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
hasProgress: false,
|
||||
url: "",
|
||||
});
|
||||
const props = defineProps<{
|
||||
nextLearningContent: LearningContentWithCompletion | undefined;
|
||||
}>();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<router-link
|
||||
class="btn-blue mt-2 pl-6"
|
||||
:to="props.url"
|
||||
:to="nextLearningContent?.continueUrl ?? '/'"
|
||||
data-cy="lp-continue-button"
|
||||
translate
|
||||
>
|
||||
<span>
|
||||
{{ props.hasProgress ? $t("general.nextStep") : $t("general.start") }}
|
||||
{{
|
||||
props.nextLearningContent?.firstInCircle
|
||||
? $t("general.start")
|
||||
: $t("general.nextStep")
|
||||
}}
|
||||
</span>
|
||||
</router-link>
|
||||
</template>
|
||||
|
|
|
|||
|
|
@ -1,17 +1,14 @@
|
|||
<script setup lang="ts">
|
||||
import LearningPathCircleListTile from "@/pages/learningPath/learningPathPage/LearningPathCircleListTile.vue";
|
||||
import type { Circle } from "@/services/circle";
|
||||
import type { LearningPath } from "@/services/learningPath";
|
||||
import { computed } from "vue";
|
||||
import type { LearningContentWithCompletion, LearningPathType } from "@/types";
|
||||
|
||||
const props = defineProps<{
|
||||
learningPath: LearningPath | undefined;
|
||||
learningPath: LearningPathType | undefined;
|
||||
nextLearningContent: LearningContentWithCompletion | undefined;
|
||||
}>();
|
||||
|
||||
const topics = computed(() => props.learningPath?.topics ?? []);
|
||||
|
||||
const isCurrentCircle = (circle: Circle) =>
|
||||
props.learningPath?.nextLearningContent?.parentCircle === circle;
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
|
@ -22,9 +19,8 @@ const isCurrentCircle = (circle: Circle) =>
|
|||
<LearningPathCircleListTile
|
||||
v-for="circle in topic.circles"
|
||||
:key="circle.id"
|
||||
:learning-path="learningPath"
|
||||
:circle="circle"
|
||||
:is-current-circle="isCurrentCircle(circle)"
|
||||
:next-learning-content="props.nextLearningContent"
|
||||
></LearningPathCircleListTile>
|
||||
</div>
|
||||
</template>
|
||||
|
|
|
|||
|
|
@ -6,19 +6,16 @@ import CircleProgress from "@/pages/learningPath/learningPathPage/LearningPathPr
|
|||
import LearningPathTopics from "@/pages/learningPath/learningPathPage/LearningPathTopics.vue";
|
||||
import type { ViewType } from "@/pages/learningPath/learningPathPage/LearningPathViewSwitch.vue";
|
||||
import LearningPathViewSwitch from "@/pages/learningPath/learningPathPage/LearningPathViewSwitch.vue";
|
||||
import { useLearningPathStore } from "@/stores/learningPath";
|
||||
import { useUserStore } from "@/stores/user";
|
||||
import { breakpointsTailwind, useBreakpoints } from "@vueuse/core";
|
||||
import * as log from "loglevel";
|
||||
import { computed, onMounted, ref } from "vue";
|
||||
import { computed, ref } from "vue";
|
||||
import { useCourseDataWithCompletion } from "@/composables";
|
||||
import { someFinishedInLearningSequence } from "@/services/circle";
|
||||
|
||||
const props = defineProps<{
|
||||
courseSlug: string;
|
||||
}>();
|
||||
|
||||
const breakpoints = useBreakpoints(breakpointsTailwind);
|
||||
const learningPathStore = useLearningPathStore();
|
||||
const userStore = useUserStore();
|
||||
|
||||
// Layout state
|
||||
const useMobileLayout = breakpoints.smaller("sm");
|
||||
|
|
@ -26,38 +23,21 @@ const selectedView = ref<ViewType>(
|
|||
(window.localStorage.getItem("learningPathView") as ViewType) || "path"
|
||||
);
|
||||
|
||||
onMounted(async () => {
|
||||
log.debug("LearningPathPage mounted");
|
||||
const lpQueryResult = useCourseDataWithCompletion(props.courseSlug);
|
||||
|
||||
try {
|
||||
await learningPathStore.loadLearningPath(props.courseSlug + "-lp");
|
||||
} catch (error) {
|
||||
log.error(error);
|
||||
}
|
||||
});
|
||||
|
||||
const learningPath = computed(() => {
|
||||
if (userStore.loggedIn && learningPathStore.state.learningPaths.size > 0) {
|
||||
const learningPathKey = `${props.courseSlug}-lp-${userStore.id}`;
|
||||
return learningPathStore.state.learningPaths.get(learningPathKey);
|
||||
}
|
||||
return undefined;
|
||||
});
|
||||
const learningPath = computed(() => lpQueryResult.learningPath.value);
|
||||
const course = computed(() => lpQueryResult.course.value);
|
||||
|
||||
const circlesCount = computed(() => {
|
||||
if (learningPath.value) {
|
||||
return learningPath.value.circles.length;
|
||||
}
|
||||
return 0;
|
||||
return lpQueryResult.circles.value?.length ?? 0;
|
||||
});
|
||||
|
||||
const inProgressCirclesCount = computed(() => {
|
||||
if (learningPath.value) {
|
||||
return learningPath.value.circles.filter(
|
||||
if (lpQueryResult.circles.value?.length) {
|
||||
return lpQueryResult.circles.value.filter(
|
||||
(circle) =>
|
||||
circle.learningSequences.filter((ls) =>
|
||||
circle.someFinishedInLearningSequence(ls.translation_key)
|
||||
).length
|
||||
circle.learning_sequences.filter((ls) => someFinishedInLearningSequence(ls))
|
||||
.length
|
||||
).length;
|
||||
}
|
||||
return 0;
|
||||
|
|
@ -80,7 +60,7 @@ const changeViewType = (viewType: ViewType) => {
|
|||
{{ $t("learningPathPage.welcomeBack") }}
|
||||
</p>
|
||||
<h2 data-cy="learning-path-title">
|
||||
{{ learningPath?.title }}
|
||||
{{ course?.title }}
|
||||
</h2>
|
||||
</div>
|
||||
|
||||
|
|
@ -101,46 +81,52 @@ const changeViewType = (viewType: ViewType) => {
|
|||
|
||||
<!-- Bottom -->
|
||||
<div class="bg-white">
|
||||
<div class="flex flex-col justify-between px-6 sm:flex-row sm:px-12">
|
||||
<!-- Topics -->
|
||||
<div
|
||||
v-if="selectedView == 'path'"
|
||||
class="order-2 pb-8 sm:order-1 sm:pb-0 sm:pt-4"
|
||||
>
|
||||
<LearningPathTopics :topics="learningPath?.topics ?? []"></LearningPathTopics>
|
||||
<div v-if="lpQueryResult.learningPath">
|
||||
<div class="flex flex-col justify-between px-6 sm:flex-row sm:px-12">
|
||||
<!-- Topics -->
|
||||
<div
|
||||
v-if="selectedView == 'path'"
|
||||
class="order-2 pb-8 sm:order-1 sm:pb-0 sm:pt-4"
|
||||
>
|
||||
<LearningPathTopics
|
||||
:topics="learningPath?.topics ?? []"
|
||||
></LearningPathTopics>
|
||||
</div>
|
||||
<div v-else class="flex-grow"></div>
|
||||
|
||||
<!-- View switch -->
|
||||
<LearningPathViewSwitch
|
||||
class="order-1 py-8 sm:order-2 sm:py-0 sm:pt-4"
|
||||
:initial-view="selectedView"
|
||||
@select-view="changeViewType($event)"
|
||||
></LearningPathViewSwitch>
|
||||
</div>
|
||||
<div v-else class="flex-grow"></div>
|
||||
|
||||
<!-- View switch -->
|
||||
<LearningPathViewSwitch
|
||||
class="order-1 py-8 sm:order-2 sm:py-0 sm:pt-4"
|
||||
:initial-view="selectedView"
|
||||
@select-view="changeViewType($event)"
|
||||
></LearningPathViewSwitch>
|
||||
</div>
|
||||
<!-- Path view -->
|
||||
<div v-if="selectedView == 'path'" class="flex flex-col" data-cy="lp-path-view">
|
||||
<LearningPathPathView
|
||||
:learning-path="learningPath"
|
||||
:use-mobile-layout="useMobileLayout"
|
||||
:next-learning-content="lpQueryResult.nextLearningContent.value"
|
||||
></LearningPathPathView>
|
||||
</div>
|
||||
|
||||
<!-- Path view -->
|
||||
<div v-if="selectedView == 'path'" class="flex flex-col" data-cy="lp-path-view">
|
||||
<LearningPathPathView
|
||||
:learning-path="learningPath"
|
||||
:use-mobile-layout="useMobileLayout"
|
||||
></LearningPathPathView>
|
||||
</div>
|
||||
|
||||
<!-- List view -->
|
||||
<div
|
||||
v-if="selectedView == 'list'"
|
||||
class="flex flex-col pl-6 sm:pl-24"
|
||||
data-cy="lp-list-view"
|
||||
>
|
||||
<LearningPathListView :learning-path="learningPath"></LearningPathListView>
|
||||
</div>
|
||||
<div
|
||||
v-if="useMobileLayout"
|
||||
class="p-6"
|
||||
:class="useMobileLayout ? 'bg-gray-200' : ''"
|
||||
>
|
||||
<!--<LearningPathAppointmentsMock></LearningPathAppointmentsMock>-->
|
||||
<!-- List view -->
|
||||
<div
|
||||
v-if="selectedView == 'list'"
|
||||
class="flex flex-col pl-6 sm:pl-24"
|
||||
data-cy="lp-list-view"
|
||||
>
|
||||
<LearningPathListView
|
||||
:learning-path="learningPath"
|
||||
:next-learning-content="lpQueryResult.nextLearningContent.value"
|
||||
></LearningPathListView>
|
||||
</div>
|
||||
<div
|
||||
v-if="useMobileLayout"
|
||||
class="p-6"
|
||||
:class="useMobileLayout ? 'bg-gray-200' : ''"
|
||||
></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,13 +1,13 @@
|
|||
<script setup lang="ts">
|
||||
import LearningPathCircleColumn from "@/pages/learningPath/learningPathPage/LearningPathCircleColumn.vue";
|
||||
import LearningPathScrollButton from "@/pages/learningPath/learningPathPage/LearningPathScrollButton.vue";
|
||||
import type { Circle } from "@/services/circle";
|
||||
import type { LearningPath } from "@/services/learningPath";
|
||||
import { useScroll } from "@vueuse/core";
|
||||
import { ref } from "vue";
|
||||
import type { LearningContentWithCompletion, LearningPathType } from "@/types";
|
||||
|
||||
const props = defineProps<{
|
||||
learningPath: LearningPath | undefined;
|
||||
learningPath: LearningPathType | undefined;
|
||||
nextLearningContent: LearningContentWithCompletion | undefined;
|
||||
useMobileLayout: boolean;
|
||||
hideButtons?: boolean;
|
||||
overrideCircleUrlBase?: string;
|
||||
|
|
@ -25,9 +25,6 @@ const isLastCircle = (topicIndex: number, circleIndex: number, numCircles: numbe
|
|||
topicIndex === (props.learningPath?.topics ?? []).length - 1 &&
|
||||
circleIndex === numCircles - 1;
|
||||
|
||||
const isCurrentCircle = (circle: Circle) =>
|
||||
props.learningPath?.nextLearningContent?.parentCircle === circle;
|
||||
|
||||
const scrollRight = () => scrollLearnPathDiagram(scrollIncrement);
|
||||
|
||||
const scrollLeft = () => scrollLearnPathDiagram(-scrollIncrement);
|
||||
|
|
@ -58,6 +55,7 @@ const scrollLearnPathDiagram = (offset: number) => {
|
|||
<p
|
||||
:id="`topic-${topic.slug}`"
|
||||
class="inline-block h-12 self-start px-4 font-bold text-gray-800"
|
||||
data-cy="lp-topic"
|
||||
>
|
||||
{{ topic.title }}
|
||||
</p>
|
||||
|
|
@ -65,14 +63,12 @@ const scrollLearnPathDiagram = (offset: number) => {
|
|||
<LearningPathCircleColumn
|
||||
v-for="(circle, circleIndex) in topic.circles"
|
||||
:key="circle.id"
|
||||
:learning-path="learningPath"
|
||||
:circle="circle"
|
||||
:topic="topic"
|
||||
:next-learning-content="props.nextLearningContent"
|
||||
:is-first-circle="isFirstCircle(topicIndex, circleIndex)"
|
||||
:is-last-circle="
|
||||
isLastCircle(topicIndex, circleIndex, topic.circles.length)
|
||||
"
|
||||
:is-current-circle="isCurrentCircle(circle) && !props.hideButtons"
|
||||
:override-circle-url="
|
||||
props.overrideCircleUrlBase
|
||||
? `${props.overrideCircleUrlBase}/${circle.slug}`
|
||||
|
|
|
|||
|
|
@ -1,11 +1,11 @@
|
|||
<script setup lang="ts">
|
||||
import type { Topic } from "@/types";
|
||||
import type { TopicType } from "@/types";
|
||||
|
||||
const props = defineProps<{
|
||||
topics: Topic[];
|
||||
topics: TopicType[];
|
||||
}>();
|
||||
|
||||
const scrollToTopic = (topic: Topic) => {
|
||||
const scrollToTopic = (topic: TopicType) => {
|
||||
const id = `topic-${topic.slug}`;
|
||||
const el = document.getElementById(id);
|
||||
el?.scrollIntoView({ behavior: "smooth", inline: "center", block: "nearest" });
|
||||
|
|
|
|||
|
|
@ -2,14 +2,18 @@ import type {
|
|||
CircleSectorData,
|
||||
CircleSectorProgress,
|
||||
} from "@/pages/learningPath/learningPathPage/LearningPathCircle.vue";
|
||||
import type { Circle } from "@/services/circle";
|
||||
import {
|
||||
allFinishedInLearningSequence,
|
||||
someFinishedInLearningSequence,
|
||||
} from "@/services/circle";
|
||||
import type { CircleType } from "@/types";
|
||||
|
||||
export function calculateCircleSectorData(circle: Circle): CircleSectorData[] {
|
||||
return circle.learningSequences.map((ls) => {
|
||||
export function calculateCircleSectorData(circle: CircleType): CircleSectorData[] {
|
||||
return circle.learning_sequences.map((ls) => {
|
||||
let progress: CircleSectorProgress = "none";
|
||||
if (circle.allFinishedInLearningSequence(ls.translation_key)) {
|
||||
if (allFinishedInLearningSequence(ls)) {
|
||||
progress = "finished";
|
||||
} else if (circle.someFinishedInLearningSequence(ls.translation_key)) {
|
||||
} else if (someFinishedInLearningSequence(ls)) {
|
||||
progress = "in_progress";
|
||||
}
|
||||
return {
|
||||
|
|
|
|||
|
|
@ -1,10 +1,9 @@
|
|||
<script setup lang="ts">
|
||||
import { useCircleStore } from "@/stores/circle";
|
||||
import type { LearningUnit } from "@/types";
|
||||
import type { CircleType, LearningUnit } from "@/types";
|
||||
import * as log from "loglevel";
|
||||
|
||||
import { useCurrentCourseSession } from "@/composables";
|
||||
import { COMPLETION_FAILURE, COMPLETION_SUCCESS } from "@/constants";
|
||||
import { useCurrentCourseSession, useCourseDataWithCompletion } from "@/composables";
|
||||
import LearningContentContainer from "@/pages/learningPath/learningContentPage/LearningContentContainer.vue";
|
||||
import LearningContentMultiLayout from "@/pages/learningPath/learningContentPage/layouts/LearningContentMultiLayout.vue";
|
||||
import eventBus from "@/utils/eventBus";
|
||||
|
|
@ -17,16 +16,17 @@ log.debug("LearningContent.vue setup");
|
|||
|
||||
const circleStore = useCircleStore();
|
||||
const courseSession = useCurrentCourseSession();
|
||||
const courseCompletionData = useCourseDataWithCompletion();
|
||||
|
||||
const questionIndex = useRouteQuery("step", "0", { transform: Number, mode: "push" });
|
||||
|
||||
const previousRoute = getPreviousRoute();
|
||||
|
||||
const props = defineProps<{
|
||||
learningUnit: LearningUnit;
|
||||
circle: CircleType;
|
||||
}>();
|
||||
|
||||
const questions = computed(() => props.learningUnit?.children);
|
||||
const questions = computed(() => props.learningUnit?.performance_criteria ?? []);
|
||||
const currentQuestion = computed(() => questions.value[questionIndex.value]);
|
||||
const showPreviousButton = computed(() => questionIndex.value != 0);
|
||||
const showNextButton = computed(
|
||||
|
|
@ -44,7 +44,7 @@ function handleContinue() {
|
|||
questionIndex.value += 1;
|
||||
} else {
|
||||
log.debug("continue to next learning content");
|
||||
circleStore.continueFromSelfEvaluation(props.learningUnit);
|
||||
circleStore.continueFromSelfEvaluation(props.learningUnit, props.circle);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -56,7 +56,7 @@ function handleBack() {
|
|||
}
|
||||
|
||||
function handleFinishedLearningContent() {
|
||||
circleStore.closeSelfEvaluation(props.learningUnit, previousRoute);
|
||||
circleStore.closeSelfEvaluation(props.learningUnit, props.circle, previousRoute);
|
||||
}
|
||||
|
||||
eventBus.on("finishedLearningContent", handleFinishedLearningContent);
|
||||
|
|
@ -69,7 +69,9 @@ onUnmounted(() => {
|
|||
<template>
|
||||
<div v-if="learningUnit">
|
||||
<LearningContentContainer
|
||||
@exit="circleStore.closeSelfEvaluation(props.learningUnit, previousRoute)"
|
||||
@exit="
|
||||
circleStore.closeSelfEvaluation(props.learningUnit, props.circle, previousRoute)
|
||||
"
|
||||
>
|
||||
<LearningContentMultiLayout
|
||||
:current-step="questionIndex"
|
||||
|
|
@ -101,7 +103,7 @@ onUnmounted(() => {
|
|||
'border-2': currentQuestion.completion_status === 'SUCCESS',
|
||||
}"
|
||||
data-cy="success"
|
||||
@click="circleStore.markCompletion(currentQuestion, COMPLETION_SUCCESS)"
|
||||
@click="courseCompletionData.markCompletion(currentQuestion, 'SUCCESS')"
|
||||
>
|
||||
<it-icon-smiley-happy class="mr-4 h-16 w-16"></it-icon-smiley-happy>
|
||||
<span class="text-large font-bold">
|
||||
|
|
@ -111,12 +113,11 @@ onUnmounted(() => {
|
|||
<button
|
||||
class="inline-flex flex-1 items-center border p-4 text-left"
|
||||
:class="{
|
||||
'border-orange-500':
|
||||
currentQuestion.completion_status === COMPLETION_FAILURE,
|
||||
'border-2': currentQuestion.completion_status === COMPLETION_FAILURE,
|
||||
'border-orange-500': currentQuestion.completion_status === 'FAIL',
|
||||
'border-2': currentQuestion.completion_status === 'FAIL',
|
||||
}"
|
||||
data-cy="fail"
|
||||
@click="circleStore.markCompletion(currentQuestion, 'FAIL')"
|
||||
@click="courseCompletionData.markCompletion(currentQuestion, 'FAIL')"
|
||||
>
|
||||
<it-icon-smiley-thinking
|
||||
class="mr-4 h-16 w-16"
|
||||
|
|
|
|||
|
|
@ -2,9 +2,8 @@
|
|||
import * as log from "loglevel";
|
||||
|
||||
import SelfEvaluation from "@/pages/learningPath/selfEvaluationPage/SelfEvaluation.vue";
|
||||
import { useCircleStore } from "@/stores/circle";
|
||||
import type { LearningUnit } from "@/types";
|
||||
import { onMounted, reactive } from "vue";
|
||||
import { computed } from "vue";
|
||||
import { useCourseDataWithCompletion } from "@/composables";
|
||||
|
||||
log.debug("LearningUnitSelfEvaluationView created");
|
||||
|
||||
|
|
@ -14,32 +13,21 @@ const props = defineProps<{
|
|||
learningUnitSlug: string;
|
||||
}>();
|
||||
|
||||
const circleStore = useCircleStore();
|
||||
|
||||
const state: { learningUnit?: LearningUnit } = reactive({});
|
||||
|
||||
onMounted(async () => {
|
||||
log.debug(
|
||||
"LearningUnitSelfEvaluationView mounted",
|
||||
props.courseSlug,
|
||||
props.circleSlug,
|
||||
props.learningUnitSlug
|
||||
);
|
||||
|
||||
try {
|
||||
state.learningUnit = await circleStore.loadSelfEvaluation(
|
||||
props.courseSlug,
|
||||
props.circleSlug,
|
||||
props.learningUnitSlug
|
||||
);
|
||||
} catch (error) {
|
||||
log.error(error);
|
||||
}
|
||||
const courseData = useCourseDataWithCompletion(props.courseSlug);
|
||||
const learningUnit = computed(() =>
|
||||
courseData.findLearningUnit(props.learningUnitSlug, props.circleSlug)
|
||||
);
|
||||
const circle = computed(() => {
|
||||
return courseData.findCircle(props.circleSlug);
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<SelfEvaluation v-if="state.learningUnit" :learning-unit="state.learningUnit" />
|
||||
<SelfEvaluation
|
||||
v-if="learningUnit && circle"
|
||||
:learning-unit="learningUnit"
|
||||
:circle="circle"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<style lang="postcss" scoped></style>
|
||||
|
|
|
|||
|
|
@ -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 { itGet } from "@/fetchHelpers";
|
||||
import type { LearningPath } from "@/services/learningPath";
|
||||
import type {
|
||||
Assignment,
|
||||
AssignmentCompletion,
|
||||
CourseSessionUser,
|
||||
LearningContentAssignment,
|
||||
UserAssignmentCompletionStatus,
|
||||
} from "@/types";
|
||||
import { sum } from "d3";
|
||||
|
|
@ -16,17 +14,6 @@ export interface GradedUser {
|
|||
points: number;
|
||||
}
|
||||
|
||||
export function calcLearningContentAssignments(learningPath?: LearningPath) {
|
||||
// TODO: filter by circle
|
||||
if (!learningPath) return [];
|
||||
|
||||
return learningPath.circles.flatMap((circle) => {
|
||||
return circle.flatLearningContents.filter(
|
||||
(lc) => lc.content_type === "learnpath.LearningContentAssignment"
|
||||
) as LearningContentAssignment[];
|
||||
});
|
||||
}
|
||||
|
||||
export async function loadAssignmentCompletionStatusData(
|
||||
assignmentId: string,
|
||||
courseSessionId: string,
|
||||
|
|
|
|||
|
|
@ -1,268 +1,70 @@
|
|||
import type { LearningPath } from "@/services/learningPath";
|
||||
import type {
|
||||
CircleChild,
|
||||
CircleGoal,
|
||||
CircleJobSituation,
|
||||
CourseCompletion,
|
||||
LearningContent,
|
||||
LearningContentInterface,
|
||||
CircleType,
|
||||
CourseCompletionStatus,
|
||||
LearningSequence,
|
||||
LearningUnit,
|
||||
LearningUnitPerformanceCriteria,
|
||||
WagtailCircle,
|
||||
} from "@/types";
|
||||
import groupBy from "lodash/groupBy";
|
||||
import partition from "lodash/partition";
|
||||
import values from "lodash/values";
|
||||
import log from "loglevel";
|
||||
|
||||
function isLearningContentType(object: any): object is LearningContent {
|
||||
return (
|
||||
object?.content_type === "learnpath.LearningContentAssignment" ||
|
||||
object?.content_type === "learnpath.LearningContentAttendanceCourse" ||
|
||||
object?.content_type === "learnpath.LearningContentDocumentList" ||
|
||||
object?.content_type === "learnpath.LearningContentFeedback" ||
|
||||
object?.content_type === "learnpath.LearningContentLearningModule" ||
|
||||
object?.content_type === "learnpath.LearningContentMediaLibrary" ||
|
||||
object?.content_type === "learnpath.LearningContentPlaceholder" ||
|
||||
object?.content_type === "learnpath.LearningContentRichText" ||
|
||||
object?.content_type === "learnpath.LearningContentEdoniqTest" ||
|
||||
object?.content_type === "learnpath.LearningContentVideo"
|
||||
);
|
||||
export function circleFlatChildren(circle: CircleType) {
|
||||
return [
|
||||
...circleFlatLearningContents(circle),
|
||||
...circleFlatLearningUnits(circle).flatMap((lu) => {
|
||||
return lu.performance_criteria;
|
||||
}),
|
||||
];
|
||||
}
|
||||
|
||||
export function parseLearningSequences(
|
||||
circle: Circle,
|
||||
children: CircleChild[]
|
||||
): LearningSequence[] {
|
||||
let learningSequence: LearningSequence | undefined;
|
||||
let learningUnit: LearningUnit | undefined;
|
||||
let learningContent: LearningContent | undefined;
|
||||
let previousLearningContent: LearningContent | undefined;
|
||||
const result: LearningSequence[] = [];
|
||||
|
||||
children.forEach((child) => {
|
||||
if (child.content_type === "learnpath.LearningSequence") {
|
||||
if (learningSequence) {
|
||||
if (learningUnit) {
|
||||
learningUnit.last = true;
|
||||
}
|
||||
}
|
||||
learningSequence = Object.assign(child, { learningUnits: [] });
|
||||
result.push(learningSequence);
|
||||
} else if (child.content_type === "learnpath.LearningUnit") {
|
||||
if (!learningSequence) {
|
||||
throw new Error("LearningUnit found before LearningSequence");
|
||||
}
|
||||
|
||||
learningUnit = Object.assign(child, {
|
||||
learningContents: [],
|
||||
parentLearningSequence: learningSequence,
|
||||
parentCircle: circle,
|
||||
children: child.children.map((c) => {
|
||||
c.parentLearningUnit = learningUnit;
|
||||
c.parentLearningSequence = learningSequence;
|
||||
return c;
|
||||
}),
|
||||
});
|
||||
learningSequence.learningUnits.push(learningUnit);
|
||||
} else if (isLearningContentType(child)) {
|
||||
if (!learningUnit) {
|
||||
throw new Error(`LearningContent found before LearningUnit ${child.slug}`);
|
||||
}
|
||||
previousLearningContent = learningContent;
|
||||
|
||||
learningContent = Object.assign(child, {
|
||||
parentCircle: circle,
|
||||
parentLearningSequence: learningSequence,
|
||||
parentLearningUnit: learningUnit,
|
||||
previousLearningContent: previousLearningContent,
|
||||
});
|
||||
|
||||
if (previousLearningContent) {
|
||||
previousLearningContent.nextLearningContent = learningContent;
|
||||
}
|
||||
|
||||
learningUnit.learningContents.push(child);
|
||||
} else {
|
||||
log.error("Unknown CircleChild found...", child);
|
||||
throw new Error("Unknown CircleChild found...");
|
||||
}
|
||||
export function circleFlatLearningContents(circle: CircleType) {
|
||||
return circleFlatLearningUnits(circle).flatMap((lu) => {
|
||||
return lu.learning_contents;
|
||||
});
|
||||
}
|
||||
|
||||
if (learningUnit) {
|
||||
learningUnit.last = true;
|
||||
} else {
|
||||
throw new Error(
|
||||
"Finished with LearningContent but there is no LearningSequence and LearningUnit"
|
||||
);
|
||||
}
|
||||
|
||||
// sum minutes
|
||||
result.forEach((learningSequence) => {
|
||||
learningSequence.minutes = 0;
|
||||
learningSequence.learningUnits.forEach((learningUnit) => {
|
||||
learningUnit.minutes = 0;
|
||||
learningUnit.learningContents.forEach((learningContent) => {
|
||||
learningUnit.minutes += learningContent.minutes;
|
||||
});
|
||||
learningSequence.minutes += learningUnit.minutes;
|
||||
});
|
||||
export function circleFlatLearningUnits(circle: CircleType) {
|
||||
return circle.learning_sequences.flatMap((ls) => {
|
||||
return ls.learning_units;
|
||||
});
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
export class Circle implements WagtailCircle {
|
||||
readonly content_type = "learnpath.Circle";
|
||||
readonly learningSequences: LearningSequence[];
|
||||
|
||||
nextCircle?: Circle;
|
||||
previousCircle?: Circle;
|
||||
|
||||
constructor(
|
||||
public readonly id: string,
|
||||
public readonly slug: string,
|
||||
public readonly title: string,
|
||||
public readonly translation_key: string,
|
||||
public readonly frontend_url: string,
|
||||
public readonly description: string,
|
||||
public readonly children: CircleChild[],
|
||||
public readonly goal_description: string,
|
||||
public readonly goals: CircleGoal[],
|
||||
public readonly job_situation_description: string,
|
||||
public readonly job_situations: CircleJobSituation[],
|
||||
public readonly parentLearningPath?: LearningPath
|
||||
) {
|
||||
this.learningSequences = parseLearningSequences(this, this.children);
|
||||
}
|
||||
|
||||
public static fromJson(
|
||||
wagtailCircle: WagtailCircle,
|
||||
learningPath?: LearningPath
|
||||
): Circle {
|
||||
// TODO add error checking when the data does not conform to the schema
|
||||
return new Circle(
|
||||
wagtailCircle.id,
|
||||
wagtailCircle.slug,
|
||||
wagtailCircle.title,
|
||||
wagtailCircle.translation_key,
|
||||
wagtailCircle.frontend_url,
|
||||
wagtailCircle.description,
|
||||
wagtailCircle.children,
|
||||
wagtailCircle.goal_description,
|
||||
wagtailCircle.goals,
|
||||
wagtailCircle.job_situation_description,
|
||||
wagtailCircle.job_situations,
|
||||
learningPath
|
||||
);
|
||||
}
|
||||
|
||||
public get flatChildren(): (
|
||||
| LearningContentInterface
|
||||
| LearningUnitPerformanceCriteria
|
||||
)[] {
|
||||
const result: (LearningContentInterface | LearningUnitPerformanceCriteria)[] = [];
|
||||
this.learningSequences.forEach((learningSequence) => {
|
||||
learningSequence.learningUnits.forEach((learningUnit) => {
|
||||
learningUnit.children.forEach((performanceCriteria) => {
|
||||
result.push(performanceCriteria);
|
||||
});
|
||||
learningUnit.learningContents.forEach((learningContent) => {
|
||||
result.push(learningContent);
|
||||
});
|
||||
});
|
||||
});
|
||||
return result;
|
||||
}
|
||||
|
||||
public get flatLearningContents(): LearningContent[] {
|
||||
const result: LearningContent[] = [];
|
||||
this.learningSequences.forEach((learningSequence) => {
|
||||
learningSequence.learningUnits.forEach((learningUnit) => {
|
||||
learningUnit.learningContents.forEach((learningContent) => {
|
||||
result.push(learningContent);
|
||||
});
|
||||
});
|
||||
});
|
||||
return result;
|
||||
}
|
||||
|
||||
public get flatLearningUnits(): LearningUnit[] {
|
||||
const result: LearningUnit[] = [];
|
||||
this.learningSequences.forEach((learningSequence) => {
|
||||
learningSequence.learningUnits.forEach((learningUnit) => {
|
||||
result.push(learningUnit);
|
||||
});
|
||||
});
|
||||
return result;
|
||||
}
|
||||
|
||||
public someFinishedInLearningSequence(translationKey: string): boolean {
|
||||
if (translationKey) {
|
||||
return (
|
||||
this.flatChildren.filter((lc) => {
|
||||
return (
|
||||
lc.completion_status === "SUCCESS" &&
|
||||
lc.parentLearningSequence?.translation_key === translationKey
|
||||
);
|
||||
}).length > 0
|
||||
);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public allFinishedInLearningSequence(translationKey: string): boolean {
|
||||
if (translationKey) {
|
||||
const [performanceCriteria, learningContents] = partition(
|
||||
this.flatChildren.filter(
|
||||
(lc) => lc.parentLearningSequence?.translation_key === translationKey
|
||||
),
|
||||
function (child) {
|
||||
return child.content_type === "competence.PerformanceCriteria";
|
||||
}
|
||||
);
|
||||
|
||||
const groupedPerformanceCriteria = values(
|
||||
groupBy(performanceCriteria, (pc) => pc.parentLearningUnit?.id)
|
||||
);
|
||||
|
||||
return (
|
||||
learningContents.every((lc) => lc.completion_status === "SUCCESS") &&
|
||||
(groupedPerformanceCriteria.length === 0 ||
|
||||
groupedPerformanceCriteria.every((group) =>
|
||||
group.every(
|
||||
(pc) =>
|
||||
pc.completion_status === "SUCCESS" || pc.completion_status === "FAIL"
|
||||
)
|
||||
))
|
||||
);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public isComplete(): boolean {
|
||||
return this.learningSequences.every((ls) =>
|
||||
this.allFinishedInLearningSequence(ls.translation_key)
|
||||
);
|
||||
}
|
||||
|
||||
public parseCompletionData(completionData: CourseCompletion[]) {
|
||||
this.flatChildren.forEach((page) => {
|
||||
const pageIndex = completionData.findIndex((e) => {
|
||||
return e.page_id === page.id;
|
||||
});
|
||||
if (pageIndex >= 0) {
|
||||
page.completion_status = completionData[pageIndex].completion_status;
|
||||
} else {
|
||||
page.completion_status = "UNKNOWN";
|
||||
}
|
||||
});
|
||||
|
||||
if (this.parentLearningPath) {
|
||||
this.parentLearningPath.calcNextLearningContent(completionData);
|
||||
}
|
||||
}
|
||||
export function learningSequenceFlatChildren(ls: LearningSequence) {
|
||||
return [
|
||||
...ls.learning_units.flatMap((lu) => {
|
||||
return lu.learning_contents;
|
||||
}),
|
||||
...ls.learning_units.flatMap((lu) => {
|
||||
return lu.performance_criteria;
|
||||
}),
|
||||
];
|
||||
}
|
||||
|
||||
export function someFinishedInLearningSequence(ls: LearningSequence) {
|
||||
return learningSequenceFlatChildren(ls).some((lc) => {
|
||||
return lc.completion_status === "SUCCESS";
|
||||
});
|
||||
}
|
||||
|
||||
export function allFinishedInLearningSequence(ls: LearningSequence) {
|
||||
return learningSequenceFlatChildren(ls).every((lc) => {
|
||||
return lc.completion_status === "SUCCESS";
|
||||
});
|
||||
}
|
||||
|
||||
export function calcSelfEvaluationStatus(
|
||||
learningUnit: LearningUnit
|
||||
): CourseCompletionStatus {
|
||||
if (learningUnit.performance_criteria.length > 0) {
|
||||
if (
|
||||
learningUnit.performance_criteria.every((q) => q.completion_status === "SUCCESS")
|
||||
) {
|
||||
return "SUCCESS";
|
||||
}
|
||||
if (
|
||||
learningUnit.performance_criteria.every(
|
||||
(q) => q.completion_status === "FAIL" || q.completion_status === "SUCCESS"
|
||||
)
|
||||
) {
|
||||
return "FAIL";
|
||||
}
|
||||
}
|
||||
return "UNKNOWN";
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
CourseCompletionStatus,
|
||||
LearningContentInterface,
|
||||
CircleType,
|
||||
LearningContent,
|
||||
LearningContentWithCompletion,
|
||||
LearningUnit,
|
||||
LearningUnitPerformanceCriteria,
|
||||
PerformanceCriteria,
|
||||
} from "@/types";
|
||||
import * as log from "loglevel";
|
||||
import { defineStore } from "pinia";
|
||||
import type { RouteLocationNormalized } from "vue-router";
|
||||
|
||||
export type CircleStoreState = {
|
||||
circle: Circle | undefined;
|
||||
page: "INDEX" | "OVERVIEW";
|
||||
};
|
||||
|
||||
function createLearningUnitHash(learningUnit: LearningUnit | undefined) {
|
||||
const luSlug = learningUnit?.slug;
|
||||
const circleSlug = learningUnit?.parentCircle?.slug;
|
||||
if (luSlug && circleSlug) {
|
||||
return "#" + luSlug.replace(`${circleSlug}-`, "");
|
||||
function createLearningUnitHash(
|
||||
circle: CircleType,
|
||||
learningUnitSlug: string | undefined
|
||||
) {
|
||||
if (learningUnitSlug && circle) {
|
||||
return "#" + learningUnitSlug.replace(`${circle.slug}-`, "");
|
||||
}
|
||||
|
||||
return "";
|
||||
|
|
@ -32,111 +27,30 @@ export const useCircleStore = defineStore({
|
|||
id: "circle",
|
||||
state: () => {
|
||||
return {
|
||||
circle: undefined,
|
||||
page: "INDEX",
|
||||
} as CircleStoreState;
|
||||
},
|
||||
getters: {},
|
||||
actions: {
|
||||
async loadCircle(
|
||||
courseSlug: string,
|
||||
circleSlug: string,
|
||||
userId: string | undefined = undefined
|
||||
): Promise<Circle> {
|
||||
if (!userId) {
|
||||
const userStore = useUserStore();
|
||||
userId = userStore.id;
|
||||
}
|
||||
|
||||
this.circle = undefined;
|
||||
const learningPathSlug = courseSlug + "-lp";
|
||||
const learningPathStore = useLearningPathStore();
|
||||
const learningPath = await learningPathStore.loadLearningPath(
|
||||
learningPathSlug,
|
||||
userId
|
||||
);
|
||||
if (learningPath) {
|
||||
this.circle = learningPath.circles.find((circle) => {
|
||||
return circle.slug.endsWith(circleSlug);
|
||||
});
|
||||
}
|
||||
|
||||
if (!this.circle) {
|
||||
throw `No circle found with slug: ${circleSlug}`;
|
||||
}
|
||||
|
||||
return this.circle;
|
||||
},
|
||||
async loadLearningContent(
|
||||
courseSlug: string,
|
||||
circleSlug: string,
|
||||
learningContentSlug: string
|
||||
) {
|
||||
const circle = await this.loadCircle(courseSlug, circleSlug);
|
||||
const result = circle.flatLearningContents.find((learningContent) => {
|
||||
return learningContent.slug.endsWith(learningContentSlug);
|
||||
});
|
||||
|
||||
if (!result) {
|
||||
throw `No learning content found with slug: ${learningContentSlug}`;
|
||||
}
|
||||
|
||||
return result;
|
||||
},
|
||||
async loadSelfEvaluation(
|
||||
courseSlug: string,
|
||||
circleSlug: string,
|
||||
learningUnitSlug: string
|
||||
) {
|
||||
const circle = await this.loadCircle(courseSlug, circleSlug);
|
||||
const learningUnit = circle.flatLearningUnits.find((child) => {
|
||||
return child.slug.endsWith(learningUnitSlug);
|
||||
});
|
||||
|
||||
if (!learningUnit) {
|
||||
throw `No self evaluation found with slug: ${learningUnitSlug}`;
|
||||
}
|
||||
|
||||
return learningUnit;
|
||||
},
|
||||
async markCompletion(
|
||||
page:
|
||||
| LearningContentInterface
|
||||
| LearningUnitPerformanceCriteria
|
||||
| PerformanceCriteria
|
||||
| undefined,
|
||||
completion_status: CourseCompletionStatus = "SUCCESS"
|
||||
) {
|
||||
const completionStore = useCompletionStore();
|
||||
|
||||
try {
|
||||
if (page) {
|
||||
page.completion_status = completion_status;
|
||||
const completionData = await completionStore.markPage(page);
|
||||
if (this.circle) {
|
||||
this.circle.parseCompletionData(completionData);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
log.error(error);
|
||||
return error;
|
||||
}
|
||||
},
|
||||
openLearningContent(learningContent: LearningContentInterface) {
|
||||
openLearningContent(learningContent: LearningContent) {
|
||||
this.router.push({
|
||||
path: learningContent.frontend_url,
|
||||
});
|
||||
},
|
||||
closeLearningContent(
|
||||
learningContent: LearningContentInterface,
|
||||
learningContent: LearningContentWithCompletion,
|
||||
circle: CircleType,
|
||||
returnRoute?: RouteLocationNormalized
|
||||
) {
|
||||
if (returnRoute) {
|
||||
this.router.push(returnRoute);
|
||||
} else {
|
||||
this.router.push({
|
||||
path: `${this.circle?.frontend_url}`,
|
||||
hash: createLearningUnitHash(learningContent.parentLearningUnit),
|
||||
path: `${circle.frontend_url}`,
|
||||
hash: createLearningUnitHash(
|
||||
circle,
|
||||
learningContent.parentLearningUnit?.slug
|
||||
),
|
||||
});
|
||||
}
|
||||
},
|
||||
|
|
@ -147,50 +61,37 @@ export const useCircleStore = defineStore({
|
|||
},
|
||||
closeSelfEvaluation(
|
||||
learningUnit: LearningUnit,
|
||||
circle: CircleType,
|
||||
returnRoute?: RouteLocationNormalized
|
||||
) {
|
||||
if (returnRoute) {
|
||||
this.router.push(returnRoute);
|
||||
} else {
|
||||
this.router.push({
|
||||
path: `${this.circle?.frontend_url}`,
|
||||
hash: createLearningUnitHash(learningUnit),
|
||||
path: `${circle.frontend_url}`,
|
||||
hash: createLearningUnitHash(circle, learningUnit.slug),
|
||||
});
|
||||
}
|
||||
},
|
||||
calcSelfEvaluationStatus(learningUnit: LearningUnit): CourseCompletionStatus {
|
||||
if (learningUnit.children.length > 0) {
|
||||
if (learningUnit.children.every((q) => q.completion_status === "SUCCESS")) {
|
||||
return "SUCCESS";
|
||||
}
|
||||
if (
|
||||
learningUnit.children.every(
|
||||
(q) => q.completion_status === "FAIL" || q.completion_status === "SUCCESS"
|
||||
)
|
||||
) {
|
||||
return "FAIL";
|
||||
}
|
||||
}
|
||||
return "UNKNOWN";
|
||||
},
|
||||
continueFromLearningContent(
|
||||
currentLearningContent: LearningContentInterface,
|
||||
returnRoute?: RouteLocationNormalized
|
||||
currentLearningContent: LearningContentWithCompletion,
|
||||
circle: CircleType,
|
||||
returnRoute?: RouteLocationNormalized,
|
||||
markCompletionFn?: (learningContent: LearningContentWithCompletion) => void
|
||||
) {
|
||||
if (currentLearningContent) {
|
||||
if (currentLearningContent.can_user_self_toggle_course_completion) {
|
||||
this.markCompletion(currentLearningContent, "SUCCESS");
|
||||
} else {
|
||||
// reload completion data anyway
|
||||
currentLearningContent.parentCircle?.parentLearningPath?.reloadCompletionData();
|
||||
if (markCompletionFn) {
|
||||
markCompletionFn(currentLearningContent);
|
||||
}
|
||||
}
|
||||
this.closeLearningContent(currentLearningContent, returnRoute);
|
||||
this.closeLearningContent(currentLearningContent, circle, returnRoute);
|
||||
} else {
|
||||
log.error("currentLearningContent is undefined");
|
||||
}
|
||||
},
|
||||
continueFromSelfEvaluation(learningUnit: LearningUnit) {
|
||||
this.closeSelfEvaluation(learningUnit);
|
||||
continueFromSelfEvaluation(learningUnit: LearningUnit, circle: CircleType) {
|
||||
this.closeSelfEvaluation(learningUnit, circle);
|
||||
},
|
||||
},
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { useLearningPathStore } from "@/stores/learningPath";
|
||||
import { useCourseData } from "@/composables";
|
||||
import { useUserStore } from "@/stores/user";
|
||||
import type { CircleLight, CourseSessionUser, ExpertSessionUser } from "@/types";
|
||||
import log from "loglevel";
|
||||
|
|
@ -12,7 +12,6 @@ export type CockpitStoreState = {
|
|||
courseSessionMembers: CourseSessionUser[] | undefined;
|
||||
circles: CircleCockpit[] | undefined;
|
||||
currentCircle: CircleCockpit | undefined;
|
||||
currentCourseSlug: string | undefined;
|
||||
};
|
||||
|
||||
export const useCockpitStore = defineStore({
|
||||
|
|
@ -22,7 +21,6 @@ export const useCockpitStore = defineStore({
|
|||
courseSessionMembers: undefined,
|
||||
circles: [],
|
||||
currentCircle: undefined,
|
||||
currentCourseSlug: undefined,
|
||||
} as CockpitStoreState;
|
||||
},
|
||||
actions: {
|
||||
|
|
@ -31,21 +29,8 @@ export const useCockpitStore = defineStore({
|
|||
currentCourseSessionUser: CourseSessionUser | undefined
|
||||
) {
|
||||
log.debug("loadCircles called", courseSlug);
|
||||
this.currentCourseSlug = courseSlug;
|
||||
|
||||
const circles = await courseCircles(
|
||||
this.currentCourseSlug,
|
||||
currentCourseSessionUser
|
||||
);
|
||||
|
||||
this.circles = circles.map((c) => {
|
||||
return {
|
||||
id: c.id,
|
||||
slug: c.slug,
|
||||
title: c.title,
|
||||
name: c.title,
|
||||
} as const;
|
||||
});
|
||||
this.circles = await courseCircles(courseSlug, currentCourseSessionUser);
|
||||
|
||||
if (this.circles?.length) {
|
||||
await this.setCurrentCourseCircle(this.circles[0].slug);
|
||||
|
|
@ -64,28 +49,27 @@ async function courseCircles(
|
|||
courseSlug: string,
|
||||
currentCourseSessionUser: CourseSessionUser | undefined
|
||||
) {
|
||||
const userStore = useUserStore();
|
||||
const userId = userStore.id;
|
||||
|
||||
if (currentCourseSessionUser && currentCourseSessionUser.role === "EXPERT") {
|
||||
const expert = currentCourseSessionUser as ExpertSessionUser;
|
||||
return expert.circles;
|
||||
return expert.circles.map((c) => {
|
||||
return { ...c, name: c.title };
|
||||
});
|
||||
}
|
||||
|
||||
const userStore = useUserStore();
|
||||
// Return all circles from learning path for admin users
|
||||
if (userStore.is_superuser) {
|
||||
const learningPathStore = useLearningPathStore();
|
||||
const learningPathCircles = learningPathStore
|
||||
.learningPathForUser(courseSlug, userId)
|
||||
?.circles.map((c) => {
|
||||
return {
|
||||
id: c.id,
|
||||
title: c.title,
|
||||
slug: c.slug,
|
||||
translation_key: c.translation_key,
|
||||
};
|
||||
});
|
||||
return learningPathCircles || [];
|
||||
const lpQueryResult = useCourseData(courseSlug);
|
||||
await lpQueryResult.resultPromise;
|
||||
|
||||
return (lpQueryResult.circles.value ?? []).map((c) => {
|
||||
return {
|
||||
id: c.id,
|
||||
slug: c.slug,
|
||||
title: c.title,
|
||||
name: c.title,
|
||||
} as const;
|
||||
});
|
||||
}
|
||||
|
||||
return [];
|
||||
|
|
|
|||
|
|
@ -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 { useCourseSessionsStore } from "@/stores/courseSessions";
|
||||
import { useUserStore } from "@/stores/user";
|
||||
import type { BaseCourseWagtailPage, CourseCompletion } from "@/types";
|
||||
import type {
|
||||
CourseCompletion,
|
||||
LearningContentWithCompletion,
|
||||
LearningUnitPerformanceCriteria,
|
||||
} from "@/types";
|
||||
import { defineStore } from "pinia";
|
||||
|
||||
export const useCompletionStore = defineStore({
|
||||
|
|
@ -29,7 +33,7 @@ export const useCompletionStore = defineStore({
|
|||
return userCompletionData || [];
|
||||
},
|
||||
async markPage(
|
||||
page: BaseCourseWagtailPage,
|
||||
page: LearningContentWithCompletion | LearningUnitPerformanceCriteria,
|
||||
userId: string | undefined = undefined,
|
||||
courseSessionId: string | undefined = undefined
|
||||
) {
|
||||
|
|
|
|||
|
|
@ -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 { Circle } from "@/services/circle";
|
||||
import type {
|
||||
ActionCompetenceObjectType,
|
||||
AssignmentAssignmentAssignmentTypeChoices,
|
||||
AssignmentCompletionObjectType,
|
||||
AssignmentCompletionStatus as AssignmentCompletionStatusGenerated,
|
||||
AssignmentObjectType,
|
||||
CircleObjectType,
|
||||
CourseCourseSessionUserRoleChoices,
|
||||
CourseSessionObjectType,
|
||||
CourseSessionUserObjectsType,
|
||||
LearningContentAssignmentObjectType,
|
||||
LearningContentAttendanceCourseObjectType,
|
||||
LearningContentDocumentListObjectType,
|
||||
LearningContentEdoniqTestObjectType,
|
||||
LearningContentFeedbackObjectType,
|
||||
LearningContentLearningModuleObjectType,
|
||||
LearningContentMediaLibraryObjectType,
|
||||
LearningContentPlaceholderObjectType,
|
||||
LearningContentRichTextObjectType,
|
||||
LearningContentVideoObjectType,
|
||||
LearningPathObjectType,
|
||||
LearningSequenceObjectType,
|
||||
LearningUnitObjectType,
|
||||
PerformanceCriteriaObjectType,
|
||||
TopicObjectType,
|
||||
} from "@/gql/graphql";
|
||||
import type { Component } from "vue";
|
||||
|
||||
export type LoginMethod = "local" | "sso";
|
||||
|
||||
export type CourseCompletionStatus = "UNKNOWN" | "FAIL" | "SUCCESS";
|
||||
|
||||
export type Completable = {
|
||||
completion_status?: CourseCompletionStatus;
|
||||
completion_status_updated_at?: string;
|
||||
};
|
||||
|
||||
export interface BaseCourseWagtailPage {
|
||||
readonly id: string;
|
||||
readonly title: string;
|
||||
|
|
@ -13,8 +42,6 @@ export interface BaseCourseWagtailPage {
|
|||
readonly content_type: string;
|
||||
readonly translation_key: string;
|
||||
readonly frontend_url: string;
|
||||
completion_status?: CourseCompletionStatus;
|
||||
completion_status_updated_at?: string;
|
||||
}
|
||||
|
||||
export interface CircleLight {
|
||||
|
|
@ -23,168 +50,120 @@ export interface CircleLight {
|
|||
readonly title: string;
|
||||
}
|
||||
|
||||
// refine generated types
|
||||
export type LearningContentAssignment = LearningContentAssignmentObjectType & {
|
||||
readonly content_type: "learnpath.LearningContentAssignment";
|
||||
};
|
||||
|
||||
export type LearningContentAttendanceCourse =
|
||||
LearningContentAttendanceCourseObjectType & {
|
||||
readonly content_type: "learnpath.LearningContentAttendanceCourse";
|
||||
};
|
||||
|
||||
export type LearningContentDocumentList = LearningContentDocumentListObjectType & {
|
||||
readonly content_type: "learnpath.LearningContentDocumentList";
|
||||
};
|
||||
|
||||
export type LearningContentEdoniqTest = LearningContentEdoniqTestObjectType & {
|
||||
readonly content_type: "learnpath.LearningContentEdoniqTest";
|
||||
};
|
||||
|
||||
export type LearningContentFeedback = LearningContentFeedbackObjectType & {
|
||||
readonly content_type: "learnpath.LearningContentFeedback";
|
||||
};
|
||||
|
||||
export type LearningContentLearningModule = LearningContentLearningModuleObjectType & {
|
||||
readonly content_type: "learnpath.LearningContentLearningModule";
|
||||
};
|
||||
|
||||
export type LearningContentMediaLibrary = LearningContentMediaLibraryObjectType & {
|
||||
readonly content_type: "learnpath.LearningContentMediaLibrary";
|
||||
};
|
||||
|
||||
export type LearningContentPlaceholder = LearningContentPlaceholderObjectType & {
|
||||
readonly content_type: "learnpath.LearningContentPlaceholder";
|
||||
};
|
||||
|
||||
export type LearningContentRichText = LearningContentRichTextObjectType & {
|
||||
readonly content_type: "learnpath.LearningContentRichText";
|
||||
};
|
||||
|
||||
export type LearningContentVideo = LearningContentVideoObjectType & {
|
||||
readonly content_type: "learnpath.LearningContentVideo";
|
||||
};
|
||||
|
||||
export type LearningContent =
|
||||
| LearningContentAssignment
|
||||
| LearningContentAttendanceCourse
|
||||
| LearningContentDocumentList
|
||||
| LearningContentEdoniqTest
|
||||
| LearningContentFeedback
|
||||
| LearningContentLearningModule
|
||||
| LearningContentMediaLibrary
|
||||
| LearningContentPlaceholder
|
||||
| LearningContentRichText
|
||||
| LearningContentEdoniqTest
|
||||
| LearningContentVideo;
|
||||
|
||||
export type LearningContentType = LearningContent["content_type"];
|
||||
export type LearningContentWithCompletion = LearningContent &
|
||||
Completable & {
|
||||
continueUrl?: string;
|
||||
firstInCircle?: boolean;
|
||||
parentLearningUnit?: {
|
||||
id: string;
|
||||
slug: string;
|
||||
title: string;
|
||||
};
|
||||
};
|
||||
|
||||
export interface LearningContentInterface extends BaseCourseWagtailPage {
|
||||
readonly content_type: LearningContentType;
|
||||
readonly minutes: number;
|
||||
readonly description: string;
|
||||
readonly content_url: string;
|
||||
readonly can_user_self_toggle_course_completion: boolean;
|
||||
parentCircle: Circle;
|
||||
parentLearningSequence?: LearningSequence;
|
||||
parentLearningUnit?: LearningUnit;
|
||||
nextLearningContent?: LearningContent;
|
||||
previousLearningContent?: LearningContent;
|
||||
}
|
||||
export type LearningContentContentType = LearningContent["content_type"];
|
||||
|
||||
export interface LearningContentAssignment extends LearningContentInterface {
|
||||
readonly content_type: "learnpath.LearningContentAssignment";
|
||||
readonly content_assignment_id: string;
|
||||
readonly assignment_type: AssignmentType;
|
||||
readonly competence_certificate?: {
|
||||
id: string;
|
||||
title: string;
|
||||
slug: string;
|
||||
content_type: string;
|
||||
translation_key: string;
|
||||
frontend_url: string;
|
||||
} | null;
|
||||
}
|
||||
export type LearningUnit = Omit<
|
||||
LearningUnitObjectType,
|
||||
"content_type" | "learning_contents" | "performance_criteria"
|
||||
> & {
|
||||
content_type: "learnpath.LearningUnit";
|
||||
learning_contents: LearningContentWithCompletion[];
|
||||
performance_criteria: LearningUnitPerformanceCriteria[];
|
||||
circle?: CircleLight;
|
||||
};
|
||||
|
||||
export interface LearningContentAttendanceCourse extends LearningContentInterface {
|
||||
readonly content_type: "learnpath.LearningContentAttendanceCourse";
|
||||
}
|
||||
export type LearningSequence = Omit<
|
||||
LearningSequenceObjectType,
|
||||
"content_type" | "learning_units"
|
||||
> & {
|
||||
content_type: "learnpath.LearningSequence";
|
||||
learning_units: LearningUnit[];
|
||||
};
|
||||
|
||||
export interface LearningContentDocument {
|
||||
readonly type: "document";
|
||||
readonly id: string;
|
||||
readonly value: MediaLibraryContentBlockValue;
|
||||
}
|
||||
export type CircleType = Omit<
|
||||
CircleObjectType,
|
||||
"content_type" | "learning_sequences"
|
||||
> & {
|
||||
content_type: "learnpath.Circle";
|
||||
learning_sequences: LearningSequence[];
|
||||
};
|
||||
|
||||
export interface LearningContentDocumentList extends LearningContentInterface {
|
||||
readonly content_type: "learnpath.LearningContentDocumentList";
|
||||
readonly documents: LearningContentDocument[];
|
||||
}
|
||||
export type TopicType = Omit<TopicObjectType, "content_type" | "circles"> & {
|
||||
content_type: "learnpath.Topic";
|
||||
circles: CircleType[];
|
||||
};
|
||||
|
||||
export interface LearningContentFeedback extends LearningContentInterface {
|
||||
readonly content_type: "learnpath.LearningContentFeedback";
|
||||
}
|
||||
export type LearningPathType = Omit<
|
||||
LearningPathObjectType,
|
||||
"content_type" | "topics"
|
||||
> & {
|
||||
content_type: "learnpath.LearningPath";
|
||||
topics: TopicType[];
|
||||
};
|
||||
|
||||
export interface LearningContentLearningModule extends LearningContentInterface {
|
||||
readonly content_type: "learnpath.LearningContentLearningModule";
|
||||
}
|
||||
|
||||
export interface LearningContentMediaLibrary extends LearningContentInterface {
|
||||
readonly content_type: "learnpath.LearningContentMediaLibrary";
|
||||
}
|
||||
|
||||
export interface LearningContentPlaceholder extends LearningContentInterface {
|
||||
readonly content_type: "learnpath.LearningContentPlaceholder";
|
||||
}
|
||||
|
||||
export interface LearningContentRichText extends LearningContentInterface {
|
||||
readonly content_type: "learnpath.LearningContentRichText";
|
||||
readonly text: string;
|
||||
}
|
||||
|
||||
export interface LearningContentEdoniqTest extends LearningContentInterface {
|
||||
readonly content_type: "learnpath.LearningContentEdoniqTest";
|
||||
readonly checkbox_text: string;
|
||||
readonly has_extended_time_test: boolean;
|
||||
readonly content_assignment_id: string;
|
||||
readonly competence_certificate?: {
|
||||
id: string;
|
||||
title: string;
|
||||
slug: string;
|
||||
content_type: string;
|
||||
translation_key: string;
|
||||
frontend_url: string;
|
||||
} | null;
|
||||
}
|
||||
|
||||
export interface LearningContentVideo extends LearningContentInterface {
|
||||
readonly content_type: "learnpath.LearningContentVideo";
|
||||
}
|
||||
|
||||
export interface LearningUnitPerformanceCriteria extends BaseCourseWagtailPage {
|
||||
readonly content_type: "competence.PerformanceCriteria";
|
||||
readonly competence_id: string;
|
||||
parentLearningSequence?: LearningSequence;
|
||||
parentLearningUnit?: LearningUnit;
|
||||
}
|
||||
|
||||
export interface LearningUnit extends BaseCourseWagtailPage {
|
||||
readonly content_type: "learnpath.LearningUnit";
|
||||
readonly evaluate_url: string;
|
||||
readonly course_category: CourseCategory;
|
||||
readonly title_hidden: boolean;
|
||||
children: LearningUnitPerformanceCriteria[];
|
||||
|
||||
// additional frontend fields
|
||||
learningContents: LearningContent[];
|
||||
minutes: number;
|
||||
parentLearningSequence?: LearningSequence;
|
||||
parentCircle?: Circle;
|
||||
last?: boolean;
|
||||
}
|
||||
|
||||
export interface LearningSequence extends BaseCourseWagtailPage {
|
||||
readonly content_type: "learnpath.LearningSequence";
|
||||
icon: string;
|
||||
learningUnits: LearningUnit[];
|
||||
minutes: number;
|
||||
}
|
||||
|
||||
export type CircleChild = LearningContentInterface | LearningUnit | LearningSequence;
|
||||
|
||||
export interface WagtailLearningPath extends BaseCourseWagtailPage {
|
||||
readonly content_type: "learnpath.LearningPath";
|
||||
course: Course;
|
||||
children: LearningPathChild[];
|
||||
}
|
||||
|
||||
export interface CircleGoal {
|
||||
type: "goal";
|
||||
value: string;
|
||||
id: string;
|
||||
}
|
||||
|
||||
export interface CircleJobSituation {
|
||||
type: "job_situation";
|
||||
value: string;
|
||||
id: string;
|
||||
}
|
||||
|
||||
export interface WagtailCircle extends BaseCourseWagtailPage {
|
||||
readonly content_type: "learnpath.Circle";
|
||||
readonly description: string;
|
||||
readonly goal_description: string;
|
||||
readonly goals: CircleGoal[];
|
||||
readonly job_situation_description: string;
|
||||
readonly job_situations: CircleJobSituation[];
|
||||
readonly children: CircleChild[];
|
||||
}
|
||||
|
||||
export interface Topic extends BaseCourseWagtailPage {
|
||||
readonly content_type: "learnpath.Topic";
|
||||
readonly is_visible: boolean;
|
||||
circles: Circle[];
|
||||
}
|
||||
|
||||
export type LearningPathChild = Topic | WagtailCircle;
|
||||
export type LearningUnitPerformanceCriteria = Omit<
|
||||
PerformanceCriteriaObjectType,
|
||||
"content_type"
|
||||
> &
|
||||
Completable & {
|
||||
readonly content_type: "competence.PerformanceCriteria";
|
||||
circle?: CircleLight;
|
||||
};
|
||||
|
||||
export interface CourseCompletion {
|
||||
readonly id: string;
|
||||
|
|
@ -361,57 +340,31 @@ export interface AssignmentEvaluationTask {
|
|||
};
|
||||
}
|
||||
|
||||
export type AssignmentType =
|
||||
| "CASEWORK"
|
||||
| "PREP_ASSIGNMENT"
|
||||
| "REFLECTION"
|
||||
| "CONDITION_ACCEPTANCE"
|
||||
| "EDONIQ_TEST";
|
||||
export type AssignmentType = AssignmentAssignmentAssignmentTypeChoices;
|
||||
|
||||
export interface Assignment extends BaseCourseWagtailPage {
|
||||
export type Assignment = Omit<
|
||||
AssignmentObjectType,
|
||||
"content_type" | "performance_objectives" | "tasks" | "evaluation_tasks"
|
||||
> & {
|
||||
readonly content_type: "assignment.Assignment";
|
||||
readonly assignment_type: AssignmentType;
|
||||
readonly needs_expert_evaluation: boolean;
|
||||
readonly intro_text: string;
|
||||
readonly effort_required: string;
|
||||
readonly performance_objectives: AssignmentPerformanceObjective[];
|
||||
readonly evaluation_description: string;
|
||||
readonly evaluation_document_url: string;
|
||||
readonly tasks: AssignmentTask[];
|
||||
readonly evaluation_tasks: AssignmentEvaluationTask[];
|
||||
readonly max_points: number;
|
||||
readonly competence_certificate?: {
|
||||
id: string;
|
||||
title: string;
|
||||
slug: string;
|
||||
content_type: string;
|
||||
translation_key: string;
|
||||
frontend_url: string;
|
||||
} | null;
|
||||
}
|
||||
};
|
||||
|
||||
export interface PerformanceCriteria extends BaseCourseWagtailPage {
|
||||
readonly content_type: "competence.PerformanceCriteria";
|
||||
readonly competence_id: string;
|
||||
readonly circle: CircleLight;
|
||||
readonly course_category: CourseCategory;
|
||||
readonly learning_unit: BaseCourseWagtailPage & {
|
||||
evaluate_url: string;
|
||||
export type PerformanceCriteria = Omit<PerformanceCriteriaObjectType, "content_type"> &
|
||||
Completable & {
|
||||
readonly content_type: "competence.PerformanceCriteria";
|
||||
circle: CircleLight;
|
||||
};
|
||||
}
|
||||
|
||||
export interface CompetencePage extends BaseCourseWagtailPage {
|
||||
readonly content_type: "competence.CompetencePage";
|
||||
readonly competence_id: string;
|
||||
readonly children: PerformanceCriteria[];
|
||||
}
|
||||
|
||||
export interface CompetenceProfilePage extends BaseCourseWagtailPage {
|
||||
readonly content_type: "competence.CompetenceProfilePage";
|
||||
readonly course: Course;
|
||||
readonly circles: CircleLight[];
|
||||
readonly children: CompetencePage[];
|
||||
}
|
||||
export type ActionCompetence = Omit<
|
||||
ActionCompetenceObjectType,
|
||||
"content_type" | "performance_criteria"
|
||||
> & {
|
||||
readonly content_type: "competence.ActionCompetence";
|
||||
performance_criteria: PerformanceCriteria[];
|
||||
};
|
||||
|
||||
export interface CompetenceCertificateAssignment extends BaseCourseWagtailPage {
|
||||
assignment_type: "CASEWORK" | "EDONIQ_TEST";
|
||||
|
|
@ -471,47 +424,6 @@ export interface CircleDocument {
|
|||
};
|
||||
}
|
||||
|
||||
export interface CourseSessionAttendanceCourse {
|
||||
id: string;
|
||||
due_date: SimpleDueDate;
|
||||
location: string;
|
||||
trainer: string;
|
||||
circle_title: string;
|
||||
learning_content: {
|
||||
id: string;
|
||||
title: string;
|
||||
circle: CircleLight;
|
||||
};
|
||||
}
|
||||
|
||||
export interface CourseSessionAssignment {
|
||||
id: string;
|
||||
submission_deadline: SimpleDueDate;
|
||||
evaluation_deadline: SimpleDueDate;
|
||||
learning_content: {
|
||||
id: string;
|
||||
content_assignment: {
|
||||
id: string;
|
||||
title: string;
|
||||
assignment_type: AssignmentType;
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
export interface CourseSessionEdoniqTest {
|
||||
id: number;
|
||||
course_session_id: string;
|
||||
deadline: SimpleDueDate;
|
||||
learning_content: {
|
||||
id: string;
|
||||
content_assignment: {
|
||||
id: string;
|
||||
title: string;
|
||||
assignment_type: AssignmentType;
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
export interface CourseSession {
|
||||
id: string;
|
||||
created_at: string;
|
||||
|
|
@ -523,46 +435,15 @@ export interface CourseSession {
|
|||
due_dates: DueDate[];
|
||||
}
|
||||
|
||||
export type Role = "MEMBER" | "EXPERT" | "TUTOR";
|
||||
export type Role = CourseCourseSessionUserRoleChoices;
|
||||
|
||||
export interface CourseSessionUser {
|
||||
user_id: string;
|
||||
first_name: string;
|
||||
last_name: string;
|
||||
email: string;
|
||||
avatar_url: string;
|
||||
role: Role;
|
||||
circles: {
|
||||
id: string;
|
||||
title: string;
|
||||
slug: string;
|
||||
translation_key: string;
|
||||
}[];
|
||||
}
|
||||
export type CourseSessionUser = CourseSessionUserObjectsType;
|
||||
|
||||
export interface ExpertSessionUser extends CourseSessionUser {
|
||||
role: "EXPERT";
|
||||
circles: {
|
||||
id: string;
|
||||
title: string;
|
||||
slug: string;
|
||||
translation_key: string;
|
||||
}[];
|
||||
}
|
||||
|
||||
export interface CourseSessionDetail {
|
||||
id: string;
|
||||
title: string;
|
||||
course: {
|
||||
id: string;
|
||||
title: string;
|
||||
slug: string;
|
||||
};
|
||||
assignments: CourseSessionAssignment[];
|
||||
attendance_courses: CourseSessionAttendanceCourse[];
|
||||
edoniq_tests: CourseSessionEdoniqTest[];
|
||||
users: CourseSessionUser[];
|
||||
}
|
||||
export type CourseSessionDetail = CourseSessionObjectType;
|
||||
|
||||
// document upload
|
||||
export interface DocumentUploadData {
|
||||
|
|
@ -641,37 +522,11 @@ export interface AssignmentTaskCompletionData {
|
|||
};
|
||||
}
|
||||
|
||||
export interface AssignmentCompletion {
|
||||
id: string;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
submitted_at: string;
|
||||
evaluation_submitted_at: string | null;
|
||||
assignment_user: string;
|
||||
assignment: number;
|
||||
course_session: number;
|
||||
completion_status: AssignmentCompletionStatus;
|
||||
evaluation_user: string | null;
|
||||
export type AssignmentCompletion = AssignmentCompletionObjectType & {
|
||||
// assignment_user: Pick<UserObjectType, "id" | "__typename">;
|
||||
// evaluation_user: Pick<UserObjectType, "id" | "__typename">;
|
||||
completion_data: AssignmentCompletionData;
|
||||
task_completion_data: AssignmentTaskCompletionData;
|
||||
edoniq_extended_time_flag: boolean;
|
||||
evaluation_points: number | null;
|
||||
evaluation_max_points: number | null;
|
||||
evaluation_passed: boolean | null;
|
||||
}
|
||||
|
||||
export type UpsertUserAssignmentCompletion = {
|
||||
assignment_id: string;
|
||||
course_session_id: string;
|
||||
completion_status: AssignmentCompletionStatus;
|
||||
completion_data: AssignmentCompletionData;
|
||||
};
|
||||
|
||||
export type EvaluationCompletionData = UpsertUserAssignmentCompletion & {
|
||||
assignment_user_id: string;
|
||||
evaluation_points?: number;
|
||||
evaluation_max_points?: number;
|
||||
evaluation_passed?: boolean;
|
||||
};
|
||||
|
||||
export interface UserAssignmentCompletionStatus {
|
||||
|
|
|
|||
|
|
@ -5,6 +5,10 @@ export function assertUnreachable(msg: string): never {
|
|||
throw new Error("Didn't expect to get here, " + msg);
|
||||
}
|
||||
|
||||
export function stringifyParse(obj: any): any {
|
||||
return JSON.parse(JSON.stringify(obj));
|
||||
}
|
||||
|
||||
function createCourseUrl(courseSlug: string | undefined, specificSub: string): string {
|
||||
if (!courseSlug) {
|
||||
return "/";
|
||||
|
|
|
|||
|
|
@ -43,6 +43,7 @@ describe("circle.cy.js", () => {
|
|||
"Verschaffe dir einen Überblick"
|
||||
);
|
||||
cy.get('[data-cy="complete-and-continue"]').click({ force: true });
|
||||
cy.get('[data-cy="circle-title"]').should("contain", "Fahrzeug");
|
||||
|
||||
cy.get('[data-cy="ls-continue-button"]').click();
|
||||
cy.get('[data-cy="lc-title"]').should(
|
||||
|
|
@ -50,6 +51,7 @@ describe("circle.cy.js", () => {
|
|||
"Handlungsfeld «Fahrzeug»"
|
||||
);
|
||||
cy.get('[data-cy="complete-and-continue"]').click({ force: true });
|
||||
cy.get('[data-cy="circle-title"]').should("contain", "Fahrzeug");
|
||||
|
||||
cy.get(
|
||||
'[data-cy="test-lehrgang-lp-circle-fahrzeug-lc-verschaffe-dir-einen-überblick-checkbox"]'
|
||||
|
|
@ -87,4 +89,35 @@ describe("circle.cy.js", () => {
|
|||
cy.get('[data-cy="close-learning-content"]').click();
|
||||
cy.get('[data-cy="circle-title"]').should("contain", "Fahrzeug");
|
||||
});
|
||||
|
||||
it("checks number of sequences and contents", () => {
|
||||
cy.get('[data-cy="lp-learning-sequence"]').should("have.length", 3);
|
||||
cy.get('[data-cy="lp-learning-sequence"]')
|
||||
.first()
|
||||
.should("contain", "Vorbereitung");
|
||||
cy.get('[data-cy="lp-learning-sequence"]')
|
||||
.eq(1)
|
||||
.should("contain", "Training");
|
||||
cy.get('[data-cy="lp-learning-sequence"]')
|
||||
.last()
|
||||
.should("contain", "Transfer");
|
||||
|
||||
cy.get('[data-cy="lp-learning-content"]').should("have.length", 10);
|
||||
cy.get('[data-cy="lp-learning-content"]')
|
||||
.first()
|
||||
.should("contain", "Verschaffe dir einen Überblick");
|
||||
cy.get('[data-cy="lp-learning-content"]')
|
||||
.eq(4)
|
||||
.should("contain", "Präsenzkurs Fahrzeug");
|
||||
cy.get('[data-cy="lp-learning-content"]')
|
||||
.eq(7)
|
||||
.should("contain", "Reflexion");
|
||||
cy.get('[data-cy="lp-learning-content"]')
|
||||
.last()
|
||||
.should("contain", "Feedback");
|
||||
|
||||
cy.visit("/course/test-lehrgang/learn/reisen");
|
||||
cy.get('[data-cy="lp-learning-sequence"]').should("have.length", 3);
|
||||
cy.get('[data-cy="lp-learning-content"]').should("have.length", 7);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -52,4 +52,14 @@ describe("learningPath.cy.js", () => {
|
|||
.click();
|
||||
cy.get('[data-cy="circle-title"]').should("contain", "Reisen");
|
||||
});
|
||||
|
||||
it("checks contents", () => {
|
||||
cy.get('[data-cy="lp-topic"]').should("have.length", 2);
|
||||
cy.get('[data-cy="lp-topic"]').first().should("contain", "Circle ÜK");
|
||||
cy.get('[data-cy="lp-topic"]').eq(1).should("contain", "Circle VV");
|
||||
|
||||
cy.get(".cy-lp-circle").should("have.length", 2);
|
||||
cy.get(".cy-lp-circle").first().should("contain", "Fahrzeug");
|
||||
cy.get(".cy-lp-circle").eq(1).should("contain", "Reisen");
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,16 +1,20 @@
|
|||
import graphene
|
||||
from graphene import List
|
||||
from graphene_django import DjangoObjectType
|
||||
|
||||
from vbv_lernwelt.assignment.graphql.types import AssignmentObjectType
|
||||
from vbv_lernwelt.competence.models import (
|
||||
ActionCompetence,
|
||||
CompetenceCertificate,
|
||||
CompetenceCertificateList,
|
||||
PerformanceCriteria,
|
||||
)
|
||||
from vbv_lernwelt.course.graphql.interfaces import CoursePageInterface
|
||||
from vbv_lernwelt.learnpath.graphql.types import LearningUnitObjectType
|
||||
|
||||
|
||||
class CompetenceCertificateObjectType(DjangoObjectType):
|
||||
assignments = List(AssignmentObjectType)
|
||||
assignments = List(graphene.NonNull(AssignmentObjectType), required=True)
|
||||
|
||||
class Meta:
|
||||
model = CompetenceCertificate
|
||||
|
|
@ -22,11 +26,40 @@ class CompetenceCertificateObjectType(DjangoObjectType):
|
|||
|
||||
|
||||
class CompetenceCertificateListObjectType(DjangoObjectType):
|
||||
competence_certificates = List(CompetenceCertificateObjectType)
|
||||
competence_certificates = List(
|
||||
graphene.NonNull(CompetenceCertificateObjectType), required=True
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = CompetenceCertificateList
|
||||
interfaces = (CoursePageInterface,)
|
||||
fields = []
|
||||
|
||||
def resolve_competence_certificates(self, info):
|
||||
return CompetenceCertificate.objects.child_of(self)
|
||||
|
||||
|
||||
class PerformanceCriteriaObjectType(DjangoObjectType):
|
||||
learning_unit = graphene.Field(LearningUnitObjectType)
|
||||
|
||||
class Meta:
|
||||
model = PerformanceCriteria
|
||||
interfaces = (CoursePageInterface,)
|
||||
fields = ["competence_id"]
|
||||
|
||||
def resolve_learning_unit(self: PerformanceCriteria, info):
|
||||
return self.learning_unit
|
||||
|
||||
|
||||
class ActionCompetenceObjectType(DjangoObjectType):
|
||||
performance_criteria = graphene.List(
|
||||
graphene.NonNull(PerformanceCriteriaObjectType), required=True
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = ActionCompetence
|
||||
interfaces = (CoursePageInterface,)
|
||||
fields = ["competence_id"]
|
||||
|
||||
def resolve_performance_criteria(self, info):
|
||||
return self.get_children().specific()
|
||||
|
|
|
|||
|
|
@ -105,6 +105,9 @@ class ActionCompetence(CourseBasePage):
|
|||
use_json_field=True,
|
||||
)
|
||||
|
||||
def get_frontend_url(self):
|
||||
return f""
|
||||
|
||||
content_panels = [
|
||||
FieldPanel("title"),
|
||||
FieldPanel("competence_id"),
|
||||
|
|
@ -141,6 +144,9 @@ class PerformanceCriteria(CourseBasePage):
|
|||
FieldPanel("learning_unit"),
|
||||
]
|
||||
|
||||
def get_frontend_url(self):
|
||||
return ""
|
||||
|
||||
def save(self, clean=True, user=None, log_action=False, **kwargs):
|
||||
profile_parent = (
|
||||
self.get_ancestors().exact_type(ActionCompetenceListPage).last()
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ from graphene_django import DjangoObjectType
|
|||
from vbv_lernwelt.core.models import User
|
||||
|
||||
|
||||
class UserType(DjangoObjectType):
|
||||
class UserObjectType(DjangoObjectType):
|
||||
class Meta:
|
||||
model = User
|
||||
fields = (
|
||||
|
|
|
|||
|
|
@ -354,15 +354,15 @@ In diesem Circle erfährst du wie die überbetrieblichen Kurse aufgebaut sind. Z
|
|||
],
|
||||
)
|
||||
LearningUnitFactory(title="Kompetenznachweis", title_hidden=True, parent=circle)
|
||||
LearningContentEdoniqTestFactory(
|
||||
title="Wissens- und Verständnisfragen",
|
||||
parent=circle,
|
||||
description=RichText(
|
||||
"<p>Folgender Test mit Wissens- und Verständnisfragen ist Teil des Kompetenznachweises. Der Test kann nur einmal durchgeführt werden und ist notenrelevant.</p>"
|
||||
),
|
||||
checkbox_text="Hiermit bestätige ich, dass ich die Anweisungen verstanden und die Redlichkeitserklärung akzeptiert habe.",
|
||||
test_url="https://exam.vbv-afa.ch/e-tutor/v4/user/course/pre_course_object?aid=1689096897473,2147466097",
|
||||
)
|
||||
# LearningContentEdoniqTestFactory(
|
||||
# title="Wissens- und Verständnisfragen",
|
||||
# parent=circle,
|
||||
# description=RichText(
|
||||
# "<p>Folgender Test mit Wissens- und Verständnisfragen ist Teil des Kompetenznachweises. Der Test kann nur einmal durchgeführt werden und ist notenrelevant.</p>"
|
||||
# ),
|
||||
# checkbox_text="Hiermit bestätige ich, dass ich die Anweisungen verstanden und die Redlichkeitserklärung akzeptiert habe.",
|
||||
# test_url="https://exam.vbv-afa.ch/e-tutor/v4/user/course/pre_course_object?aid=1689096897473,2147466097",
|
||||
# )
|
||||
LearningUnitFactory(title="Feedback", title_hidden=True, parent=circle)
|
||||
LearningContentFeedbackFactory(
|
||||
parent=circle,
|
||||
|
|
|
|||
|
|
@ -5,14 +5,13 @@ from vbv_lernwelt.learnpath.models import Circle
|
|||
|
||||
|
||||
class CoursePageInterface(graphene.Interface):
|
||||
id = graphene.ID()
|
||||
title = graphene.String()
|
||||
slug = graphene.String()
|
||||
content_type = graphene.String()
|
||||
live = graphene.Boolean()
|
||||
translation_key = graphene.String()
|
||||
frontend_url = graphene.String()
|
||||
circle = graphene.Field("vbv_lernwelt.learnpath.graphql.types.CircleObjectType")
|
||||
id = graphene.ID(required=True)
|
||||
title = graphene.String(required=True)
|
||||
slug = graphene.String(required=True)
|
||||
content_type = graphene.String(required=True)
|
||||
live = graphene.Boolean(required=True)
|
||||
translation_key = graphene.String(required=True)
|
||||
frontend_url = graphene.String(required=True)
|
||||
course = graphene.Field("vbv_lernwelt.course.graphql.types.CourseObjectType")
|
||||
|
||||
def resolve_frontend_url(self, info):
|
||||
|
|
@ -21,11 +20,5 @@ class CoursePageInterface(graphene.Interface):
|
|||
def resolve_content_type(self, info):
|
||||
return get_django_content_type(self)
|
||||
|
||||
def resolve_circle(self, info):
|
||||
circle = self.get_ancestors().exact_type(Circle).first()
|
||||
if circle:
|
||||
return circle.specific
|
||||
return None
|
||||
|
||||
def resolve_course(self, info):
|
||||
return self.get_course()
|
||||
|
|
|
|||
|
|
@ -1,22 +1,62 @@
|
|||
import graphene
|
||||
from django.db.models import Q
|
||||
from graphql import GraphQLError
|
||||
|
||||
from vbv_lernwelt.course.graphql.types import CourseObjectType, CourseSessionObjectType
|
||||
from vbv_lernwelt.course.models import Course, CourseSession
|
||||
from vbv_lernwelt.course.permissions import has_course_access
|
||||
from vbv_lernwelt.learnpath.graphql.types import (
|
||||
LearningContentAssignmentObjectType,
|
||||
LearningContentAttendanceCourseObjectType,
|
||||
LearningContentDocumentListObjectType,
|
||||
LearningContentEdoniqTestObjectType,
|
||||
LearningContentFeedbackObjectType,
|
||||
LearningContentLearningModuleObjectType,
|
||||
LearningContentMediaLibraryObjectType,
|
||||
LearningContentPlaceholderObjectType,
|
||||
LearningContentRichTextObjectType,
|
||||
LearningContentVideoObjectType,
|
||||
)
|
||||
|
||||
|
||||
class CourseQuery(graphene.ObjectType):
|
||||
course = graphene.Field(CourseObjectType, id=graphene.ID())
|
||||
course = graphene.Field(
|
||||
CourseObjectType,
|
||||
id=graphene.ID(),
|
||||
slug=graphene.String(),
|
||||
)
|
||||
course_session = graphene.Field(CourseSessionObjectType, id=graphene.ID())
|
||||
|
||||
def resolve_course(root, info, id):
|
||||
course = Course.objects.get(pk=id)
|
||||
def resolve_course(self, info, id=None, slug=None):
|
||||
if id is None and slug is None:
|
||||
raise GraphQLError("Either 'id', 'slug' must be provided.")
|
||||
course = Course.objects.get(Q(id=id) | Q(slug=slug))
|
||||
if has_course_access(info.context.user, course):
|
||||
return course
|
||||
raise PermissionError("You do not have access to this course")
|
||||
|
||||
def resolve_course_session(root, info, id):
|
||||
def resolve_course_session(self, info, id):
|
||||
course_session = CourseSession.objects.get(pk=id)
|
||||
if has_course_access(info.context.user, course_session.course):
|
||||
return course_session
|
||||
raise PermissionError("You do not have access to this course session")
|
||||
|
||||
# dummy import, so that graphene recognizes the types
|
||||
learning_content_media_library = graphene.Field(
|
||||
LearningContentMediaLibraryObjectType
|
||||
)
|
||||
learning_content_assignment = graphene.Field(LearningContentAssignmentObjectType)
|
||||
learning_content_attendance_course = graphene.Field(
|
||||
LearningContentAttendanceCourseObjectType
|
||||
)
|
||||
learning_content_feedback = graphene.Field(LearningContentFeedbackObjectType)
|
||||
learning_content_learning_module = graphene.Field(
|
||||
LearningContentLearningModuleObjectType
|
||||
)
|
||||
learning_content_placeholder = graphene.Field(LearningContentPlaceholderObjectType)
|
||||
learning_content_rich_text = graphene.Field(LearningContentRichTextObjectType)
|
||||
learning_content_test = graphene.Field(LearningContentEdoniqTestObjectType)
|
||||
learning_content_video = graphene.Field(LearningContentVideoObjectType)
|
||||
learning_content_document_list = graphene.Field(
|
||||
LearningContentDocumentListObjectType
|
||||
)
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ from graphene_django import DjangoObjectType
|
|||
from graphql import GraphQLError
|
||||
from rest_framework.exceptions import PermissionDenied
|
||||
|
||||
from vbv_lernwelt.competence.graphql.types import ActionCompetenceObjectType
|
||||
from vbv_lernwelt.course.models import (
|
||||
CircleDocument,
|
||||
Course,
|
||||
|
|
@ -84,30 +85,40 @@ def resolve_course_page(
|
|||
|
||||
|
||||
class CourseObjectType(DjangoObjectType):
|
||||
learning_path = graphene.Field(LearningPathObjectType)
|
||||
learning_path = graphene.Field(LearningPathObjectType, required=True)
|
||||
action_competences = graphene.List(
|
||||
graphene.NonNull(ActionCompetenceObjectType), required=True
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = Course
|
||||
fields = ("id", "title", "category_name", "slug", "learning_path")
|
||||
fields = ("id", "title", "category_name", "slug")
|
||||
|
||||
def resolve_learning_path(self, info):
|
||||
return self.get_learning_path()
|
||||
@staticmethod
|
||||
def resolve_learning_path(root: Course, info):
|
||||
return root.get_learning_path()
|
||||
|
||||
@staticmethod
|
||||
def resolve_action_competences(root: Course, info):
|
||||
return root.get_action_competences()
|
||||
|
||||
|
||||
class CourseSessionUserExpertCircleType(ObjectType):
|
||||
id = graphene.ID()
|
||||
title = graphene.String()
|
||||
slug = graphene.String()
|
||||
id = graphene.ID(required=True)
|
||||
title = graphene.String(required=True)
|
||||
slug = graphene.String(required=True)
|
||||
|
||||
|
||||
class CourseSessionUserObjectsType(DjangoObjectType):
|
||||
user_id = graphene.UUID()
|
||||
first_name = graphene.String()
|
||||
last_name = graphene.String()
|
||||
email = graphene.String()
|
||||
avatar_url = graphene.String()
|
||||
role = graphene.String()
|
||||
circles = graphene.List(CourseSessionUserExpertCircleType)
|
||||
user_id = graphene.UUID(required=True)
|
||||
first_name = graphene.String(required=True)
|
||||
last_name = graphene.String(required=True)
|
||||
email = graphene.String(required=True)
|
||||
avatar_url = graphene.String(required=True)
|
||||
# role = graphene.String(required=True)
|
||||
circles = graphene.List(
|
||||
graphene.NonNull(CourseSessionUserExpertCircleType), required=True
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = CourseSessionUser
|
||||
|
|
@ -166,12 +177,16 @@ class CircleDocumentObjectType(DjangoObjectType):
|
|||
|
||||
|
||||
class CourseSessionObjectType(DjangoObjectType):
|
||||
attendance_courses = graphene.List(CourseSessionAttendanceCourseObjectType)
|
||||
assignments = graphene.List(CourseSessionAssignmentObjectType)
|
||||
edoniq_tests = graphene.List(CourseSessionEdoniqTestObjectType)
|
||||
documents = graphene.List(CircleDocumentObjectType)
|
||||
|
||||
users = graphene.List(CourseSessionUserObjectsType)
|
||||
attendance_courses = graphene.List(
|
||||
graphene.NonNull(CourseSessionAttendanceCourseObjectType), required=True
|
||||
)
|
||||
assignments = graphene.List(
|
||||
graphene.NonNull(CourseSessionAssignmentObjectType), required=True
|
||||
)
|
||||
edoniq_tests = graphene.List(
|
||||
graphene.NonNull(CourseSessionEdoniqTestObjectType), required=True
|
||||
)
|
||||
users = graphene.List(graphene.NonNull(CourseSessionUserObjectsType), required=True)
|
||||
|
||||
class Meta:
|
||||
model = CourseSession
|
||||
|
|
@ -195,10 +210,5 @@ class CourseSessionObjectType(DjangoObjectType):
|
|||
def resolve_edoniq_tests(self, info):
|
||||
return CourseSessionEdoniqTest.objects.filter(course_session=self)
|
||||
|
||||
def resolve_documents(self, info):
|
||||
return CircleDocument.objects.filter(
|
||||
course_session=self, file__upload_finished_at__isnull=False
|
||||
)
|
||||
|
||||
def resolve_users(self, info):
|
||||
return CourseSessionUser.objects.filter(course_session_id=self.id).distinct()
|
||||
|
|
|
|||
|
|
@ -27,26 +27,18 @@ class Course(models.Model):
|
|||
def get_course_url(self):
|
||||
return f"/course/{self.slug}"
|
||||
|
||||
def get_cockpit_url(self):
|
||||
return f"/{self.get_course_url()}/cockpit"
|
||||
|
||||
def get_learning_path(self):
|
||||
from vbv_lernwelt.learnpath.models import LearningPath
|
||||
|
||||
return self.coursepage.get_children().exact_type(LearningPath).first().specific
|
||||
|
||||
def get_learning_path_url(self):
|
||||
return self.get_learning_path().get_frontend_url()
|
||||
def get_action_competences(self):
|
||||
from vbv_lernwelt.competence.models import ActionCompetence
|
||||
|
||||
def get_cockpit_url(self):
|
||||
return self.get_learning_path().get_cockpit_url()
|
||||
|
||||
def get_competence_url(self):
|
||||
from vbv_lernwelt.competence.models import ActionCompetenceListPage
|
||||
|
||||
competence_page = (
|
||||
self.coursepage.get_descendants()
|
||||
.exact_type(ActionCompetenceListPage)
|
||||
.first()
|
||||
)
|
||||
return competence_page.specific.get_frontend_url()
|
||||
return self.coursepage.get_descendants().exact_type(ActionCompetence).specific()
|
||||
|
||||
def get_media_library_url(self):
|
||||
from vbv_lernwelt.media_library.models import MediaLibraryPage
|
||||
|
|
|
|||
|
|
@ -33,13 +33,14 @@ class CourseSessionAttendanceCourseObjectType(DjangoObjectType):
|
|||
AttendanceUserObjectType, source="attendance_user_list"
|
||||
)
|
||||
due_date = graphene.Field(DueDateObjectType)
|
||||
learning_content = graphene.Field(LearningContentAttendanceCourseObjectType)
|
||||
learning_content = graphene.Field(
|
||||
LearningContentAttendanceCourseObjectType, required=True
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = CourseSessionAttendanceCourse
|
||||
fields = (
|
||||
"id",
|
||||
"course_session_id",
|
||||
"learning_content",
|
||||
"location",
|
||||
"trainer",
|
||||
|
|
@ -53,17 +54,18 @@ class CourseSessionAttendanceCourseObjectType(DjangoObjectType):
|
|||
|
||||
|
||||
class CourseSessionAssignmentObjectType(DjangoObjectType):
|
||||
course_session_id = graphene.ID(source="course_session_id")
|
||||
learning_content_id = graphene.ID(source="learning_content_id")
|
||||
course_session_id = graphene.ID(source="course_session_id", required=True)
|
||||
learning_content_id = graphene.ID(source="learning_content_id", required=True)
|
||||
submission_deadline = graphene.Field(DueDateObjectType)
|
||||
evaluation_deadline = graphene.Field(DueDateObjectType)
|
||||
learning_content = graphene.Field(LearningContentAssignmentObjectType)
|
||||
learning_content = graphene.Field(
|
||||
LearningContentAssignmentObjectType, required=True
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = CourseSessionAssignment
|
||||
fields = (
|
||||
"id",
|
||||
"course_session_id",
|
||||
"learning_content",
|
||||
"submission_deadline",
|
||||
"evaluation_deadline",
|
||||
|
|
@ -71,16 +73,18 @@ class CourseSessionAssignmentObjectType(DjangoObjectType):
|
|||
|
||||
|
||||
class CourseSessionEdoniqTestObjectType(DjangoObjectType):
|
||||
course_session_id = graphene.ID(source="course_session_id")
|
||||
learning_content_id = graphene.ID(source="learning_content_id")
|
||||
course_session_id = graphene.ID(source="course_session_id", required=True)
|
||||
learning_content_id = graphene.ID(source="learning_content_id", required=True)
|
||||
|
||||
deadline = graphene.Field(DueDateObjectType)
|
||||
learning_content = graphene.Field(LearningContentEdoniqTestObjectType)
|
||||
learning_content = graphene.Field(
|
||||
LearningContentEdoniqTestObjectType, required=True
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = CourseSessionEdoniqTest
|
||||
fields = (
|
||||
"id",
|
||||
"course_session_id",
|
||||
"learning_content",
|
||||
"deadline",
|
||||
)
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ from rest_framework.test import APITestCase
|
|||
from vbv_lernwelt.core.create_default_users import create_default_users
|
||||
from vbv_lernwelt.core.models import User
|
||||
from vbv_lernwelt.course.creators.test_course import create_test_course
|
||||
from vbv_lernwelt.course.models import CourseSession, CourseSessionUser
|
||||
from vbv_lernwelt.course.models import CourseSession
|
||||
from vbv_lernwelt.feedback.factories import FeedbackResponseFactory
|
||||
from vbv_lernwelt.feedback.models import FeedbackResponse
|
||||
from vbv_lernwelt.learnpath.models import Circle
|
||||
|
|
|
|||
|
|
@ -13,7 +13,6 @@ from vbv_lernwelt.course.models import CourseCategory, CoursePage
|
|||
from vbv_lernwelt.learnpath.tests.learning_path_factories import (
|
||||
CircleFactory,
|
||||
LearningContentAssignmentFactory,
|
||||
LearningContentEdoniqTestFactory,
|
||||
LearningContentFeedbackFactory,
|
||||
LearningContentLearningModuleFactory,
|
||||
LearningContentMediaLibraryFactory,
|
||||
|
|
@ -362,14 +361,13 @@ def create_circle_fahrzeug(lp, title="Fahrzeug"):
|
|||
# checkbox_text="Hiermit bestätige ich, dass ich die Anweisungen verstanden und die Redlichkeitserklärung akzeptiert habe.",
|
||||
# content_url="https://s3.eu-central-1.amazonaws.com/myvbv-wbt.iterativ.ch/fachcheck-fahrzeug-xapi-LFv8YiyM/index.html#/",
|
||||
# )
|
||||
LearningContentAssignmentFactory(
|
||||
title="Reflexion",
|
||||
assignment_type="REFLECTION",
|
||||
parent=circle,
|
||||
content_assignment=Assignment.objects.get(
|
||||
slug__startswith=f"{circle.get_course().slug}-assignment-reflexion"
|
||||
),
|
||||
),
|
||||
# LearningContentAssignmentFactory(
|
||||
# title="Reflexion",
|
||||
# assignment_type="REFLECTION",
|
||||
# content_assignment=Assignment.objects.get(
|
||||
# slug__startswith=f"{circle.get_course().slug}-assignment-reflexion"
|
||||
# ),
|
||||
# ),
|
||||
LearningContentFeedbackFactory(
|
||||
parent=circle,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -1,21 +1,8 @@
|
|||
import graphene
|
||||
|
||||
from vbv_lernwelt.course.graphql.types import resolve_course_page
|
||||
from vbv_lernwelt.learnpath.graphql.types import (
|
||||
CircleObjectType,
|
||||
LearningContentAssignmentObjectType,
|
||||
LearningContentAttendanceCourseObjectType,
|
||||
LearningContentDocumentListObjectType,
|
||||
LearningContentEdoniqTestObjectType,
|
||||
LearningContentFeedbackObjectType,
|
||||
LearningContentLearningModuleObjectType,
|
||||
LearningContentMediaLibraryObjectType,
|
||||
LearningContentPlaceholderObjectType,
|
||||
LearningContentRichTextObjectType,
|
||||
LearningContentVideoObjectType,
|
||||
LearningPathObjectType,
|
||||
)
|
||||
from vbv_lernwelt.learnpath.models import Circle, LearningPath
|
||||
from vbv_lernwelt.learnpath.graphql.types import LearningPathObjectType
|
||||
from vbv_lernwelt.learnpath.models import LearningPath
|
||||
|
||||
|
||||
class LearningPathQuery:
|
||||
|
|
@ -26,10 +13,6 @@ class LearningPathQuery:
|
|||
course_id=graphene.ID(),
|
||||
course_slug=graphene.String(),
|
||||
)
|
||||
circle = graphene.Field(CircleObjectType, id=graphene.ID(), slug=graphene.String())
|
||||
|
||||
def resolve_circle(root, info, id=None, slug=None):
|
||||
return resolve_course_page(Circle, root, info, id=id, slug=slug)
|
||||
|
||||
def resolve_learning_path(
|
||||
root, info, id=None, slug=None, course_id=None, course_slug=None
|
||||
|
|
@ -43,53 +26,3 @@ class LearningPathQuery:
|
|||
course_id=course_id,
|
||||
course_slug=course_slug,
|
||||
)
|
||||
|
||||
# dummy import, so that graphene recognizes the types
|
||||
learning_content_media_library = graphene.Field(
|
||||
LearningContentMediaLibraryObjectType
|
||||
)
|
||||
learning_content_assignment = graphene.Field(LearningContentAssignmentObjectType)
|
||||
learning_content_attendance_course = graphene.Field(
|
||||
LearningContentAttendanceCourseObjectType
|
||||
)
|
||||
learning_content_feedback = graphene.Field(LearningContentFeedbackObjectType)
|
||||
learning_content_learning_module = graphene.Field(
|
||||
LearningContentLearningModuleObjectType
|
||||
)
|
||||
learning_content_placeholder = graphene.Field(LearningContentPlaceholderObjectType)
|
||||
learning_content_rich_text = graphene.Field(LearningContentRichTextObjectType)
|
||||
learning_content_test = graphene.Field(LearningContentEdoniqTestObjectType)
|
||||
learning_content_video = graphene.Field(LearningContentVideoObjectType)
|
||||
learning_content_document_list = graphene.Field(
|
||||
LearningContentDocumentListObjectType
|
||||
)
|
||||
|
||||
def resolve_learning_content_media_library(self, info):
|
||||
return None
|
||||
|
||||
def resolve_learning_content_assignment(self, info):
|
||||
return None
|
||||
|
||||
def resolve_learning_content_attendance_course(self, info):
|
||||
return None
|
||||
|
||||
def resolve_learning_content_feedback(self, info):
|
||||
return None
|
||||
|
||||
def resolve_learning_content_learning_module(self, info):
|
||||
return None
|
||||
|
||||
def resolve_learning_content_placeholder(self, info):
|
||||
return None
|
||||
|
||||
def resolve_learning_content_rich_text(self, info):
|
||||
return None
|
||||
|
||||
def resolve_learning_content_test(self, info):
|
||||
return None
|
||||
|
||||
def resolve_learning_content_video(self, info):
|
||||
return None
|
||||
|
||||
def resolve_learning_content_document_list(self, info):
|
||||
return None
|
||||
|
|
|
|||
|
|
@ -25,10 +25,22 @@ from vbv_lernwelt.learnpath.models import (
|
|||
logger = structlog.get_logger(__name__)
|
||||
|
||||
|
||||
class CircleLightObjectType(graphene.ObjectType):
|
||||
id = graphene.ID(required=True)
|
||||
title = graphene.String(required=True)
|
||||
slug = graphene.String(required=True)
|
||||
|
||||
|
||||
class LearningContentInterface(CoursePageInterface):
|
||||
minutes = graphene.Int()
|
||||
description = graphene.String()
|
||||
content = graphene.String()
|
||||
description = graphene.String(required=True)
|
||||
content_url = graphene.String(required=True)
|
||||
can_user_self_toggle_course_completion = graphene.Boolean(required=True)
|
||||
circle = graphene.Field(CircleLightObjectType)
|
||||
|
||||
@staticmethod
|
||||
def resolve_circle(root, info):
|
||||
return root.get_parent_circle()
|
||||
|
||||
@classmethod
|
||||
def resolve_type(cls, instance, info):
|
||||
|
|
@ -63,96 +75,156 @@ class LearningContentInterface(CoursePageInterface):
|
|||
class LearningContentAttendanceCourseObjectType(DjangoObjectType):
|
||||
class Meta:
|
||||
model = LearningContentAttendanceCourse
|
||||
interfaces = (LearningContentInterface,)
|
||||
interfaces = (
|
||||
CoursePageInterface,
|
||||
LearningContentInterface,
|
||||
)
|
||||
fields = []
|
||||
|
||||
|
||||
class LearningContentVideoObjectType(DjangoObjectType):
|
||||
class Meta:
|
||||
model = LearningContentVideo
|
||||
interfaces = (LearningContentInterface,)
|
||||
interfaces = (
|
||||
CoursePageInterface,
|
||||
LearningContentInterface,
|
||||
)
|
||||
fields = []
|
||||
|
||||
|
||||
class LearningContentPlaceholderObjectType(DjangoObjectType):
|
||||
class Meta:
|
||||
model = LearningContentPlaceholder
|
||||
interfaces = (LearningContentInterface,)
|
||||
interfaces = (
|
||||
CoursePageInterface,
|
||||
LearningContentInterface,
|
||||
)
|
||||
fields = []
|
||||
|
||||
|
||||
class LearningContentFeedbackObjectType(DjangoObjectType):
|
||||
class Meta:
|
||||
model = LearningContentFeedback
|
||||
interfaces = (LearningContentInterface,)
|
||||
interfaces = (
|
||||
CoursePageInterface,
|
||||
LearningContentInterface,
|
||||
)
|
||||
fields = []
|
||||
|
||||
|
||||
class LearningContentLearningModuleObjectType(DjangoObjectType):
|
||||
class Meta:
|
||||
model = LearningContentLearningModule
|
||||
interfaces = (LearningContentInterface,)
|
||||
interfaces = (
|
||||
CoursePageInterface,
|
||||
LearningContentInterface,
|
||||
)
|
||||
fields = []
|
||||
|
||||
|
||||
class LearningContentMediaLibraryObjectType(DjangoObjectType):
|
||||
class Meta:
|
||||
model = LearningContentMediaLibrary
|
||||
interfaces = (LearningContentInterface,)
|
||||
interfaces = (
|
||||
CoursePageInterface,
|
||||
LearningContentInterface,
|
||||
)
|
||||
fields = []
|
||||
|
||||
|
||||
class LearningContentEdoniqTestObjectType(DjangoObjectType):
|
||||
competence_certificate = graphene.Field(
|
||||
"vbv_lernwelt.competence.graphql.types.CompetenceCertificateObjectType",
|
||||
)
|
||||
has_extended_time_test = graphene.Boolean(required=True)
|
||||
|
||||
class Meta:
|
||||
model = LearningContentEdoniqTest
|
||||
interfaces = (LearningContentInterface,)
|
||||
fields = [
|
||||
"content_assignment",
|
||||
]
|
||||
interfaces = (
|
||||
CoursePageInterface,
|
||||
LearningContentInterface,
|
||||
)
|
||||
fields = ["content_assignment", "checkbox_text", "has_extended_time_test"]
|
||||
|
||||
def resolve_competence_certificate(self: LearningContentEdoniqTest, info):
|
||||
return self.content_assignment.competence_certificate
|
||||
|
||||
def resolve_has_extended_time_test(self: LearningContentEdoniqTest, info):
|
||||
return self.has_extended_time_test
|
||||
|
||||
|
||||
class LearningContentRichTextObjectType(DjangoObjectType):
|
||||
class Meta:
|
||||
model = LearningContentRichText
|
||||
interfaces = (LearningContentInterface,)
|
||||
fields = []
|
||||
interfaces = (
|
||||
CoursePageInterface,
|
||||
LearningContentInterface,
|
||||
)
|
||||
fields = ["text"]
|
||||
|
||||
|
||||
class LearningContentAssignmentObjectType(DjangoObjectType):
|
||||
competence_certificate = graphene.Field(
|
||||
"vbv_lernwelt.competence.graphql.types.CompetenceCertificateObjectType",
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = LearningContentAssignment
|
||||
interfaces = (LearningContentInterface,)
|
||||
interfaces = (
|
||||
CoursePageInterface,
|
||||
LearningContentInterface,
|
||||
)
|
||||
fields = [
|
||||
"content_assignment",
|
||||
"assignment_type",
|
||||
]
|
||||
|
||||
def resolve_competence_certificate(self: LearningContentAssignment, info):
|
||||
return self.content_assignment.competence_certificate
|
||||
|
||||
|
||||
class LearningContentDocumentListObjectType(DjangoObjectType):
|
||||
class Meta:
|
||||
model = LearningContentDocumentList
|
||||
interfaces = (LearningContentInterface,)
|
||||
interfaces = (
|
||||
CoursePageInterface,
|
||||
LearningContentInterface,
|
||||
)
|
||||
fields = []
|
||||
|
||||
|
||||
class LearningUnitObjectType(DjangoObjectType):
|
||||
learning_contents = graphene.List(LearningContentInterface)
|
||||
learning_contents = graphene.List(
|
||||
graphene.NonNull(LearningContentInterface), required=True
|
||||
)
|
||||
performance_criteria = graphene.List(
|
||||
graphene.NonNull(
|
||||
"vbv_lernwelt.competence.graphql.types.PerformanceCriteriaObjectType"
|
||||
),
|
||||
required=True,
|
||||
)
|
||||
evaluate_url = graphene.String(required=True)
|
||||
|
||||
class Meta:
|
||||
model = LearningUnit
|
||||
interfaces = (CoursePageInterface,)
|
||||
fields = []
|
||||
fields = ["evaluate_url", "title_hidden"]
|
||||
|
||||
@staticmethod
|
||||
def resolve_learning_contents(root: LearningUnit, info, **kwargs):
|
||||
def resolve_evaluate_url(self: LearningUnit, info, **kwargs):
|
||||
return self.get_evaluate_url()
|
||||
|
||||
def resolve_performance_criteria(self: LearningUnit, info, **kwargs):
|
||||
return self.performancecriteria_set.all()
|
||||
|
||||
def resolve_learning_contents(self: LearningUnit, info, **kwargs):
|
||||
siblings = None
|
||||
if hasattr(info.context, "circle_descendants"):
|
||||
circle_descendants = info.context.circle_descendants
|
||||
index = circle_descendants.index(root)
|
||||
index = circle_descendants.index(self)
|
||||
siblings = circle_descendants[index + 1 :]
|
||||
|
||||
if not siblings:
|
||||
siblings = root.get_siblings().live().specific()
|
||||
siblings = self.get_siblings().live().specific()
|
||||
|
||||
learning_contents = []
|
||||
for sibling in siblings:
|
||||
|
|
@ -167,7 +239,9 @@ class LearningUnitObjectType(DjangoObjectType):
|
|||
|
||||
|
||||
class LearningSequenceObjectType(DjangoObjectType):
|
||||
learning_units = graphene.List(LearningUnitObjectType)
|
||||
learning_units = graphene.List(
|
||||
graphene.NonNull(LearningUnitObjectType), required=True
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = LearningSequence
|
||||
|
|
@ -196,7 +270,9 @@ class LearningSequenceObjectType(DjangoObjectType):
|
|||
|
||||
|
||||
class CircleObjectType(DjangoObjectType):
|
||||
learning_sequences = graphene.List(LearningSequenceObjectType)
|
||||
learning_sequences = graphene.List(
|
||||
graphene.NonNull(LearningSequenceObjectType), required=True
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = Circle
|
||||
|
|
@ -206,21 +282,23 @@ class CircleObjectType(DjangoObjectType):
|
|||
"goals",
|
||||
]
|
||||
|
||||
@staticmethod
|
||||
def resolve_learning_sequences(root: Circle, info, **kwargs):
|
||||
def resolve_learning_sequences(self: Circle, info, **kwargs):
|
||||
circle_descendants = None
|
||||
|
||||
if hasattr(info.context, "learning_path_descendants"):
|
||||
children = info.context.learning_path_descendants
|
||||
circle_start_index = children.index(root)
|
||||
circle_start_index = children.index(self)
|
||||
next_children = children[circle_start_index + 1 :]
|
||||
next_circle_index = find_first_index(
|
||||
children[circle_start_index + 1 :],
|
||||
next_children,
|
||||
pred=lambda child: child.specific_class == Circle,
|
||||
)
|
||||
circle_descendants = children[circle_start_index + 1 : next_circle_index]
|
||||
circle_descendants = next_children[0:next_circle_index]
|
||||
if circle_descendants[-1].specific_class == Topic:
|
||||
circle_descendants = circle_descendants[:-1]
|
||||
|
||||
if not circle_descendants:
|
||||
circle_descendants = list(root.get_descendants().live().specific())
|
||||
circle_descendants = list(self.get_descendants().live().specific())
|
||||
|
||||
# store flattened descendents to improve performance (no need for db queries)
|
||||
info.context.circle_descendants = list(circle_descendants)
|
||||
|
|
@ -233,7 +311,7 @@ class CircleObjectType(DjangoObjectType):
|
|||
|
||||
|
||||
class TopicObjectType(DjangoObjectType):
|
||||
circles = graphene.List(CircleObjectType)
|
||||
circles = graphene.List(graphene.NonNull(CircleObjectType), required=True)
|
||||
|
||||
class Meta:
|
||||
model = Topic
|
||||
|
|
@ -265,11 +343,12 @@ class TopicObjectType(DjangoObjectType):
|
|||
|
||||
|
||||
class LearningPathObjectType(DjangoObjectType):
|
||||
topics = graphene.List(TopicObjectType)
|
||||
topics = graphene.List(graphene.NonNull(TopicObjectType), required=True)
|
||||
|
||||
class Meta:
|
||||
model = LearningPath
|
||||
interfaces = (CoursePageInterface,)
|
||||
fields = []
|
||||
|
||||
@staticmethod
|
||||
def resolve_topics(root: LearningPath, info, **kwargs):
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
return f"/course/{self.slug.replace('-lp', '')}/learn"
|
||||
|
||||
def get_cockpit_url(self):
|
||||
return f"/course/{self.slug.replace('-lp', '')}/cockpit"
|
||||
|
||||
|
||||
class Topic(CourseBasePage):
|
||||
serialize_field_names = ["is_visible"]
|
||||
|
|
@ -58,6 +55,9 @@ class Topic(CourseBasePage):
|
|||
def get_admin_display_title(self):
|
||||
return f"Thema: {self.draft_title}"
|
||||
|
||||
def get_frontend_url(self):
|
||||
return ""
|
||||
|
||||
class Meta:
|
||||
verbose_name = "Topic"
|
||||
|
||||
|
|
@ -372,10 +372,7 @@ class LearningContentEdoniqTest(LearningContent):
|
|||
|
||||
content_assignment = models.ForeignKey(
|
||||
"assignment.Assignment",
|
||||
# `null=True` is only set because of existing data...
|
||||
null=True,
|
||||
blank=True,
|
||||
on_delete=models.SET_NULL,
|
||||
on_delete=models.PROTECT,
|
||||
)
|
||||
|
||||
@property
|
||||
|
|
|
|||
Loading…
Reference in New Issue