diff --git a/client/src/components/dueDates/dueDatesUtils.ts b/client/src/components/dueDates/dueDatesUtils.ts index 0c305f05..850377f8 100644 --- a/client/src/components/dueDates/dueDatesUtils.ts +++ b/client/src/components/dueDates/dueDatesUtils.ts @@ -3,23 +3,25 @@ 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) => { dayjs.extend(LocalizedFormat); const startDayjs = dayjs(start); - const endDayjs = dayjs(end); const startDateString = getDateString(startDayjs); - const endDateString = getDateString(endDayjs); - // if startDayjs isundefined, dont show the day twice + let endDayjs; + let endDateString; + if (end) { + endDayjs = dayjs(end); + endDateString = getDateString(endDayjs); + } - if (!startDayjs.isValid() && !endDayjs.isValid()) { + // at least `start` must be provided and valid + if (!startDayjs.isValid()) { return i18next.t("Termin nicht festgelegt"); } - if (!startDayjs || (!startDayjs.isValid() && endDayjs.isValid())) { - return `${endDateString} ${getTimeString(endDayjs)} ${endDayjs.format("[Uhr]")}`; - } - if (!endDayjs || (!endDayjs.isValid() && startDayjs.isValid())) { + // when only `start` is provided, show only the start date with time + if (!endDayjs || !endDayjs.isValid()) { return `${startDateString} ${getTimeString(startDayjs)} ${startDayjs.format( "[Uhr]" )}`; diff --git a/client/src/pages/learningPath/learningContentPage/blocks/EdoniqTestBlock.vue b/client/src/pages/learningPath/learningContentPage/blocks/EdoniqTestBlock.vue index 22b47398..47abd727 100644 --- a/client/src/pages/learningPath/learningContentPage/blocks/EdoniqTestBlock.vue +++ b/client/src/pages/learningPath/learningContentPage/blocks/EdoniqTestBlock.vue @@ -2,17 +2,20 @@ import { useTranslation } from "i18next-vue"; import ItCheckbox from "@/components/ui/ItCheckbox.vue"; import LearningContentSimpleLayout from "@/pages/learningPath/learningContentPage/layouts/LearningContentSimpleLayout.vue"; -import type { - Assignment, - AssignmentCompletion, - LearningContentEdoniqTest, -} from "@/types"; +import type { AssignmentCompletion, LearningContentEdoniqTest } from "@/types"; import { computed, ref } from "vue"; import * as log from "loglevel"; import { itPost } from "@/fetchHelpers"; import { useQuery } from "@urql/vue"; import { ASSIGNMENT_COMPLETION_QUERY } from "@/graphql/queries"; import { useCurrentCourseSession } from "@/composables"; +import { useCourseSessionsStore } from "@/stores/courseSessions"; +import dayjs from "dayjs"; +import { + formatDueDate, + getDateString, +} from "../../../../components/dueDates/dueDatesUtils"; +import ItSuccessAlert from "@/components/ui/ItSuccessAlert.vue"; const { t } = useTranslation(); @@ -21,6 +24,11 @@ const props = defineProps<{ }>(); const courseSession = useCurrentCourseSession(); +const courseSessionsStore = useCourseSessionsStore(); + +const courseSessionEdoniqTest = computed(() => { + return courseSessionsStore.findCourseSessionEdoniqTest(props.content.id); +}); const queryResult = useQuery({ query: ASSIGNMENT_COMPLETION_QUERY, @@ -31,21 +39,25 @@ const queryResult = useQuery({ }, }); -const assignment = computed( - () => queryResult.data.value?.assignment as Assignment | undefined -); const assignmentCompletion = computed( () => queryResult.data.value?.assignment_completion as AssignmentCompletion | undefined ); const completionStatus = computed(() => { - return assignmentCompletion.value?.completion_status ?? "IN_PROGRESS"; + return assignmentCompletion.value?.completion_status ?? ""; }); const termsAccepted = ref(false); const extendedTimeTest = ref(false); +const deadlineInPast = computed(() => { + // with 16 minutes buffer + return dayjs(courseSessionEdoniqTest.value?.deadline_start) + .add(16, "minute") + .isBefore(dayjs()); +}); + async function startTest() { log.info("start test", props.content); const response = await itPost("/api/core/edoniq-test/redirect/", { @@ -62,24 +74,46 @@ async function startTest() { :title="props.content.title" :learning-content="props.content" > - -
-
-
- {{ $t("a.Bewertung") }}: - {{ assignmentCompletion?.evaluation_points }} - {{ $t("assignment.von x Punkten", { x: assignment?.max_points }) }} -
-
Ergebnisse abgeben, Bewertung ausstehend
-
-
-

+
+
+

{{ $t("a.Aufgabe") }}

+

{{ $t("edoniqTest.testDescription") }}

+
-
+
+

{{ $t("a.Abgabetermin") }}

+

+ {{ + $t("edoniqTest.submitDateDescription", { + x: formatDueDate(courseSessionEdoniqTest.deadline_start), + }) + }} +

+
+ +
+

{{ $t("a.Kompetenznachweis") }}

+
+ {{ + $t("circlePage.Dieser Inhalt gehört zu x", { + x: content.competence_certificate?.title, + }) + }}. +
+
+ + {{ $t("circlePage.Im KompetenzNavi anschauen") }} + +
+
+ +
+
+

{{ $t("edoniqTest.checkboxTitle") }}

-
+

{{ $t("edoniqTest.qualifiesForExtendedTimeTitle") }}

-
+
+ +
+
+ {{ $t("edoniqTest.deadlineInPast") }} +
+
+ {{ $t("a.Abgabetermin") }}: + {{ getDateString(dayjs(courseSessionEdoniqTest?.deadline_start)) }} +
+
-
+
+ +
+
+ +
+ {{ $t("a.Resultat") }}: + + {{ assignmentCompletion.evaluation_points }} + + {{ + $t("assignment.von x Punkten", { + x: assignmentCompletion.evaluation_max_points, + }) + }} +
+ +
+ +
+
+
diff --git a/client/src/services/assignmentService.ts b/client/src/services/assignmentService.ts index 439dfb5d..dc6bb4da 100644 --- a/client/src/services/assignmentService.ts +++ b/client/src/services/assignmentService.ts @@ -121,3 +121,10 @@ export function userAssignmentPoints( ) ); } + +export function pointsToGrade(points: number, maxPoints: number) { + // round to half-grades + const grade = Math.round((points / maxPoints) * 10); + const halfGrade = grade / 2; + return Math.min(halfGrade, 5) + 1; +} diff --git a/client/src/stores/courseSessions.ts b/client/src/stores/courseSessions.ts index 7c6bb476..d5fd89d2 100644 --- a/client/src/stores/courseSessions.ts +++ b/client/src/stores/courseSessions.ts @@ -5,6 +5,7 @@ import type { CourseSession, CourseSessionAssignment, CourseSessionAttendanceCourse, + CourseSessionEdoniqTest, CourseSessionUser, DueDate, ExpertSessionUser, @@ -264,6 +265,16 @@ export const useCourseSessionsStore = defineStore("courseSessions", () => { } } + function findCourseSessionEdoniqTest( + contentId?: number + ): CourseSessionEdoniqTest | undefined { + if (contentId && currentCourseSession.value) { + return currentCourseSession.value.edoniq_tests.find( + (a) => a.learning_content_id === contentId + ); + } + } + return { uniqueCourseSessionsByCourse, allCurrentCourseSessions, @@ -280,6 +291,7 @@ export const useCourseSessionsStore = defineStore("courseSessions", () => { removeDocument, findAttendanceCourse, findCourseSessionAssignment, + findCourseSessionEdoniqTest, allDueDates, // use `useCurrentCourseSession` whenever possible diff --git a/client/src/types.ts b/client/src/types.ts index 18b8e04d..4ea6bda2 100644 --- a/client/src/types.ts +++ b/client/src/types.ts @@ -485,6 +485,14 @@ export interface CourseSessionAssignment { evaluation_deadline_start: string; } +export interface CourseSessionEdoniqTest { + id: number; + course_session_id: number; + learning_content_id: number; + deadline_id: number; + deadline_start: string; +} + export interface CourseSession { id: number; created_at: string; @@ -500,6 +508,7 @@ export interface CourseSession { media_library_url: string; attendance_courses: CourseSessionAttendanceCourse[]; assignments: CourseSessionAssignment[]; + edoniq_tests: CourseSessionEdoniqTest[]; documents: CircleDocument[]; users: CourseSessionUser[]; due_dates: DueDate[]; diff --git a/server/vbv_lernwelt/course/creators/test_course.py b/server/vbv_lernwelt/course/creators/test_course.py index ac32e2fa..4582f963 100644 --- a/server/vbv_lernwelt/course/creators/test_course.py +++ b/server/vbv_lernwelt/course/creators/test_course.py @@ -49,12 +49,14 @@ from vbv_lernwelt.course.utils import get_wagtail_default_site from vbv_lernwelt.course_session.models import ( CourseSessionAssignment, CourseSessionAttendanceCourse, + CourseSessionEdoniqTest, ) from vbv_lernwelt.feedback.services import update_feedback_response from vbv_lernwelt.learnpath.models import ( Circle, LearningContentAssignment, LearningContentAttendanceCourse, + LearningContentEdoniqTest, ) from vbv_lernwelt.learnpath.tests.learning_path_factories import ( CircleFactory, @@ -168,6 +170,19 @@ def create_test_course(include_uk=True, include_vv=True, with_sessions=False): ) csa.submission_deadline.save() + cset = CourseSessionEdoniqTest.objects.create( + course_session=cs_bern, + learning_content=LearningContentEdoniqTest.objects.get( + slug="test-lehrgang-lp-circle-fahrzeug-lc-wissens-und-verständnisfragen" + ), + ) + cset.deadline.start = timezone.make_aware( + (next_monday + relativedelta(days=3)).replace( + hour=21, minute=0, second=0, microsecond=0 + ) + ) + cset.deadline.save() + cs_zurich = CourseSession.objects.create( course_id=COURSE_TEST_ID, title="Test Zürich 2022 a", diff --git a/server/vbv_lernwelt/course/serializers.py b/server/vbv_lernwelt/course/serializers.py index 69d326b2..a1b1ba56 100644 --- a/server/vbv_lernwelt/course/serializers.py +++ b/server/vbv_lernwelt/course/serializers.py @@ -10,10 +10,12 @@ from vbv_lernwelt.course.models import ( from vbv_lernwelt.course_session.models import ( CourseSessionAssignment, CourseSessionAttendanceCourse, + CourseSessionEdoniqTest, ) from vbv_lernwelt.course_session.serializers import ( CourseSessionAssignmentSerializer, CourseSessionAttendanceCourseSerializer, + CourseSessionEdoniqTestSerializer, ) from vbv_lernwelt.duedate.models import DueDate from vbv_lernwelt.duedate.serializers import DueDateSerializer @@ -61,6 +63,7 @@ class CourseSessionSerializer(serializers.ModelSerializer): documents = serializers.SerializerMethodField() attendance_courses = serializers.SerializerMethodField() assignments = serializers.SerializerMethodField() + edoniq_tests = serializers.SerializerMethodField() due_dates = serializers.SerializerMethodField() def get_course(self, obj): @@ -97,6 +100,11 @@ class CourseSessionSerializer(serializers.ModelSerializer): CourseSessionAssignment.objects.filter(course_session=obj), many=True ).data + def get_edoniq_tests(self, obj): + return CourseSessionEdoniqTestSerializer( + CourseSessionEdoniqTest.objects.filter(course_session=obj), many=True + ).data + def get_due_dates(self, obj): due_dates = DueDate.objects.filter(course_session=obj) return DueDateSerializer(due_dates, many=True).data @@ -114,6 +122,7 @@ class CourseSessionSerializer(serializers.ModelSerializer): "additional_json_data", "attendance_courses", "assignments", + "edoniq_tests", "learning_path_url", "cockpit_url", "competence_url", diff --git a/server/vbv_lernwelt/course_session/serializers.py b/server/vbv_lernwelt/course_session/serializers.py index 76c9d9f8..7bec74a2 100644 --- a/server/vbv_lernwelt/course_session/serializers.py +++ b/server/vbv_lernwelt/course_session/serializers.py @@ -3,6 +3,7 @@ from rest_framework import serializers from vbv_lernwelt.course_session.models import ( CourseSessionAssignment, CourseSessionAttendanceCourse, + CourseSessionEdoniqTest, ) @@ -61,3 +62,21 @@ class CourseSessionAssignmentSerializer(serializers.ModelSerializer): def get_submission_deadline_start(self, obj): if obj.submission_deadline: return obj.submission_deadline.start + + +class CourseSessionEdoniqTestSerializer(serializers.ModelSerializer): + deadline_start = serializers.SerializerMethodField() + + class Meta: + model = CourseSessionEdoniqTest + fields = [ + "id", + "course_session_id", + "learning_content_id", + "deadline_id", + "deadline_start", + ] + + def get_deadline_start(self, obj): + if obj.deadline: + return obj.deadline.start