diff --git a/client/src/pages/learningPath/circlePage/DocumentListItem.vue b/client/src/components/circle/DocumentListItem.vue similarity index 79% rename from client/src/pages/learningPath/circlePage/DocumentListItem.vue rename to client/src/components/circle/DocumentListItem.vue index c9b4632f..b11f8327 100644 --- a/client/src/pages/learningPath/circlePage/DocumentListItem.vue +++ b/client/src/components/circle/DocumentListItem.vue @@ -20,14 +20,19 @@ diff --git a/client/src/components/dueDates/DueDateSingle.vue b/client/src/components/dueDates/DueDateSingle.vue index 8a5f179e..45148417 100644 --- a/client/src/components/dueDates/DueDateSingle.vue +++ b/client/src/components/dueDates/DueDateSingle.vue @@ -1,26 +1,41 @@ diff --git a/client/src/components/dueDates/DueDatesList.vue b/client/src/components/dueDates/DueDatesList.vue index 4ba57f1f..f5ce0702 100644 --- a/client/src/components/dueDates/DueDatesList.vue +++ b/client/src/components/dueDates/DueDatesList.vue @@ -7,6 +7,9 @@ const props = defineProps<{ maxCount: number; dueDates: DueDate[]; showTopBorder: boolean; + showBottomBorder: boolean; + showAllDueDatesLink: boolean; + showCourseSession: boolean; }>(); const allDueDates = computed(() => { @@ -20,20 +23,32 @@ const dueDatesDisplayed = computed(() => { + + diff --git a/client/src/components/dueDates/DueDatesShortList.vue b/client/src/components/dueDates/DueDatesShortList.vue index 752206e6..48bb324f 100644 --- a/client/src/components/dueDates/DueDatesShortList.vue +++ b/client/src/components/dueDates/DueDatesShortList.vue @@ -4,6 +4,9 @@ :due-dates="allDueDates" :max-count="props.maxCount" :show-top-border="props.showTopBorder" + show-all-due-dates-link + show-bottom-border + :show-course-session="false" > diff --git a/client/src/components/header/AccountMenu.vue b/client/src/components/header/AccountMenu.vue index 3ed1bc55..a6f0db29 100644 --- a/client/src/components/header/AccountMenu.vue +++ b/client/src/components/header/AccountMenu.vue @@ -18,7 +18,7 @@ const logout = () => { userStore.handleLogout(); }; const selectCourseSession = (courseSession: CourseSession) => { - courseSessionsStore.switchCourseSession(courseSession); + courseSessionsStore.switchCourseSessionById(courseSession.id); }; const courseSessionsStore = useCourseSessionsStore(); diff --git a/client/src/components/header/MainNavigationBar.vue b/client/src/components/header/MainNavigationBar.vue index 09de45af..c95e586e 100644 --- a/client/src/components/header/MainNavigationBar.vue +++ b/client/src/components/header/MainNavigationBar.vue @@ -23,8 +23,14 @@ const breakpoints = useBreakpoints(breakpointsTailwind); const userStore = useUserStore(); const courseSessionsStore = useCourseSessionsStore(); const notificationsStore = useNotificationsStore(); -const { inCockpit, inCompetenceProfile, inCourse, inLearningPath, inMediaLibrary } = - useRouteLookups(); +const { + inCockpit, + inCompetenceProfile, + inCourse, + inLearningPath, + inMediaLibrary, + inAppointments, +} = useRouteLookups(); const { t } = useTranslation(); const state = reactive({ @@ -43,6 +49,15 @@ const selectedCourseSessionTitle = computed(() => { return courseSessionsStore.currentCourseSession?.title; }); +const appointmentsUrl = computed(() => { + const currentCourseSession = courseSessionsStore.currentCourseSession; + if (currentCourseSession) { + return `/course/${currentCourseSession.course.slug}/appointments`; + } else { + return `/appointments`; + } +}); + onMounted(() => { log.debug("MainNavigationBar mounted"); }); @@ -169,6 +184,16 @@ onMounted(() => { + + + + diff --git a/client/src/pages/cockpit/cockpitPage/CockpitDates.vue b/client/src/pages/cockpit/cockpitPage/CockpitDates.vue index c3724c96..f1cea728 100644 --- a/client/src/pages/cockpit/cockpitPage/CockpitDates.vue +++ b/client/src/pages/cockpit/cockpitPage/CockpitDates.vue @@ -9,9 +9,9 @@ const courseSession = useCurrentCourseSession(); const circleDates = computed(() => { const dueDates = courseSession.value.due_dates.filter((dueDate) => { - if (!cockpitStore.selectedCircle) return false; + if (!cockpitStore.currentCircle) return false; return ( - cockpitStore.selectedCircle.translation_key == dueDate?.circle?.translation_key + cockpitStore.currentCircle.translation_key == dueDate?.circle?.translation_key ); }); return dueDates.slice(0, 4); diff --git a/client/src/pages/cockpit/cockpitPage/CockpitPage.vue b/client/src/pages/cockpit/cockpitPage/CockpitPage.vue index 1bc0ffac..99c0668b 100644 --- a/client/src/pages/cockpit/cockpitPage/CockpitPage.vue +++ b/client/src/pages/cockpit/cockpitPage/CockpitPage.vue @@ -10,6 +10,7 @@ import { useCompetenceStore } from "@/stores/competence"; import { useLearningPathStore } from "@/stores/learningPath"; import log from "loglevel"; import CockpitDates from "@/pages/cockpit/cockpitPage/CockpitDates.vue"; +import { useCourseSessionsStore } from "@/stores/courseSessions"; import ItDropdownSelect from "@/components/ui/ItDropdownSelect.vue"; const props = defineProps<{ @@ -22,12 +23,13 @@ const cockpitStore = useCockpitStore(); const competenceStore = useCompetenceStore(); const learningPathStore = useLearningPathStore(); const courseSession = useCurrentCourseSession(); +const courseSessionsStore = useCourseSessionsStore(); function userCountStatusForCircle(userId: string) { - if (!cockpitStore.selectedCircle) return { FAIL: 0, SUCCESS: 0, UNKNOWN: 0 }; + if (!cockpitStore.currentCircle) return { FAIL: 0, SUCCESS: 0, UNKNOWN: 0 }; const criteria = competenceStore.flatPerformanceCriteria( userId, - cockpitStore.selectedCircle.id + cockpitStore.currentCircle.id ); return competenceStore.calcStatusCount(criteria); } @@ -35,146 +37,173 @@ function userCountStatusForCircle(userId: string) { -
- -
- - - - - -
-

- {{ $t("circlePage.documents.trainerTitle") }} -

-
- {{ $t("circlePage.documents.trainerDescription") }} -
- - {{ $t("circlePage.documents.trainerLinkText") }} -
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: