From 97edbbd751b182224d7e37d7b1e229219307750b Mon Sep 17 00:00:00 2001 From: Livio Bieri Date: Mon, 25 Sep 2023 17:13:27 +0200 Subject: [PATCH 01/21] wip: expose url for expert in duedate model --- server/vbv_lernwelt/course_session/models.py | 14 +++++++++++ server/vbv_lernwelt/duedate/admin.py | 1 + .../migrations/0005_auto_20230925_1648.py | 23 +++++++++++++++++++ server/vbv_lernwelt/duedate/models.py | 8 ++++++- server/vbv_lernwelt/duedate/serializers.py | 4 ++++ 5 files changed, 49 insertions(+), 1 deletion(-) create mode 100644 server/vbv_lernwelt/duedate/migrations/0005_auto_20230925_1648.py diff --git a/server/vbv_lernwelt/course_session/models.py b/server/vbv_lernwelt/course_session/models.py index 33ac4e6b..d72199a7 100644 --- a/server/vbv_lernwelt/course_session/models.py +++ b/server/vbv_lernwelt/course_session/models.py @@ -63,7 +63,11 @@ class CourseSessionAttendanceCourse(models.Model): ) if not self.due_date.manual_override_fields: + course = self.learning_content.get_course() + self.due_date.url = self.learning_content.get_frontend_url() + # TODO: @livioso move this to the learning content? + self.due_date.url_expert = f"/course/{course.slug}/cockpit/attendance?id={self.learning_content_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 = ( @@ -130,6 +134,12 @@ class CourseSessionAssignment(models.Model): AssignmentType.REFLECTION.value: "learningContentTypes.reflection", } + # TODO @livioso move this to the learning content? + course = self.learning_content.get_course() + url_expert = ( + f"/course/{course.slug}/cockpit/assignment/{self.learning_content_id}" + ) + if assignment_type in ( AssignmentType.CASEWORK.value, AssignmentType.PREP_ASSIGNMENT.value, @@ -141,6 +151,8 @@ class CourseSessionAssignment(models.Model): if not self.submission_deadline.manual_override_fields: self.submission_deadline.url = url + # TODO: @livioso check what's expected here + 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 +172,7 @@ class CourseSessionAssignment(models.Model): if not self.evaluation_deadline.manual_override_fields: self.evaluation_deadline.url = url + self.submission_deadline.url_expert = url_expert self.evaluation_deadline.title = title self.evaluation_deadline.assignment_type_translation_key = ( assignment_type_translation_keys[assignment_type] @@ -208,6 +221,7 @@ class CourseSessionEdoniqTest(models.Model): if not self.deadline.manual_override_fields: self.deadline.url = self.learning_content.get_frontend_url() + self.deadline.expert_url = self.learning_content.get_expert_url() 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..b73e904d --- /dev/null +++ b/server/vbv_lernwelt/duedate/migrations/0005_auto_20230925_1648.py @@ -0,0 +1,23 @@ +# Generated by Django 3.2.20 on 2023-09-25 14:48 + +from django.db import migrations, models + + +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), + ), + ] 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..3e2bc427 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,9 @@ class DueDateSerializer(serializers.ModelSerializer): def get_circle(self, obj): circle = obj.get_circle() + + page = obj.page + if circle: return { "id": circle.id, From 9fc62607811666c72665122a9bec29f1ce6b7490 Mon Sep 17 00:00:00 2001 From: Livio Bieri Date: Mon, 25 Sep 2023 17:15:33 +0200 Subject: [PATCH 02/21] revert: snafu unused debugging variable --- server/vbv_lernwelt/duedate/serializers.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/server/vbv_lernwelt/duedate/serializers.py b/server/vbv_lernwelt/duedate/serializers.py index 3e2bc427..5529a924 100644 --- a/server/vbv_lernwelt/duedate/serializers.py +++ b/server/vbv_lernwelt/duedate/serializers.py @@ -26,8 +26,6 @@ class DueDateSerializer(serializers.ModelSerializer): def get_circle(self, obj): circle = obj.get_circle() - page = obj.page - if circle: return { "id": circle.id, From 18bbc6701c0c1b7270ca29acf4aa1ead8bade2fc Mon Sep 17 00:00:00 2001 From: Livio Bieri Date: Mon, 25 Sep 2023 17:24:50 +0200 Subject: [PATCH 03/21] fix: due date CI errors fixes --- server/vbv_lernwelt/course_session/models.py | 2 +- .../migrations/0005_auto_20230925_1648.py | 25 +++++++++++++------ 2 files changed, 18 insertions(+), 9 deletions(-) diff --git a/server/vbv_lernwelt/course_session/models.py b/server/vbv_lernwelt/course_session/models.py index d72199a7..e54aa503 100644 --- a/server/vbv_lernwelt/course_session/models.py +++ b/server/vbv_lernwelt/course_session/models.py @@ -221,7 +221,7 @@ class CourseSessionEdoniqTest(models.Model): if not self.deadline.manual_override_fields: self.deadline.url = self.learning_content.get_frontend_url() - self.deadline.expert_url = self.learning_content.get_expert_url() + self.deadline.expert_url = self.learning_content.get_frontend_url() 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/migrations/0005_auto_20230925_1648.py b/server/vbv_lernwelt/duedate/migrations/0005_auto_20230925_1648.py index b73e904d..cc71a2be 100644 --- a/server/vbv_lernwelt/duedate/migrations/0005_auto_20230925_1648.py +++ b/server/vbv_lernwelt/duedate/migrations/0005_auto_20230925_1648.py @@ -4,20 +4,29 @@ from django.db import migrations, models class Migration(migrations.Migration): - dependencies = [ - ('duedate', '0004_alter_duedate_start'), + ("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), + 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), + 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, + ), ), ] From 4672698895ed7d4d8d803f9e3733ccdc7c75fae4 Mon Sep 17 00:00:00 2001 From: Livio Bieri Date: Thu, 28 Sep 2023 17:08:22 +0200 Subject: [PATCH 04/21] wip: appointment filtering poc --- .../components/header/MainNavigationBar.vue | 20 ++- client/src/pages/AppointmentsPage.vue | 118 ++++++++++++++++++ client/src/router/index.ts | 4 + client/src/utils/route.ts | 14 ++- 4 files changed, 153 insertions(+), 3 deletions(-) create mode 100644 client/src/pages/AppointmentsPage.vue diff --git a/client/src/components/header/MainNavigationBar.vue b/client/src/components/header/MainNavigationBar.vue index 09de45af..75792952 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, + inDueDates, +} = useRouteLookups(); const { t } = useTranslation(); const state = reactive({ @@ -159,6 +165,16 @@ onMounted(() => {
+ + + + +import { computed, ref, watch } from "vue"; +import { useCourseSessionsStore } from "@/stores/courseSessions"; +import { useLearningPathStore } from "@/stores/learningPath"; +import ItDropdownSelect from "@/components/ui/ItDropdownSelect.vue"; + +const UNFILTERED = Number.MAX_SAFE_INTEGER; + +const courseSessionsStore = useCourseSessionsStore(); +const learningPathStore = useLearningPathStore(); + +function getCourseSession(id: number) { + return courseSessionsStore.allCourseSessions.find((cs) => cs.id === id); +} + +const allItem = { + id: UNFILTERED, + name: "Alle", +}; + +const circles = ref([{ ...allItem }]); + +const courseSessions = [ + { ...allItem }, + ...courseSessionsStore.allCourseSessions.map((courseSession) => ({ + id: courseSession.id, + name: courseSession.title, + })), +]; + +const selectedSession = ref(courseSessions[0]); +const selectedCircle = ref(circles.value[0]); + +watch(selectedSession, async () => { + const cs = getCourseSession(selectedSession.value.id); + + if (selectedSession.value.id === UNFILTERED || !cs) { + circles.value = [{ ...allItem }]; + selectedCircle.value = circles.value[0]; + return; + } + + const data = await learningPathStore.loadLearningPath( + cs.course.slug + "-lp", + undefined, + false, + false + ); + + circles.value = + data?.circles.map((circle) => ({ + id: circle.id, + name: circle.title, + })) || []; + + circles.value = [{ ...allItem }, ...circles.value]; + selectedCircle.value = circles.value[0]; +}); + +const appointments = computed(() => { + console.log("recomputing appointments"); + console.log("current session", selectedSession.value.name); + console.log("current circle", selectedCircle.value.name); + const filtered = courseSessionsStore + .allDueDates() + .filter( + (appointment) => + // first filter by course session + selectedSession.value.id === UNFILTERED || + appointment.course_session === selectedSession.value.id + ) + .filter( + (appointment) => + // then filter by circle + selectedCircle.value.id === UNFILTERED || + appointment.circle?.id === selectedCircle.value.id + ); + console.table(filtered); + return filtered; +}); + + + + + diff --git a/client/src/router/index.ts b/client/src/router/index.ts index dac7036f..80aa4add 100644 --- a/client/src/router/index.ts +++ b/client/src/router/index.ts @@ -183,6 +183,10 @@ const router = createRouter({ path: "/notifications", component: () => import("@/pages/NotificationsPage.vue"), }, + { + path: "/appointments", + component: () => import("@/pages/AppointmentsPage.vue"), + }, { path: "/styleguide", component: () => import("../pages/StyleGuidePage.vue"), diff --git a/client/src/utils/route.ts b/client/src/utils/route.ts index 71f147a2..11a566a4 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 inDueDates() { + const regex = new RegExp("/[^/]+/duedates"); + return regex.test(route.path); + } + + return { + inMediaLibrary, + inCockpit, + inLearningPath, + inCompetenceProfile, + inCourse, + inDueDates, + }; } From daaecb57a077bfddd36a9e7816d38359161064aa Mon Sep 17 00:00:00 2001 From: Livio Bieri Date: Thu, 28 Sep 2023 17:44:07 +0200 Subject: [PATCH 05/21] fix: use trainer url --- .../src/components/dueDates/DueDateSingle.vue | 19 +++++++------------ client/src/types.ts | 1 + 2 files changed, 8 insertions(+), 12 deletions(-) diff --git a/client/src/components/dueDates/DueDateSingle.vue b/client/src/components/dueDates/DueDateSingle.vue index 8a5f179e..5ba7556c 100644 --- a/client/src/components/dueDates/DueDateSingle.vue +++ b/client/src/components/dueDates/DueDateSingle.vue @@ -8,19 +8,17 @@ const props = defineProps<{ singleLine?: boolean; }>(); -/* FIXME @livioso 19.09.23: This is a temporary workaround to have a ship-able / deployable - version of the preview feature (VBV-516). The plan is to tackle the role-based - due dates calendar next (VBV-524) which will touch all usage of this component. - For now, just disable links for trainer / expert -> to reduce level of confusion ;) -*/ const courseSessionsStore = useCourseSessionsStore(); const courseSession = courseSessionsStore.allCourseSessions.find( (cs: CourseSession) => cs.id === props.dueDate.course_session ); -const disableLink = courseSession - ? !courseSessionsStore.hasCockpit(courseSession) - : false; +if (!courseSession) { + throw new Error("Course session not found"); +} + +const isExpert = courseSessionsStore.hasCockpit(courseSession); +const url = isExpert ? props.dueDate.url_expert : props.dueDate.url; - + From 13581389c0dc0e7982628edee7c975bda673bd1e Mon Sep 17 00:00:00 2001 From: Livio Bieri Date: Mon, 2 Oct 2023 13:53:39 +0200 Subject: [PATCH 11/21] fix: navigation --- .../components/header/MainNavigationBar.vue | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/client/src/components/header/MainNavigationBar.vue b/client/src/components/header/MainNavigationBar.vue index 75792952..e58a89a6 100644 --- a/client/src/components/header/MainNavigationBar.vue +++ b/client/src/components/header/MainNavigationBar.vue @@ -29,7 +29,7 @@ const { inCourse, inLearningPath, inMediaLibrary, - inDueDates, + inAppointments, } = useRouteLookups(); const { t } = useTranslation(); @@ -49,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"); }); @@ -167,12 +176,12 @@ onMounted(() => {
- + Date: Mon, 2 Oct 2023 15:09:37 +0200 Subject: [PATCH 12/21] chore: due dates list refactor --- .../src/components/dueDates/DueDatesList.vue | 20 +++++++--- .../components/dueDates/DueDatesShortList.vue | 2 + .../components/header/MainNavigationBar.vue | 20 +++++----- client/src/pages/AppointmentsPage.vue | 40 ++++++++++++------- client/src/pages/DashboardPage.vue | 2 + 5 files changed, 55 insertions(+), 29 deletions(-) diff --git a/client/src/components/dueDates/DueDatesList.vue b/client/src/components/dueDates/DueDatesList.vue index 4ba57f1f..5b02c6a6 100644 --- a/client/src/components/dueDates/DueDatesList.vue +++ b/client/src/components/dueDates/DueDatesList.vue @@ -7,6 +7,8 @@ const props = defineProps<{ maxCount: number; dueDates: DueDate[]; showTopBorder: boolean; + showBottomBorder: boolean; + showAllDueDatesLink: boolean; }>(); const allDueDates = computed(() => { @@ -20,7 +22,7 @@ const dueDatesDisplayed = computed(() => { + + diff --git a/client/src/components/dueDates/DueDatesShortList.vue b/client/src/components/dueDates/DueDatesShortList.vue index 752206e6..166a4d40 100644 --- a/client/src/components/dueDates/DueDatesShortList.vue +++ b/client/src/components/dueDates/DueDatesShortList.vue @@ -4,6 +4,8 @@ :due-dates="allDueDates" :max-count="props.maxCount" :show-top-border="props.showTopBorder" + show-all-due-dates-link + show-bottom-border >
diff --git a/client/src/components/header/MainNavigationBar.vue b/client/src/components/header/MainNavigationBar.vue index e58a89a6..c95e586e 100644 --- a/client/src/components/header/MainNavigationBar.vue +++ b/client/src/components/header/MainNavigationBar.vue @@ -174,16 +174,6 @@ onMounted(() => {
- - - - { + + + +
-
    -
  • - -
  • -
-
- {{ $t("dueDates.noDueDatesAvailable") }} -
+ +
diff --git a/client/src/pages/DashboardPage.vue b/client/src/pages/DashboardPage.vue index 4a2b8523..e08ff039 100644 --- a/client/src/pages/DashboardPage.vue +++ b/client/src/pages/DashboardPage.vue @@ -71,6 +71,8 @@ const getNextStepLink = (courseSession: CourseSession) => { :due-dates="allDueDates" :max-count="13" :show-top-border="false" + :show-all-due-dates-link="true" + :show-bottom-border="true" > From 79d4246b8873948cf68c1a5c8f6b7d40e08be49c Mon Sep 17 00:00:00 2001 From: Livio Bieri Date: Mon, 2 Oct 2023 16:22:21 +0200 Subject: [PATCH 13/21] chore: some tests --- client/src/pages/AppointmentsPage.vue | 3 ++ cypress/e2e/appointments.cy.js | 49 +++++++++++++++++++++++++++ 2 files changed, 52 insertions(+) create mode 100644 cypress/e2e/appointments.cy.js diff --git a/client/src/pages/AppointmentsPage.vue b/client/src/pages/AppointmentsPage.vue index cb251c83..3518ab65 100644 --- a/client/src/pages/AppointmentsPage.vue +++ b/client/src/pages/AppointmentsPage.vue @@ -117,12 +117,14 @@ async function loadAdditionalAppointments() {