Refactor `CourseCompletion` model

This commit is contained in:
Daniel Egger 2023-06-26 17:05:45 +02:00
parent ab8dbd09ef
commit 3bd489d2ae
27 changed files with 206 additions and 162 deletions

View File

@ -19,10 +19,10 @@ const props = withDefaults(defineProps<Props>(), {
<div class="flex flex-row items-center"> <div class="flex flex-row items-center">
<div v-if="showState" class="mr-4 h-8 w-8"> <div v-if="showState" class="mr-4 h-8 w-8">
<it-icon-smiley-happy <it-icon-smiley-happy
v-if="criteria.completion_status === 'success'" v-if="criteria.completion_status === 'SUCCESS'"
></it-icon-smiley-happy> ></it-icon-smiley-happy>
<it-icon-smiley-thinking <it-icon-smiley-thinking
v-else-if="criteria.completion_status === 'fail'" v-else-if="criteria.completion_status === 'FAIL'"
></it-icon-smiley-thinking> ></it-icon-smiley-thinking>
<it-icon-smiley-neutral v-else></it-icon-smiley-neutral> <it-icon-smiley-neutral v-else></it-icon-smiley-neutral>
</div> </div>

View File

@ -1,7 +1,7 @@
<script setup lang="ts"> <script setup lang="ts">
import { computed } from "vue"; import { computed } from "vue";
export type StatusCountKey = "fail" | "success" | "unknown"; export type StatusCountKey = "FAIL" | "SUCCESS" | "UNKNOWN";
export type StatusCount = Record<StatusCountKey, number>; export type StatusCount = Record<StatusCountKey, number>;
const props = defineProps<{ const props = defineProps<{
@ -10,9 +10,9 @@ const props = defineProps<{
const total = computed(() => { const total = computed(() => {
return ( return (
(props.statusCount?.fail || 0) + (props.statusCount?.FAIL || 0) +
(props.statusCount?.success || 0) + (props.statusCount?.SUCCESS || 0) +
(props.statusCount?.unknown || 0) (props.statusCount?.UNKNOWN || 0)
); );
}); });
@ -21,7 +21,7 @@ const done = computed(() => {
return 0; return 0;
} }
return ((props.statusCount?.success || 0) / total.value) * 100; return ((props.statusCount?.SUCCESS || 0) / total.value) * 100;
}); });
const notDone = computed(() => { const notDone = computed(() => {
@ -29,7 +29,7 @@ const notDone = computed(() => {
return 0; return 0;
} }
return ((props.statusCount?.fail || 0) / total.value) * 100 + done.value; return ((props.statusCount?.FAIL || 0) / total.value) * 100 + done.value;
}); });
</script> </script>

View File

@ -1,5 +1,5 @@
import type { CourseCompletionStatus } from "@/types"; import type { CourseCompletionStatus } from "@/types";
export const COMPLETION_SUCCESS: CourseCompletionStatus = "success"; export const COMPLETION_SUCCESS: CourseCompletionStatus = "SUCCESS";
export const COMPLETION_FAILURE: CourseCompletionStatus = "fail"; export const COMPLETION_FAILURE: CourseCompletionStatus = "FAIL";
export const COMPLETION_UNKNOWN: CourseCompletionStatus = "unknown"; export const COMPLETION_UNKNOWN: CourseCompletionStatus = "UNKNOWN";

View File

@ -129,7 +129,7 @@ const assignmentDetail = computed(() =>
</template> </template>
<template #link> <template #link>
<router-link <router-link
v-if="submissionStatusForUser(csu.user_id)?.progressStatus === 'success'" v-if="submissionStatusForUser(csu.user_id)?.progressStatus === 'SUCCESS'"
:to="`/course/${props.courseSession.course.slug}/cockpit/assignment/${assignment.assignmentId}/${csu.user_id}`" :to="`/course/${props.courseSession.course.slug}/cockpit/assignment/${assignment.assignmentId}/${csu.user_id}`"
class="w-full text-right underline" class="w-full text-right underline"
data-cy="show-results" data-cy="show-results"

View File

@ -45,10 +45,10 @@ onMounted(async () => {
</div> </div>
<div><ItProgress :status-count="state.progressStatusCount" /></div> <div><ItProgress :status-count="state.progressStatusCount" /></div>
<div class="text-gray-900" :class="{ 'text-gray-900': showTitle }"> <div class="text-gray-900" :class="{ 'text-gray-900': showTitle }">
{{ state.progressStatusCount.success || 0 }} von {{ state.progressStatusCount.SUCCESS || 0 }} von
{{ {{
(state.progressStatusCount.success || 0) + (state.progressStatusCount.SUCCESS || 0) +
(state.progressStatusCount.unknown || 0) (state.progressStatusCount.UNKNOWN || 0)
}} }}
Lernenden haben ihre Ergebnisse eingereicht. Lernenden haben ihre Ergebnisse eingereicht.
</div> </div>

View File

@ -174,7 +174,7 @@ function setActiveClasses(translationKey: string) {
class="mr-2 inline-block h-8 w-8" class="mr-2 inline-block h-8 w-8"
></it-icon-smiley-thinking> ></it-icon-smiley-thinking>
<p class="text-bold inline-block"> <p class="text-bold inline-block">
{{ userCountStatusForCircle(csu.user_id, circle).fail }} {{ userCountStatusForCircle(csu.user_id, circle).FAIL }}
</p> </p>
</div> </div>
<li class="mr-6 flex flex-row items-center"> <li class="mr-6 flex flex-row items-center">
@ -182,7 +182,7 @@ function setActiveClasses(translationKey: string) {
class="mr-2 inline-block h-8 w-8" class="mr-2 inline-block h-8 w-8"
></it-icon-smiley-happy> ></it-icon-smiley-happy>
<p class="text-bold inline-block"> <p class="text-bold inline-block">
{{ userCountStatusForCircle(csu.user_id, circle).success }} {{ userCountStatusForCircle(csu.user_id, circle).SUCCESS }}
</p> </p>
</li> </li>
<li class="flex flex-row items-center"> <li class="flex flex-row items-center">
@ -190,7 +190,7 @@ function setActiveClasses(translationKey: string) {
class="mr-2 inline-block h-8 w-8" class="mr-2 inline-block h-8 w-8"
></it-icon-smiley-neutral> ></it-icon-smiley-neutral>
<p class="text-bold inline-block"> <p class="text-bold inline-block">
{{ userCountStatusForCircle(csu.user_id, circle).unknown }} {{ userCountStatusForCircle(csu.user_id, circle).UNKNOWN }}
</p> </p>
</li> </li>
</div> </div>

View File

@ -22,7 +22,7 @@ const failedCriteria = computed(() => {
return competenceStore return competenceStore
.flatPerformanceCriteria() .flatPerformanceCriteria()
.filter((criteria) => { .filter((criteria) => {
return criteria.completion_status === "fail"; return criteria.completion_status === "FAIL";
}) })
.slice(0, 3); .slice(0, 3);
}); });
@ -111,7 +111,7 @@ const countStatus = computed(() => {
<h5 class="mb-4 text-gray-700">«{{ $t("selfEvaluation.no") }}»</h5> <h5 class="mb-4 text-gray-700">«{{ $t("selfEvaluation.no") }}»</h5>
<div class="flex flex-row items-center"> <div class="flex flex-row items-center">
<it-icon-smiley-thinking class="h-16 w-16"></it-icon-smiley-thinking> <it-icon-smiley-thinking class="h-16 w-16"></it-icon-smiley-thinking>
<p class="ml-4 inline-block text-7xl font-bold">{{ countStatus.fail }}</p> <p class="ml-4 inline-block text-7xl font-bold">{{ countStatus.FAIL }}</p>
</div> </div>
</li> </li>
<li <li
@ -121,7 +121,7 @@ const countStatus = computed(() => {
<div class="flex flex-row items-center"> <div class="flex flex-row items-center">
<it-icon-smiley-happy class="h-16 w-16"></it-icon-smiley-happy> <it-icon-smiley-happy class="h-16 w-16"></it-icon-smiley-happy>
<p class="ml-4 inline-block text-7xl font-bold"> <p class="ml-4 inline-block text-7xl font-bold">
{{ countStatus.success }} {{ countStatus.SUCCESS }}
</p> </p>
</div> </div>
</li> </li>
@ -130,7 +130,7 @@ const countStatus = computed(() => {
<div class="flex flex-row items-center"> <div class="flex flex-row items-center">
<it-icon-smiley-neutral class="h-16 w-16"></it-icon-smiley-neutral> <it-icon-smiley-neutral class="h-16 w-16"></it-icon-smiley-neutral>
<p class="ml-4 inline-block text-7xl font-bold"> <p class="ml-4 inline-block text-7xl font-bold">
{{ countStatus.unknown }} {{ countStatus.UNKNOWN }}
</p> </p>
</div> </div>
</li> </li>

View File

@ -32,17 +32,17 @@ const { t } = useTranslation();
const mobileMenuItems: MenuItem[] = [ const mobileMenuItems: MenuItem[] = [
{ {
id: "fail", id: "FAIL",
name: `«${t("selfEvaluation.no")}»`, name: `«${t("selfEvaluation.no")}»`,
iconName: "it-icon-smiley-thinking", iconName: "it-icon-smiley-thinking",
}, },
{ {
id: "success", id: "SUCCESS",
name: `«${t("selfEvaluation.yes")}»`, name: `«${t("selfEvaluation.yes")}»`,
iconName: "it-icon-smiley-happy", iconName: "it-icon-smiley-happy",
}, },
{ {
id: "unknown", id: "UNKNOWN",
name: t("competences.notAssessed"), name: t("competences.notAssessed"),
iconName: "it-icon-smiley-neutral", iconName: "it-icon-smiley-neutral",
}, },
@ -91,7 +91,7 @@ function updateActiveState(status: CourseCompletionStatus) {
:key="item.id" :key="item.id"
:class="{ :class="{
'bg-gray-200': activeMenuItem.id === item.id, 'bg-gray-200': activeMenuItem.id === item.id,
'mr-6': item.id !== 'unknown', 'mr-6': item.id !== 'UNKNOWN',
}" }"
class="mr-6 inline-block px-2 py-4" class="mr-6 inline-block px-2 py-4"
@click="updateActiveState(item.id)" @click="updateActiveState(item.id)"

View File

@ -39,11 +39,11 @@ const singleCriteria = computed(() => {
<button <button
class="inline-flex flex-1 items-center border p-4 text-left" class="inline-flex flex-1 items-center border p-4 text-left"
:class="{ :class="{
'border-green-500': singleCriteria.completion_status === 'success', 'border-green-500': singleCriteria.completion_status === 'SUCCESS',
'border-2': singleCriteria.completion_status === 'success', 'border-2': singleCriteria.completion_status === 'SUCCESS',
}" }"
data-cy="success" data-cy="success"
@click="circleStore.markCompletion(singleCriteria, 'success')" @click="circleStore.markCompletion(singleCriteria, 'SUCCESS')"
> >
<it-icon-smiley-happy class="mr-4 h-16 w-16"></it-icon-smiley-happy> <it-icon-smiley-happy class="mr-4 h-16 w-16"></it-icon-smiley-happy>
<span class="text-large font-bold">{{ $t("selfEvaluation.yes") }}</span> <span class="text-large font-bold">{{ $t("selfEvaluation.yes") }}</span>
@ -51,11 +51,11 @@ const singleCriteria = computed(() => {
<button <button
class="inline-flex flex-1 items-center border p-4 text-left" class="inline-flex flex-1 items-center border p-4 text-left"
:class="{ :class="{
'border-orange-500': singleCriteria.completion_status === 'fail', 'border-orange-500': singleCriteria.completion_status === 'FAIL',
'border-2': singleCriteria.completion_status === 'fail', 'border-2': singleCriteria.completion_status === 'FAIL',
}" }"
data-cy="fail" data-cy="fail"
@click="circleStore.markCompletion(singleCriteria, 'fail')" @click="circleStore.markCompletion(singleCriteria, 'FAIL')"
> >
<it-icon-smiley-thinking class="mr-4 h-16 w-16"></it-icon-smiley-thinking> <it-icon-smiley-thinking class="mr-4 h-16 w-16"></it-icon-smiley-thinking>
<span class="text-xl font-bold">{{ $t("selfEvaluation.no") }}</span> <span class="text-xl font-bold">{{ $t("selfEvaluation.no") }}</span>

View File

@ -24,9 +24,9 @@ const props = withDefaults(defineProps<Props>(), {
const circleStore = useCircleStore(); const circleStore = useCircleStore();
function toggleCompleted(learningContent: LearningContentInterface) { function toggleCompleted(learningContent: LearningContentInterface) {
let completionStatus: CourseCompletionStatus = "success"; let completionStatus: CourseCompletionStatus = "SUCCESS";
if (learningContent.completion_status === "success") { if (learningContent.completion_status === "SUCCESS") {
completionStatus = "fail"; completionStatus = "FAIL";
} }
circleStore.markCompletion(learningContent, completionStatus); circleStore.markCompletion(learningContent, completionStatus);
} }
@ -56,7 +56,7 @@ const continueTranslationKeyTuple = computed(() => {
const lastFinished = findLast( const lastFinished = findLast(
circleStore.circle.flatLearningContents, circleStore.circle.flatLearningContents,
(learningContent) => { (learningContent) => {
return learningContent.completion_status === "success"; return learningContent.completion_status === "SUCCESS";
} }
); );
@ -127,7 +127,7 @@ const learningSequenceBorderClass = computed(() => {
> >
<div v-if="props.readonly"> <div v-if="props.readonly">
<it-icon-check <it-icon-check
v-if="learningContent.completion_status === 'success'" v-if="learningContent.completion_status === 'SUCCESS'"
class="block h-8 w-8" class="block h-8 w-8"
></it-icon-check> ></it-icon-check>
<div v-else class="h-8 w-8"></div> <div v-else class="h-8 w-8"></div>
@ -136,7 +136,7 @@ const learningSequenceBorderClass = computed(() => {
v-else v-else
:checkbox-item="{ :checkbox-item="{
value: learningContent.completion_status, value: learningContent.completion_status,
checked: learningContent.completion_status === 'success', checked: learningContent.completion_status === 'SUCCESS',
}" }"
:data-cy="`${learningContent.slug}-checkbox`" :data-cy="`${learningContent.slug}-checkbox`"
@toggle="toggleCompleted(learningContent)" @toggle="toggleCompleted(learningContent)"
@ -194,14 +194,14 @@ const learningSequenceBorderClass = computed(() => {
@click="!props.readonly && circleStore.openSelfEvaluation(learningUnit)" @click="!props.readonly && circleStore.openSelfEvaluation(learningUnit)"
> >
<div <div
v-if="circleStore.calcSelfEvaluationStatus(learningUnit) === 'success'" v-if="circleStore.calcSelfEvaluationStatus(learningUnit) === 'SUCCESS'"
class="self-evaluation-success flex items-center gap-4 pb-3 lg:pb-6" class="self-evaluation-success flex items-center gap-4 pb-3 lg:pb-6"
> >
<it-icon-smiley-happy class="h-8 w-8 flex-none" data-cy="success" /> <it-icon-smiley-happy class="h-8 w-8 flex-none" data-cy="success" />
<div>{{ $t("selfEvaluation.selfEvaluationYes") }}</div> <div>{{ $t("selfEvaluation.selfEvaluationYes") }}</div>
</div> </div>
<div <div
v-else-if="circleStore.calcSelfEvaluationStatus(learningUnit) === 'fail'" v-else-if="circleStore.calcSelfEvaluationStatus(learningUnit) === 'FAIL'"
class="self-evaluation-fail flex items-center gap-4 pb-3 lg:pb-6" class="self-evaluation-fail flex items-center gap-4 pb-3 lg:pb-6"
> >
<it-icon-smiley-thinking class="h-8 w-8 flex-none" data-cy="fail" /> <it-icon-smiley-thinking class="h-8 w-8 flex-none" data-cy="fail" />

View File

@ -93,8 +93,8 @@ onUnmounted(() => {
<button <button
class="inline-flex flex-1 items-center border p-4 text-left" class="inline-flex flex-1 items-center border p-4 text-left"
:class="{ :class="{
'border-green-500': currentQuestion.completion_status === 'success', 'border-green-500': currentQuestion.completion_status === 'SUCCESS',
'border-2': currentQuestion.completion_status === 'success', 'border-2': currentQuestion.completion_status === 'SUCCESS',
}" }"
data-cy="success" data-cy="success"
@click="circleStore.markCompletion(currentQuestion, COMPLETION_SUCCESS)" @click="circleStore.markCompletion(currentQuestion, COMPLETION_SUCCESS)"
@ -112,7 +112,7 @@ onUnmounted(() => {
'border-2': currentQuestion.completion_status === COMPLETION_FAILURE, 'border-2': currentQuestion.completion_status === COMPLETION_FAILURE,
}" }"
data-cy="fail" data-cy="fail"
@click="circleStore.markCompletion(currentQuestion, 'fail')" @click="circleStore.markCompletion(currentQuestion, 'FAIL')"
> >
<it-icon-smiley-thinking <it-icon-smiley-thinking
class="mr-4 h-16 w-16" class="mr-4 h-16 w-16"

View File

@ -70,13 +70,13 @@ export function calcUserAssignmentCompletionStatus(
if (userAssignmentStatus) { if (userAssignmentStatus) {
userStatus = userAssignmentStatus.completion_status; userStatus = userAssignmentStatus.completion_status;
} }
let progressStatus: StatusCountKey = "unknown"; let progressStatus: StatusCountKey = "UNKNOWN";
if ( if (
["SUBMITTED", "EVALUATION_IN_PROGRESS", "EVALUATION_SUBMITTED"].includes( ["SUBMITTED", "EVALUATION_IN_PROGRESS", "EVALUATION_SUBMITTED"].includes(
userStatus userStatus
) )
) { ) {
progressStatus = "success"; progressStatus = "SUCCESS";
} }
return { return {

View File

@ -203,7 +203,7 @@ export class Circle implements WagtailCircle {
return ( return (
this.flatChildren.filter((lc) => { this.flatChildren.filter((lc) => {
return ( return (
lc.completion_status === "success" && lc.completion_status === "SUCCESS" &&
lc.parentLearningSequence?.translation_key === translationKey lc.parentLearningSequence?.translation_key === translationKey
); );
}).length > 0 }).length > 0
@ -229,12 +229,12 @@ export class Circle implements WagtailCircle {
); );
return ( return (
learningContents.every((lc) => lc.completion_status === "success") && learningContents.every((lc) => lc.completion_status === "SUCCESS") &&
(groupedPerformanceCriteria.length === 0 || (groupedPerformanceCriteria.length === 0 ||
groupedPerformanceCriteria.every((group) => groupedPerformanceCriteria.every((group) =>
group.every( group.every(
(pc) => (pc) =>
pc.completion_status === "success" || pc.completion_status === "fail" pc.completion_status === "SUCCESS" || pc.completion_status === "FAIL"
) )
)) ))
); );
@ -252,12 +252,12 @@ export class Circle implements WagtailCircle {
public parseCompletionData(completionData: CourseCompletion[]) { public parseCompletionData(completionData: CourseCompletion[]) {
this.flatChildren.forEach((page) => { this.flatChildren.forEach((page) => {
const pageIndex = completionData.findIndex((e) => { const pageIndex = completionData.findIndex((e) => {
return e.page_key === page.translation_key; return e.page_id === page.id;
}); });
if (pageIndex >= 0) { if (pageIndex >= 0) {
page.completion_status = completionData[pageIndex].completion_status; page.completion_status = completionData[pageIndex].completion_status;
} else { } else {
page.completion_status = "unknown"; page.completion_status = "UNKNOWN";
} }
}); });

View File

@ -25,7 +25,7 @@ function getLastCompleted(courseSlug: string, completionData: CourseCompletion[]
const courseSession = courseSessionsStore.courseSessionForCourse(courseSlug); const courseSession = courseSessionsStore.courseSessionForCourse(courseSlug);
return orderBy(completionData, ["updated_at"], "desc").find((c: CourseCompletion) => { return orderBy(completionData, ["updated_at"], "desc").find((c: CourseCompletion) => {
return ( return (
c.completion_status === "success" && c.completion_status === "SUCCESS" &&
c.course_session === courseSession?.id && c.course_session === courseSession?.id &&
c.page_type.startsWith("learnpath.LearningContent") c.page_type.startsWith("learnpath.LearningContent")
); );
@ -129,13 +129,13 @@ export class LearningPath implements WagtailLearningPath {
const lastCircle = this.circles.find((circle) => { const lastCircle = this.circles.find((circle) => {
return circle.flatLearningContents.find( return circle.flatLearningContents.find(
(learningContent) => (learningContent) =>
learningContent.translation_key === lastCompletedLearningContent.page_key learningContent.id === lastCompletedLearningContent.page_id
); );
}); });
if (lastCircle) { if (lastCircle) {
const lastLearningContent = lastCircle.flatLearningContents.find( const lastLearningContent = lastCircle.flatLearningContents.find(
(learningContent) => (learningContent) =>
learningContent.translation_key === lastCompletedLearningContent.page_key learningContent.id === lastCompletedLearningContent.page_id
); );
if (lastLearningContent && lastLearningContent.nextLearningContent) { if (lastLearningContent && lastLearningContent.nextLearningContent) {
this.nextLearningContent = lastLearningContent.nextLearningContent; this.nextLearningContent = lastLearningContent.nextLearningContent;

View File

@ -105,7 +105,7 @@ export const useCircleStore = defineStore({
| LearningUnitPerformanceCriteria | LearningUnitPerformanceCriteria
| PerformanceCriteria | PerformanceCriteria
| undefined, | undefined,
completion_status: CourseCompletionStatus = "success" completion_status: CourseCompletionStatus = "SUCCESS"
) { ) {
const completionStore = useCompletionStore(); const completionStore = useCompletionStore();
@ -146,22 +146,22 @@ export const useCircleStore = defineStore({
}, },
calcSelfEvaluationStatus(learningUnit: LearningUnit): CourseCompletionStatus { calcSelfEvaluationStatus(learningUnit: LearningUnit): CourseCompletionStatus {
if (learningUnit.children.length > 0) { if (learningUnit.children.length > 0) {
if (learningUnit.children.every((q) => q.completion_status === "success")) { if (learningUnit.children.every((q) => q.completion_status === "SUCCESS")) {
return "success"; return "SUCCESS";
} }
if ( if (
learningUnit.children.every( learningUnit.children.every(
(q) => q.completion_status === "fail" || q.completion_status === "success" (q) => q.completion_status === "FAIL" || q.completion_status === "SUCCESS"
) )
) { ) {
return "fail"; return "FAIL";
} }
} }
return "unknown"; return "UNKNOWN";
}, },
continueFromLearningContent(currentLearningContent: LearningContentInterface) { continueFromLearningContent(currentLearningContent: LearningContentInterface) {
if (currentLearningContent) { if (currentLearningContent) {
this.markCompletion(currentLearningContent, "success"); this.markCompletion(currentLearningContent, "SUCCESS");
this.closeLearningContent(currentLearningContent); this.closeLearningContent(currentLearningContent);
} else { } else {
log.error("currentLearningContent is undefined"); log.error("currentLearningContent is undefined");

View File

@ -35,16 +35,16 @@ export const useCompetenceStore = defineStore({
if (criteria) { if (criteria) {
const grouped = groupBy(criteria, "completion_status"); const grouped = groupBy(criteria, "completion_status");
return { return {
fail: grouped?.fail?.length || 0, FAIL: grouped?.FAIL.length || 0,
success: grouped?.success?.length || 0, SUCCESS: grouped?.SUCCESS.length || 0,
unknown: grouped?.unknown?.length || 0, UNKNOWN: grouped?.UNKNOWN.length || 0,
}; };
} }
return { return {
success: 0, SUCCESS: 0,
fail: 0, FAIL: 0,
unknown: 0, UNKNOWN: 0,
}; };
}, },
criteriaByCompetence(competence: CompetencePage) { criteriaByCompetence(competence: CompetencePage) {
@ -177,14 +177,14 @@ export const useCompetenceStore = defineStore({
competenceProfilePage.children.forEach((competence) => { competenceProfilePage.children.forEach((competence) => {
competence.children.forEach((performanceCriteria) => { competence.children.forEach((performanceCriteria) => {
const completion = completionData.find( const completion = completionData.find(
(c) => c.page_key === performanceCriteria.translation_key (c) => c.page_id === performanceCriteria.id
); );
if (completion) { if (completion) {
performanceCriteria.completion_status = completion.completion_status; performanceCriteria.completion_status = completion.completion_status;
performanceCriteria.completion_status_updated_at = performanceCriteria.completion_status_updated_at =
completion.updated_at; completion.updated_at;
} else { } else {
performanceCriteria.completion_status = "unknown"; performanceCriteria.completion_status = "UNKNOWN";
performanceCriteria.completion_status_updated_at = ""; performanceCriteria.completion_status_updated_at = "";
} }
}); });

View File

@ -43,7 +43,7 @@ export const useCompletionStore = defineStore({
if (courseSessionId) { if (courseSessionId) {
const completionData = await itPost("/api/course/completion/mark/", { const completionData = await itPost("/api/course/completion/mark/", {
page_key: page.translation_key, page_id: page.id,
completion_status: page.completion_status, completion_status: page.completion_status,
course_session_id: courseSessionId, course_session_id: courseSessionId,
}); });

View File

@ -5,7 +5,7 @@ import type { Component } from "vue";
export type LoginMethod = "local" | "sso"; export type LoginMethod = "local" | "sso";
export type CourseCompletionStatus = "unknown" | "fail" | "success"; export type CourseCompletionStatus = "UNKNOWN" | "FAIL" | "SUCCESS";
export interface BaseCourseWagtailPage { export interface BaseCourseWagtailPage {
readonly id: number; readonly id: number;
@ -169,14 +169,13 @@ export interface Topic extends BaseCourseWagtailPage {
export type LearningPathChild = Topic | WagtailCircle; export type LearningPathChild = Topic | WagtailCircle;
export interface CourseCompletion { export interface CourseCompletion {
id: number; readonly id: number;
created_at: string; created_at: string;
updated_at: string; updated_at: string;
user: number; readonly user: number;
page_key: string; readonly page_id: number;
page_type: string; readonly page_type: string;
page_slug: string; readonly course_session: number;
course_session: number;
completion_status: CourseCompletionStatus; completion_status: CourseCompletionStatus;
additional_json_data: unknown; additional_json_data: unknown;
} }

View File

@ -464,19 +464,19 @@ def create_course_uk_de_completion_data(course_session):
for circle in circles: for circle in circles:
for index, lc in enumerate(circle.get_descendants().type(LearningContent)): for index, lc in enumerate(circle.get_descendants().type(LearningContent)):
mark_course_completion( mark_course_completion(
str(lc.translation_key), page=lc,
User.objects.get(email="lina.egger@example.com"), user=User.objects.get(email="lina.egger@example.com"),
course_session=course_session, course_session=course_session,
completion_status="success", completion_status="SUCCESS",
) )
random_number = random.randint(1, 3) random_number = random.randint(1, 3)
if index % random_number == 0: if index % random_number == 0:
mark_course_completion( mark_course_completion(
str(lc.translation_key), page=lc,
User.objects.get(email="michael.meier@example.com"), user=User.objects.get(email="michael.meier@example.com"),
course_session=course_session, course_session=course_session,
completion_status="success", completion_status="SUCCESS",
) )
performance_criteria = ( performance_criteria = (
@ -486,26 +486,26 @@ def create_course_uk_de_completion_data(course_session):
) )
for index, pc in enumerate(performance_criteria): for index, pc in enumerate(performance_criteria):
mark_course_completion( mark_course_completion(
str(pc.translation_key), page=pc,
User.objects.get(email="lina.egger@example.com"), user=User.objects.get(email="lina.egger@example.com"),
course_session=course_session, course_session=course_session,
completion_status="success", completion_status="SUCCESS",
) )
random_number = random.randint(1, 4) random_number = random.randint(1, 4)
if index % random_number == 0: if index % random_number == 0:
mark_course_completion( mark_course_completion(
str(pc.translation_key), page=pc,
User.objects.get(email="michael.meier@example.com"), user=User.objects.get(email="michael.meier@example.com"),
course_session=course_session, course_session=course_session,
completion_status="success", completion_status="SUCCESS",
) )
if index % random_number == 1: if index % random_number == 1:
mark_course_completion( mark_course_completion(
str(pc.translation_key), page=pc,
User.objects.get(email="michael.meier@example.com"), user=User.objects.get(email="michael.meier@example.com"),
course_session=course_session, course_session=course_session,
completion_status="fail", completion_status="FAIL",
) )

View File

@ -0,0 +1,43 @@
# Generated by Django 3.2.13 on 2023-06-26 15:24
from django.db import migrations, models
import django.db.models.deletion
import vbv_lernwelt.course.models
class Migration(migrations.Migration):
dependencies = [
('wagtailcore', '0083_workflowcontenttype'),
('course', '0005_remove_coursesession_attendance_courses'),
]
operations = [
migrations.RemoveConstraint(
model_name='coursecompletion',
name='course_completion_unique_user_page_key',
),
migrations.RemoveField(
model_name='coursecompletion',
name='page_key',
),
migrations.RemoveField(
model_name='coursecompletion',
name='page_slug',
),
migrations.AddField(
model_name='coursecompletion',
name='page',
field=models.ForeignKey(default=1, on_delete=django.db.models.deletion.CASCADE, to='wagtailcore.page'),
preserve_default=False,
),
migrations.AlterField(
model_name='coursecompletion',
name='completion_status',
field=models.CharField(choices=[(vbv_lernwelt.course.models.CourseCompletionStatus['SUCCESS'], 'SUCCESS'), (vbv_lernwelt.course.models.CourseCompletionStatus['FAIL'], 'FAIL'), (vbv_lernwelt.course.models.CourseCompletionStatus['UNKNOWN'], 'UNKNOWN')], default='UNKNOWN', max_length=255),
),
migrations.AddConstraint(
model_name='coursecompletion',
constraint=models.UniqueConstraint(fields=('user', 'page', 'course_session'), name='course_completion_unique_user_page_key'),
),
]

View File

@ -1,3 +1,5 @@
import enum
from django.db import models from django.db import models
from django.db.models import UniqueConstraint from django.db.models import UniqueConstraint
from django.utils.text import slugify from django.utils.text import slugify
@ -152,6 +154,12 @@ class CoursePage(CourseBasePage):
return f"{self.title}" return f"{self.title}"
class CourseCompletionStatus(enum.Enum):
SUCCESS = "SUCCESS"
FAIL = "FAIL"
UNKNOWN = "UNKNOWN"
class CourseCompletion(models.Model): class CourseCompletion(models.Model):
created_at = models.DateTimeField(auto_now_add=True) created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True) updated_at = models.DateTimeField(auto_now=True)
@ -159,27 +167,24 @@ class CourseCompletion(models.Model):
user = models.ForeignKey(User, on_delete=models.CASCADE) user = models.ForeignKey(User, on_delete=models.CASCADE)
# page can logically be a LearningContent or a PerformanceCriteria for now # page can logically be a LearningContent or a PerformanceCriteria for now
page_key = models.UUIDField() page = models.ForeignKey(Page, on_delete=models.CASCADE)
# store for convenience and performance...
page_type = models.CharField(max_length=255, default="", blank=True) page_type = models.CharField(max_length=255, default="", blank=True)
page_slug = models.CharField(max_length=255, default="", blank=True)
course_session = models.ForeignKey("course.CourseSession", on_delete=models.CASCADE) course_session = models.ForeignKey("course.CourseSession", on_delete=models.CASCADE)
completion_status = models.CharField( completion_status = models.CharField(
max_length=255, max_length=255,
choices=[ choices=[(status, status.value) for status in CourseCompletionStatus],
("unknown", "unknown"), default=CourseCompletionStatus.UNKNOWN.value,
("success", "success"),
("fail", "fail"),
],
default="unknown",
) )
additional_json_data = models.JSONField(default=dict) additional_json_data = models.JSONField(default=dict)
class Meta: class Meta:
constraints = [ constraints = [
UniqueConstraint( UniqueConstraint(
fields=["user", "page_key", "course_session"], fields=["user", "page", "course_session"],
name="course_completion_unique_user_page_key", name="course_completion_unique_user_page_key",
) )
] ]

View File

@ -43,10 +43,9 @@ class CourseCompletionSerializer(serializers.ModelSerializer):
"created_at", "created_at",
"updated_at", "updated_at",
"user", "user",
"page_key", "page_id",
"page_type", "page_type",
"page_slug", "course_session_id",
"course_session",
"completion_status", "completion_status",
"additional_json_data", "additional_json_data",
] ]

View File

@ -1,20 +1,23 @@
from wagtail.models import Page from vbv_lernwelt.course.models import CourseCompletion, CourseCompletionStatus
from vbv_lernwelt.course.models import CourseCompletion
from vbv_lernwelt.learnpath.utils import get_wagtail_type from vbv_lernwelt.learnpath.utils import get_wagtail_type
def mark_course_completion(page_key, user, course_session, completion_status="success"): def mark_course_completion(
page = Page.objects.get(translation_key=page_key, locale__language_code="de-CH") page, user, course_session, completion_status=CourseCompletionStatus.SUCCESS.value
page_type = get_wagtail_type(page.specific) ):
course = page.specific.get_course() if completion_status not in CourseCompletionStatus.__members__:
raise ValueError(
f"Invalid value for CourseCompletionStatus: {completion_status}"
)
# TODO: check if this page can be "marked" by user
cc, created = CourseCompletion.objects.get_or_create( cc, created = CourseCompletion.objects.get_or_create(
user=user, user_id=user.id,
page_key=page_key, page_id=page.id,
course_session_id=course_session.id, course_session_id=course_session.id,
) )
cc.page_slug = page.slug
cc.page_type = page_type
cc.completion_status = completion_status cc.completion_status = completion_status
cc.page_type = get_wagtail_type(page.specific)
cc.save() cc.save()
return cc return cc

View File

@ -18,7 +18,7 @@ class CourseCompletionApiTestCase(APITestCase):
def setUp(self) -> None: def setUp(self) -> None:
create_default_users() create_default_users()
create_test_course(include_uk=False) create_test_course(include_uk=False)
self.user = User.objects.get(username="admin") self.user = User.objects.get(username="test-student1@example.com")
self.cs = CourseSession.objects.create( self.cs = CourseSession.objects.create(
course_id=COURSE_TEST_ID, course_id=COURSE_TEST_ID,
title="Test Lehrgang Session", title="Test Lehrgang Session",
@ -27,20 +27,19 @@ class CourseCompletionApiTestCase(APITestCase):
course_session=self.cs, course_session=self.cs,
user=self.user, user=self.user,
) )
self.client.login(username="admin", password="test") self.client.force_login(self.user)
def test_completeLearningContent_works(self): def test_completeLearningContent_happyCase(self):
learning_content = LearningContentPlaceholder.objects.get( learning_content = LearningContentPlaceholder.objects.get(
title="Fachcheck Reisen" title="Fachcheck Reisen"
) )
learning_content_key = str(learning_content.translation_key)
mark_url = f"/api/course/completion/mark/" mark_url = f"/api/course/completion/mark/"
response = self.client.post( response = self.client.post(
mark_url, mark_url,
{ {
"page_key": learning_content_key, "page_id": learning_content.id,
"course_session_id": self.cs.id, "course_session_id": self.cs.id,
}, },
) )
@ -49,13 +48,16 @@ class CourseCompletionApiTestCase(APITestCase):
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
self.assertEqual(len(response_json), 1) self.assertEqual(len(response_json), 1)
self.assertEqual(response_json[0]["page_key"], learning_content_key) self.assertEqual(response_json[0]["page_id"], learning_content.id)
self.assertEqual(response_json[0]["completion_status"], "success") self.assertEqual(
response_json[0]["page_type"], "learnpath.LearningContentPlaceholder"
)
self.assertEqual(response_json[0]["completion_status"], "SUCCESS")
db_entry = CourseCompletion.objects.get( db_entry = CourseCompletion.objects.get(
user=self.user, course_session_id=self.cs.id, page_key=learning_content_key user=self.user, course_session_id=self.cs.id, page_id=learning_content.id
) )
self.assertEqual(db_entry.completion_status, "success") self.assertEqual(db_entry.completion_status, "SUCCESS")
# test getting the circle data # test getting the circle data
response = self.client.get(f"/api/course/completion/{self.cs.id}/") response = self.client.get(f"/api/course/completion/{self.cs.id}/")
@ -65,15 +67,18 @@ class CourseCompletionApiTestCase(APITestCase):
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
self.assertEqual(len(response_json), 1) self.assertEqual(len(response_json), 1)
self.assertEqual(response_json[0]["page_key"], learning_content_key) self.assertEqual(response_json[0]["page_id"], learning_content.id)
self.assertTrue(response_json[0]["completion_status"], "success") self.assertEqual(
response_json[0]["page_type"], "learnpath.LearningContentPlaceholder"
)
self.assertEqual(response_json[0]["completion_status"], "SUCCESS")
# test with "fail" # test with "fail"
response = self.client.post( response = self.client.post(
mark_url, mark_url,
{ {
"page_key": learning_content_key, "page_id": learning_content.id,
"completion_status": "fail", "completion_status": "FAIL",
"course_session_id": self.cs.id, "course_session_id": self.cs.id,
}, },
) )
@ -81,10 +86,13 @@ class CourseCompletionApiTestCase(APITestCase):
response_json = response.json() response_json = response.json()
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
self.assertEqual(len(response_json), 1) self.assertEqual(len(response_json), 1)
self.assertEqual(response_json[0]["page_key"], learning_content_key) self.assertEqual(response_json[0]["page_id"], learning_content.id)
self.assertEqual(response_json[0]["completion_status"], "fail") self.assertEqual(
response_json[0]["page_type"], "learnpath.LearningContentPlaceholder"
)
self.assertEqual(response_json[0]["completion_status"], "FAIL")
db_entry = CourseCompletion.objects.get( db_entry = CourseCompletion.objects.get(
user=self.user, course_session_id=self.cs.id, page_key=learning_content_key user=self.user, course_session_id=self.cs.id, page_id=learning_content.id
) )
self.assertEqual(db_entry.completion_status, "fail") self.assertEqual(db_entry.completion_status, "FAIL")

View File

@ -89,20 +89,17 @@ def request_course_completion_for_user(request, course_session_id, user_id):
@api_view(["POST"]) @api_view(["POST"])
def mark_course_completion_view(request): def mark_course_completion_view(request):
try: try:
page_key = request.data.get("page_key") page_id = request.data.get("page_id")
completion_status = request.data.get("completion_status", "success") completion_status = request.data.get("completion_status", "SUCCESS")
course_session_id = request.data.get("course_session_id") course_session_id = request.data.get("course_session_id")
page = Page.objects.get(translation_key=page_key, locale__language_code="de-CH") page = Page.objects.get(id=page_id)
if not has_course_access_by_page_request(request, page): if not has_course_access_by_page_request(request, page):
raise PermissionDenied() raise PermissionDenied()
page_type = get_wagtail_type(page.specific)
course = page.specific.get_course()
mark_course_completion( mark_course_completion(
page_key, page=page,
request.user, user=request.user,
course_session=CourseSession.objects.get(id=course_session_id), course_session=CourseSession.objects.get(id=course_session_id),
completion_status=completion_status, completion_status=completion_status,
) )
@ -117,8 +114,8 @@ def mark_course_completion_view(request):
logger.debug( logger.debug(
"mark_course_completion successful", "mark_course_completion successful",
label="completion_api", label="completion_api",
page_key=page_key, page_id=page_id,
page_type=page_type, page_type=get_wagtail_type(page.specific),
page_slug=page.slug, page_slug=page.slug,
page_title=page.title, page_title=page.title,
user_id=request.user.id, user_id=request.user.id,

View File

@ -38,12 +38,12 @@ def update_attendance_list(
} }
) )
completion_status = ( completion_status = (
"success" "SUCCESS"
if attendance_user.get("status").value == "PRESENT" if attendance_user.get("status").value == "PRESENT"
else "fail" else "FAIL"
) )
mark_course_completion( mark_course_completion(
page_key=attendance_course.learning_content.translation_key, page=attendance_course.learning_content,
user=u, user=u,
course_session=attendance_course.course_session, course_session=attendance_course.course_session,
completion_status=completion_status, completion_status=completion_status,
@ -63,10 +63,10 @@ def update_attendance_list(
u = User.objects.filter(id=user_id).first() u = User.objects.filter(id=user_id).first()
if u is not None: if u is not None:
mark_course_completion( mark_course_completion(
page_key=attendance_course.learning_content.translation_key, page=attendance_course.learning_content,
user=u, user=u,
course_session=attendance_course.course_session, course_session=attendance_course.course_session,
completion_status="fail", completion_status="FAIL",
) )
return attendance_course return attendance_course

View File

@ -59,13 +59,8 @@ class AttendanceServicesTestCase(TestCase):
self.assertEqual(CourseCompletion.objects.count(), 1) self.assertEqual(CourseCompletion.objects.count(), 1)
cc = CourseCompletion.objects.first() cc = CourseCompletion.objects.first()
self.assertEqual(cc.user, student) self.assertEqual(cc.user, student)
self.assertEqual(cc.completion_status, "success") self.assertEqual(cc.completion_status, "SUCCESS")
self.assertEqual( self.assertEqual(cc.page_id, self.attendance_course.learning_content.id)
cc.page_key, self.attendance_course.learning_content.translation_key
)
self.assertEqual(
cc.page_slug, "test-lehrgang-lp-circle-fahrzeug-lc-präsenzkurs-fahrzeug"
)
def test_updateAttendanceList_withRemovedUser_willUpdateUserCourseCompletion(self): def test_updateAttendanceList_withRemovedUser_willUpdateUserCourseCompletion(self):
student = User.objects.get(username="test-student1@example.com") student = User.objects.get(username="test-student1@example.com")
@ -80,20 +75,15 @@ class AttendanceServicesTestCase(TestCase):
] ]
self.attendance_course.save() self.attendance_course.save()
mark_course_completion( mark_course_completion(
page_key=self.attendance_course.learning_content.translation_key, page=self.attendance_course.learning_content,
user=student, user=student,
course_session=self.course_session, course_session=self.course_session,
completion_status="success", completion_status="SUCCESS",
) )
update_attendance_list(self.attendance_course, []) update_attendance_list(self.attendance_course, [])
self.assertEqual(CourseCompletion.objects.count(), 1) self.assertEqual(CourseCompletion.objects.count(), 1)
cc = CourseCompletion.objects.first() cc = CourseCompletion.objects.first()
self.assertEqual(cc.user, student) self.assertEqual(cc.user, student)
self.assertEqual(cc.completion_status, "fail") self.assertEqual(cc.completion_status, "FAIL")
self.assertEqual( self.assertEqual(cc.page_id, self.attendance_course.learning_content.id)
cc.page_key, self.attendance_course.learning_content.translation_key
)
self.assertEqual(
cc.page_slug, "test-lehrgang-lp-circle-fahrzeug-lc-präsenzkurs-fahrzeug"
)