+
+
+
+ {{ $t("Trainerunterlagen") }}
+
+
+ {{ $t("cockpit.trainerFilesText") }}
-
-
-
- {{ $t("Anwesenheitskontrolle Präsenzkurse") }}
-
-
- {{
- $t(
- "Hier überprüfst und bestätigst du die Anwesenheit deiner Teilnehmenden."
- )
- }}
-
-
-
-
- {{ $t("Anwesenheit prüfen") }}
-
-
-
-
-
-
-
-
-
{{ $t("cockpit.progress") }}
-
-
-
-
-
+
+
+ {{ $t("a.Unterlagen für Teilnehmenden") }}
+
+
+ {{ $t("a.Stelle deinen Lernenden zusätzliche Inhalte zur Verfügung.") }}
+
+
+
+ {{ courseSessionsStore.circleDocuments.length }} {{ $t("a.Unterlagen") }}
+
+
+
+
+ {{ $t("a.Zum Unterlagen-Upload") }}
+
+
+
+
+
+
+ {{ $t("Anwesenheitskontrolle Präsenzkurse") }}
+
+
+ {{
+ $t(
+ "Hier überprüfst und bestätigst du die Anwesenheit deiner Teilnehmenden."
+ )
+ }}
+
+
+
+
+ {{ $t("Anwesenheit prüfen") }}
+
+
+
+
+
+
+
+
+
+
+
+
+
{{ $t("cockpit.progress") }}
+
+
+
+
+
-
- {{ cockpitStore.selectedCircle.name }}
-
-
-
-
-
- {{ userCountStatusForCircle(csu.user_id).FAIL }}
-
-
-
-
-
-
- {{ userCountStatusForCircle(csu.user_id).SUCCESS }}
-
-
-
-
-
-
- {{ userCountStatusForCircle(csu.user_id).UNKNOWN }}
-
-
+ :show-circle-translation-keys="[
+ cockpitStore.currentCircle.translation_key,
+ ]"
+ diagram-type="singleSmall"
+ class="mr-4"
+ >
+
+ {{ cockpitStore.currentCircle.title }}
+
+
+
+
+
+ {{ userCountStatusForCircle(csu.user_id).FAIL }}
+
+
-
+
+
+ {{ userCountStatusForCircle(csu.user_id).SUCCESS }}
+
+
+
-
+
+
+ {{ userCountStatusForCircle(csu.user_id).UNKNOWN }}
+
+
-
-
-
- {{ $t("general.profileLink") }}
-
-
-
-
-
+
+
+
+
+ {{ $t("general.profileLink") }}
+
+
+
+
-
-
+
+
+
+
{{ $t("a.Kein Circle verfügbar oder ausgewählt.") }}
diff --git a/client/src/pages/cockpit/documentPage/DocumentPage.vue b/client/src/pages/cockpit/documentPage/DocumentPage.vue
new file mode 100644
index 00000000..7e68b5cd
--- /dev/null
+++ b/client/src/pages/cockpit/documentPage/DocumentPage.vue
@@ -0,0 +1,132 @@
+
+
+
+
+
+
+
+
+
{{ t("a.Unterlagen für Teilnehmenden") }}
+
+
+
+
+
+
+
+
+
+ {{ t("circlePage.documents.action") }}
+
+
+
+
+
+
+
+
diff --git a/client/src/pages/learningPath/circlePage/DocumentUploadForm.vue b/client/src/pages/cockpit/documentPage/DocumentUploadForm.vue
similarity index 100%
rename from client/src/pages/learningPath/circlePage/DocumentUploadForm.vue
rename to client/src/pages/cockpit/documentPage/DocumentUploadForm.vue
diff --git a/client/src/pages/learningPath/circlePage/DocumentSection.vue b/client/src/pages/learningPath/circlePage/DocumentSection.vue
index edabf392..03f2fa73 100644
--- a/client/src/pages/learningPath/circlePage/DocumentSection.vue
+++ b/client/src/pages/learningPath/circlePage/DocumentSection.vue
@@ -5,12 +5,7 @@
-
- {{ $t("circlePage.documents.userDescription") }}
-
-
- {{ $t("circlePage.documents.expertDescription") }}
-
+ {{ $t("circlePage.documents.userDescription") }}
-
-
-
-
-
- {{ $t("circlePage.documents.action") }}
-
-
-
-
-
diff --git a/client/src/router/guards.ts b/client/src/router/guards.ts
index f7090336..27116000 100644
--- a/client/src/router/guards.ts
+++ b/client/src/router/guards.ts
@@ -17,7 +17,9 @@ export const redirectToLoginIfRequired: NavigationGuard = (to) => {
if (loginRequired(to) && !userStore.loggedIn) {
const appEnv = import.meta.env.VITE_APP_ENVIRONMENT || "local";
const ssoLogin = appEnv.startsWith("prod") || appEnv.startsWith("stage");
- return ssoLogin ? `/login?next=${to.fullPath}` : `/login-local?next=${to.fullPath}`;
+ return ssoLogin
+ ? `/login?next=${encodeURIComponent(to.fullPath)}`
+ : `/login-local?next=${encodeURIComponent(to.fullPath)}`;
}
};
@@ -46,15 +48,52 @@ export const expertRequired: NavigationGuard = (to: RouteLocationNormalized) =>
}
};
-export async function handleCourseSessions(to: RouteLocationNormalized) {
+export async function handleCurrentCourseSession(to: RouteLocationNormalized) {
// register after login hooks
- const courseSessionsStore = useCourseSessionsStore();
- if (to.params.courseSlug) {
- courseSessionsStore._currentCourseSlug = to.params.courseSlug as string;
- } else {
- courseSessionsStore._currentCourseSlug = "";
- }
- if (!courseSessionsStore.loaded) {
- await courseSessionsStore.loadCourseSessionsData();
+ const userStore = useUserStore();
+ if (userStore.loggedIn) {
+ const courseSessionsStore = useCourseSessionsStore();
+ if (to.params.courseSlug) {
+ courseSessionsStore._currentCourseSlug = to.params.courseSlug as string;
+ } else {
+ courseSessionsStore._currentCourseSlug = "";
+ }
+ if (!courseSessionsStore.loaded) {
+ await courseSessionsStore.loadCourseSessionsData();
+ }
+ }
+}
+
+export async function handleCourseSessionAsQueryParam(to: RouteLocationNormalized) {
+ /**
+ * switch to course session with id from query param `courseSessionId` if it
+ * is present and valid.
+ */
+ // register after login hooks
+ const userStore = useUserStore();
+ if (userStore.loggedIn) {
+ const courseSessionsStore = useCourseSessionsStore();
+ if (!courseSessionsStore.loaded) {
+ await courseSessionsStore.loadCourseSessionsData();
+ }
+
+ if (to.query.courseSessionId) {
+ const { courseSessionId, ...restOfQuery } = to.query;
+ const switchSuccessful = courseSessionsStore.switchCourseSessionById(
+ courseSessionId.toString()
+ );
+ if (switchSuccessful) {
+ return {
+ path: to.path,
+ query: restOfQuery,
+ replace: true,
+ };
+ } else {
+ // courseSessionId is invalid for current user -> redirect to home
+ return {
+ path: "/",
+ };
+ }
+ }
}
}
diff --git a/client/src/router/index.ts b/client/src/router/index.ts
index dac7036f..805a1e9c 100644
--- a/client/src/router/index.ts
+++ b/client/src/router/index.ts
@@ -1,7 +1,8 @@
import DashboardPage from "@/pages/DashboardPage.vue";
import LoginPage from "@/pages/LoginPage.vue";
import {
- handleCourseSessions,
+ handleCourseSessionAsQueryParam,
+ handleCurrentCourseSession,
redirectToLoginIfRequired,
updateLoggedIn,
} from "@/router/guards";
@@ -165,6 +166,11 @@ const router = createRouter({
import("@/pages/cockpit/attendanceCheckPage/AttendanceCheckPage.vue"),
props: true,
},
+ {
+ path: "documents",
+ component: () => import("@/pages/cockpit/documentPage/DocumentPage.vue"),
+ props: true,
+ },
],
},
{
@@ -183,6 +189,14 @@ const router = createRouter({
path: "/notifications",
component: () => import("@/pages/NotificationsPage.vue"),
},
+ {
+ path: "/appointments",
+ component: () => import("@/pages/AppointmentsPage.vue"),
+ },
+ {
+ path: "/course/:courseSlug/appointments",
+ component: () => import("@/pages/AppointmentsPage.vue"),
+ },
{
path: "/styleguide",
component: () => import("../pages/StyleGuidePage.vue"),
@@ -201,7 +215,8 @@ router.beforeEach(updateLoggedIn);
router.beforeEach(redirectToLoginIfRequired);
// register after login hooks
-router.beforeEach(handleCourseSessions);
+router.beforeEach(handleCurrentCourseSession);
+router.beforeEach(handleCourseSessionAsQueryParam);
router.beforeEach(addToHistory);
diff --git a/client/src/stores/cockpit.ts b/client/src/stores/cockpit.ts
index 096e7699..a1f72db0 100644
--- a/client/src/stores/cockpit.ts
+++ b/client/src/stores/cockpit.ts
@@ -2,26 +2,18 @@ import { itGetCached } from "@/fetchHelpers";
import type { CourseSessionUser, ExpertSessionUser } from "@/types";
import log from "loglevel";
+import { useCircleStore } from "@/stores/circle";
import { useLearningPathStore } from "@/stores/learningPath";
import { useUserStore } from "@/stores/user";
import { defineStore } from "pinia";
export type CockpitStoreState = {
courseSessionMembers: CourseSessionUser[] | undefined;
- selectedCircle:
- | {
- id: number;
- name: string;
- slug: string;
- translation_key: string;
- }
- | undefined;
circles: {
- id: number;
+ id: string;
name: string;
- slug: string;
- translation_key: string;
}[];
+ currentCourseSlug: string | undefined;
};
export const useCockpitStore = defineStore({
@@ -29,29 +21,38 @@ export const useCockpitStore = defineStore({
state: () => {
return {
courseSessionMembers: undefined,
- selectedCircle: undefined,
circles: [],
+ currentCourseSlug: undefined,
} as CockpitStoreState;
},
actions: {
async loadCircles(courseSlug: string, courseSessionId: number) {
- log.debug("loadCircles called");
+ log.debug("loadCircles called", courseSlug, courseSessionId);
+ this.currentCourseSlug = courseSlug;
- const f = await courseCircles(courseSlug, courseSessionId);
+ const f = await courseCircles(this.currentCourseSlug, courseSessionId);
this.circles = f.map((c) => {
return {
- id: c.id,
+ id: c.slug,
name: c.title,
- slug: c.slug,
- translation_key: c.translation_key,
};
});
if (this.circles.length > 0) {
- this.selectedCircle = this.circles[0];
+ await this.setCurrentCourseCircle(this.circles[0].id);
}
},
+ async setCurrentCourseCircle(circleSlug: string) {
+ if (!this.currentCourseSlug) {
+ throw new Error("currentCourseSlug is undefined");
+ }
+ const circleStore = useCircleStore();
+ await circleStore.loadCircle(this.currentCourseSlug, circleSlug);
+ },
+ async setCurrentCourseCircleFromEvent(event: { id: string }) {
+ await this.setCurrentCourseCircle(event.id);
+ },
async loadCourseSessionMembers(courseSessionId: number, reload = false) {
log.debug("loadCourseSessionMembers called");
const users = (await itGetCached(
@@ -65,6 +66,19 @@ export const useCockpitStore = defineStore({
return this.courseSessionMembers;
},
},
+ getters: {
+ currentCircle: () => {
+ const circleStore = useCircleStore();
+ return circleStore.circle;
+ },
+ selectedCircle: () => {
+ const circleStore = useCircleStore();
+ return {
+ id: circleStore.circle?.id || 0,
+ name: circleStore.circle?.title || "",
+ };
+ },
+ },
});
async function courseCircles(courseSlug: string, courseSessionId: number) {
diff --git a/client/src/stores/courseSessions.ts b/client/src/stores/courseSessions.ts
index d5fd89d2..d1a74013 100644
--- a/client/src/stores/courseSessions.ts
+++ b/client/src/stores/courseSessions.ts
@@ -92,13 +92,31 @@ export const useCourseSessionsStore = defineStore("courseSessions", () => {
return undefined;
}
- function switchCourseSession(courseSession: CourseSession) {
+ function _switchCourseSession(courseSession: CourseSession) {
log.debug("switchCourseSession", courseSession);
selectedCourseSessionMap.value.set(courseSession.course.slug, courseSession.id);
// Emit event so that the App can re-render with the new courseSession
eventBus.emit("switchedCourseSession", courseSession.id);
}
+ function getCourseSessionById(courseSessionId: number | string) {
+ return allCourseSessions.value.find((cs) => {
+ return courseSessionId.toString() === cs.id.toString();
+ });
+ }
+
+ function switchCourseSessionById(courseSessionId: number | string) {
+ const courseSession = allCourseSessions.value.find((cs) => {
+ return courseSessionId.toString() === cs.id.toString();
+ });
+ if (courseSession) {
+ _switchCourseSession(courseSession);
+ return true;
+ } else {
+ return false;
+ }
+ }
+
function courseSessionForCourse(courseSlug: string) {
if (courseSlug) {
const courseSession = selectedCourseSessionForCourse(courseSlug);
@@ -279,7 +297,8 @@ export const useCourseSessionsStore = defineStore("courseSessions", () => {
uniqueCourseSessionsByCourse,
allCurrentCourseSessions,
courseSessionForCourse,
- switchCourseSession,
+ getCourseSessionById,
+ switchCourseSessionById,
hasCockpit,
hasCourseSessionPreview,
currentCourseSessionHasCockpit,
diff --git a/client/src/types.ts b/client/src/types.ts
index caa741fb..dc7afd26 100644
--- a/client/src/types.ts
+++ b/client/src/types.ts
@@ -641,6 +641,7 @@ export type DueDate = {
date_type_translation_key: string;
subtitle: string;
url: string;
+ url_expert: string;
course_session: number | null;
page: number | null;
circle: CircleLight | null;
diff --git a/client/src/utils/route.ts b/client/src/utils/route.ts
index 71f147a2..8f2a9b69 100644
--- a/client/src/utils/route.ts
+++ b/client/src/utils/route.ts
@@ -27,5 +27,17 @@ export function useRouteLookups() {
return regex.test(route.path);
}
- return { inMediaLibrary, inCockpit, inLearningPath, inCompetenceProfile, inCourse };
+ function inAppointments() {
+ const regex = new RegExp("/(?:[^/]+/)?appointments");
+ return regex.test(route.path);
+ }
+
+ return {
+ inMediaLibrary,
+ inCockpit,
+ inLearningPath,
+ inCompetenceProfile,
+ inCourse,
+ inAppointments: inAppointments,
+ };
}
diff --git a/cypress/e2e/appointments.cy.js b/cypress/e2e/appointments.cy.js
new file mode 100644
index 00000000..e3f618f5
--- /dev/null
+++ b/cypress/e2e/appointments.cy.js
@@ -0,0 +1,41 @@
+import {login} from "./helpers";
+
+// constants
+const COURSE_SELECT = "[data-cy=appointments-course-select]";
+const SESSION_SELECT = "[data-cy=appointments-session-select]";
+const CIRCLE_SELECT = "[data-cy=appointments-circle-select]";
+const APPOINTMENTS = "[data-cy=appointments-list]";
+
+describe("appointments.cy.js", () => {
+ beforeEach(() => {
+ cy.manageCommand("cypress_reset");
+ login("test-student2@example.com", "test");
+ cy.visit("/course/test-lehrgang/appointments");
+ });
+
+ it("preselects first course (Test Lehrgang)", () => {
+ cy.visit("/course/test-lehrgang/appointments");
+ cy.get(COURSE_SELECT).should("contain", "Test Lehrgang");
+ cy.get(SESSION_SELECT).should("contain", "Bern");
+ cy.get(CIRCLE_SELECT).should("contain", "Alle");
+
+ cy.get(".cy-single-due-date").should("have.length", 4);
+ });
+
+ it("can filter by circle", () => {
+ cy.get(CIRCLE_SELECT).click();
+ cy.get(CIRCLE_SELECT).contains("Fahrzeug").click();
+
+ // THEN
+ cy.get(APPOINTMENTS).should("not.contain", "Keine Termine");
+ });
+
+ it("can switch course session", () => {
+ cy.get(SESSION_SELECT).click();
+ cy.get(SESSION_SELECT).contains("Zürich").click();
+ cy.get(SESSION_SELECT).should("contain", "Zürich");
+
+ // THEN
+ cy.get(APPOINTMENTS).should("contain", "Keine Termine");
+ });
+});
diff --git a/server/vbv_lernwelt/course/admin.py b/server/vbv_lernwelt/course/admin.py
index 3a83f956..581266a9 100644
--- a/server/vbv_lernwelt/course/admin.py
+++ b/server/vbv_lernwelt/course/admin.py
@@ -37,8 +37,9 @@ class CourseSessionUserAdmin(admin.ModelAdmin):
"user_first_name",
"course_session",
"role",
- "created_at",
- "updated_at",
+ "circles",
+ # "created_at",
+ # "updated_at",
]
search_fields = [
"user__first_name",
@@ -66,6 +67,9 @@ class CourseSessionUserAdmin(admin.ModelAdmin):
user_last_name.short_description = "Last Name"
user_last_name.admin_order_field = "user__last_name"
+ def circles(self, obj):
+ return ", ".join([c.title for c in obj.expert.all()])
+
fieldsets = [
(None, {"fields": ("user", "course_session", "role")}),
(
diff --git a/server/vbv_lernwelt/course_session/models.py b/server/vbv_lernwelt/course_session/models.py
index 33ac4e6b..9c094de1 100644
--- a/server/vbv_lernwelt/course_session/models.py
+++ b/server/vbv_lernwelt/course_session/models.py
@@ -63,7 +63,10 @@ class CourseSessionAttendanceCourse(models.Model):
)
if not self.due_date.manual_override_fields:
- self.due_date.url = self.learning_content.get_frontend_url()
+ self.due_date.url = self.learning_content.get_frontend_url(
+ course_session_id=self.course_session.id
+ )
+ self.due_date.url_expert = f"/course/{self.due_date.course_session.course.slug}/cockpit/attendance?id={self.learning_content_id}&courseSessionId={self.course_session.id}"
self.due_date.title = self.learning_content.title
self.due_date.page = self.learning_content.page_ptr
self.due_date.assignment_type_translation_key = (
@@ -122,7 +125,9 @@ class CourseSessionAssignment(models.Model):
if self.learning_content_id:
title = self.learning_content.title
page = self.learning_content.page_ptr
- url = self.learning_content.get_frontend_url()
+ url = self.learning_content.get_frontend_url(
+ course_session_id=self.course_session.id
+ )
assignment_type = self.learning_content.assignment_type
assignment_type_translation_keys = {
AssignmentType.CASEWORK.value: "learningContentTypes.casework",
@@ -130,6 +135,8 @@ class CourseSessionAssignment(models.Model):
AssignmentType.REFLECTION.value: "learningContentTypes.reflection",
}
+ url_expert = f"/course/{self.course_session.course.slug}/cockpit/assignment/{self.learning_content_id}?courseSessionId={self.course_session.id}"
+
if assignment_type in (
AssignmentType.CASEWORK.value,
AssignmentType.PREP_ASSIGNMENT.value,
@@ -141,6 +148,7 @@ class CourseSessionAssignment(models.Model):
if not self.submission_deadline.manual_override_fields:
self.submission_deadline.url = url
+ self.submission_deadline.url_expert = url_expert
self.submission_deadline.title = title
self.submission_deadline.assignment_type_translation_key = (
assignment_type_translation_keys[assignment_type]
@@ -160,6 +168,7 @@ class CourseSessionAssignment(models.Model):
if not self.evaluation_deadline.manual_override_fields:
self.evaluation_deadline.url = url
+ self.evaluation_deadline.url_expert = url_expert
self.evaluation_deadline.title = title
self.evaluation_deadline.assignment_type_translation_key = (
assignment_type_translation_keys[assignment_type]
@@ -207,7 +216,10 @@ class CourseSessionEdoniqTest(models.Model):
)
if not self.deadline.manual_override_fields:
- self.deadline.url = self.learning_content.get_frontend_url()
+ self.deadline.url = self.learning_content.get_frontend_url(
+ course_session_id=self.course_session.id
+ )
+ self.deadline.url_expert = f"/course/{self.course_session.course.slug}/cockpit?courseSessionId={self.course_session.id}"
self.deadline.title = self.learning_content.title
self.deadline.page = self.learning_content.page_ptr
self.deadline.assignment_type_translation_key = (
diff --git a/server/vbv_lernwelt/duedate/admin.py b/server/vbv_lernwelt/duedate/admin.py
index dcaf2359..048f4dee 100644
--- a/server/vbv_lernwelt/duedate/admin.py
+++ b/server/vbv_lernwelt/duedate/admin.py
@@ -38,6 +38,7 @@ class DueDateAdmin(admin.ModelAdmin):
"assignment_type_translation_key",
"date_type_translation_key",
"url",
+ "url_expert",
]
return default_readonly
diff --git a/server/vbv_lernwelt/duedate/migrations/0005_auto_20230925_1648.py b/server/vbv_lernwelt/duedate/migrations/0005_auto_20230925_1648.py
new file mode 100644
index 00000000..a7df7df6
--- /dev/null
+++ b/server/vbv_lernwelt/duedate/migrations/0005_auto_20230925_1648.py
@@ -0,0 +1,71 @@
+# Generated by Django 3.2.20 on 2023-09-25 14:48
+
+from django.db import migrations, models
+
+
+def set_url_expert_course_session_assignments(apps):
+ # need to load concrete model, so that wagtail page has `specific` instance method...
+ from vbv_lernwelt.course_session.models import CourseSessionAssignment
+
+ for assignment in CourseSessionAssignment.objects.all():
+ # trigger save to update due_date foreign key fields
+ assignment.save()
+
+
+def set_url_expert_course_session_edoniq_test(apps):
+ # need to load concrete model, so that wagtail page has `specific` instance method...
+ from vbv_lernwelt.course_session.models import CourseSessionEdoniqTest
+
+ for edoniq_test in CourseSessionEdoniqTest.objects.all():
+ # trigger save to update due_date foreign key fields
+ edoniq_test.save()
+
+
+def set_url_expert_course_session_attendances(apps):
+ # need to load concrete model, so that wagtail page has `specific` instance method...
+ from vbv_lernwelt.course_session.models import CourseSessionAttendanceCourse
+
+ for attendance in CourseSessionAttendanceCourse.objects.all():
+ # trigger save to update due_date foreign key fields
+ attendance.save()
+
+
+def set_url_expert_default(apps, schema_editor):
+ set_url_expert_course_session_assignments(apps)
+ set_url_expert_course_session_attendances(apps)
+ set_url_expert_course_session_edoniq_test(apps)
+
+
+def reverse_func(apps, schema_editor):
+ # so we can reverse this migration, but noop
+ pass
+
+
+class Migration(migrations.Migration):
+ dependencies = [
+ ("duedate", "0004_alter_duedate_start"),
+ ]
+
+ operations = [
+ migrations.AddField(
+ model_name="duedate",
+ name="url_expert",
+ field=models.CharField(
+ blank=True,
+ default="",
+ help_text="URL wird aus dem LearningContent generiert (sichtbar für den Experten/Trainer)",
+ max_length=1024,
+ ),
+ ),
+ migrations.AlterField(
+ model_name="duedate",
+ name="url",
+ field=models.CharField(
+ blank=True,
+ default="",
+ help_text="URL wird vom LearningContent übernommen (sichtbar für Member/Teilnehmer)",
+ max_length=1024,
+ ),
+ ),
+ migrations.RunPython(set_url_expert_default, reverse_func),
+ ]
diff --git a/server/vbv_lernwelt/duedate/models.py b/server/vbv_lernwelt/duedate/models.py
index 5156bb59..a5ba2854 100644
--- a/server/vbv_lernwelt/duedate/models.py
+++ b/server/vbv_lernwelt/duedate/models.py
@@ -45,7 +45,13 @@ class DueDate(models.Model):
default="",
blank=True,
max_length=1024,
- help_text="URL wird vom LearningContent übernommen",
+ help_text="URL wird vom LearningContent übernommen (sichtbar für Member/Teilnehmer)",
+ )
+ url_expert = models.CharField(
+ default="",
+ blank=True,
+ max_length=1024,
+ help_text="URL wird aus dem LearningContent generiert (sichtbar für den Experten/Trainer)",
)
course_session = models.ForeignKey(
"course.CourseSession",
diff --git a/server/vbv_lernwelt/duedate/serializers.py b/server/vbv_lernwelt/duedate/serializers.py
index 425f8b01..5529a924 100644
--- a/server/vbv_lernwelt/duedate/serializers.py
+++ b/server/vbv_lernwelt/duedate/serializers.py
@@ -17,6 +17,7 @@ class DueDateSerializer(serializers.ModelSerializer):
"date_type_translation_key",
"subtitle",
"url",
+ "url_expert",
"course_session",
"page",
"circle",
@@ -24,6 +25,7 @@ class DueDateSerializer(serializers.ModelSerializer):
def get_circle(self, obj):
circle = obj.get_circle()
+
if circle:
return {
"id": circle.id,
diff --git a/server/vbv_lernwelt/importer/services.py b/server/vbv_lernwelt/importer/services.py
index 935e6f1d..be64194a 100644
--- a/server/vbv_lernwelt/importer/services.py
+++ b/server/vbv_lernwelt/importer/services.py
@@ -202,6 +202,7 @@ TRANSLATIONS = {
}
T2L_IGNORE_FIELDS = ["Vorname", "Name", "Email", "Sprache", "Durchführungen"]
+EDONIQ_TEST_PERIOD = 14
class DataImportError(Exception):
@@ -487,7 +488,9 @@ def create_or_update_course_session_edoniq_test(
# trigger save to update due date
cset.save()
- cset.deadline.start = timezone.make_aware(start) + timezone.timedelta(days=10)
+ cset.deadline.start = timezone.make_aware(start) + timezone.timedelta(
+ days=EDONIQ_TEST_PERIOD
+ )
cset.deadline.end = None
cset.deadline.save()
diff --git a/server/vbv_lernwelt/importer/tests/test_import_course_sessions.py b/server/vbv_lernwelt/importer/tests/test_import_course_sessions.py
index 6112eaa0..9e276392 100644
--- a/server/vbv_lernwelt/importer/tests/test_import_course_sessions.py
+++ b/server/vbv_lernwelt/importer/tests/test_import_course_sessions.py
@@ -309,7 +309,7 @@ class CreateOrUpdateEdoniqTestCase(TestCase):
self._create_or_update_edonqi_test("2023-06-06T11:30:00+00:00")
test = CourseSessionEdoniqTest.objects.first()
- self.assertEqual(test.deadline.start.isoformat(), "2023-06-16T11:30:00+00:00")
+ self.assertEqual(test.deadline.start.isoformat(), "2023-06-20T11:30:00+00:00")
def test_update_course_session(self):
self._create_or_update_edonqi_test("2023-06-06T11:30:00+00:00")
@@ -318,4 +318,4 @@ class CreateOrUpdateEdoniqTestCase(TestCase):
self.assertEqual(duedate_count, DueDate.objects.count())
test = CourseSessionEdoniqTest.objects.first()
- self.assertEqual(test.deadline.start.isoformat(), "2023-07-16T11:30:00+00:00")
+ self.assertEqual(test.deadline.start.isoformat(), "2023-07-20T11:30:00+00:00")
diff --git a/server/vbv_lernwelt/learnpath/models.py b/server/vbv_lernwelt/learnpath/models.py
index ba01ba54..9a1703e1 100644
--- a/server/vbv_lernwelt/learnpath/models.py
+++ b/server/vbv_lernwelt/learnpath/models.py
@@ -268,14 +268,19 @@ class LearningContent(CourseBasePage):
{self.get_admin_display_title()}
"""
- def get_frontend_url(self):
+ def get_frontend_url(self, course_session_id=None):
r = re.compile(
r"^(?P
.+?)-lp-circle-(?P.+?)-lc-(?P.+)$"
)
m = r.match(self.slug)
if m is None:
return "ERROR: could not parse slug"
- return f"/course/{m.group('coursePart')}/learn/{m.group('circlePart')}/{m.group('lcPart')}"
+ url = f"/course/{m.group('coursePart')}/learn/{m.group('circlePart')}/{m.group('lcPart')}"
+
+ if course_session_id:
+ url += f"?courseSessionId={course_session_id}"
+
+ return url
def get_parent_circle(self):
try: