From ff61c3ba997ac65fe20521ba1b57d762ba6900cf Mon Sep 17 00:00:00 2001 From: Hangzhi Yu <hangzhi@protonmail.com> Date: Sat, 11 May 2024 23:24:14 +0200 Subject: [PATCH 01/34] Allow for including courses of child groups in TCC raster --- .../TimeboundCourseConfigRaster.vue | 37 +++++++++++++++---- .../timeboundCourseConfig.graphql | 8 +++- .../apps/lesrooster/frontend/messages/en.json | 5 ++- aleksis/apps/lesrooster/schema/__init__.py | 15 +++++--- 4 files changed, 51 insertions(+), 14 deletions(-) diff --git a/aleksis/apps/lesrooster/frontend/components/timebound_course_config/TimeboundCourseConfigRaster.vue b/aleksis/apps/lesrooster/frontend/components/timebound_course_config/TimeboundCourseConfigRaster.vue index e6f52d9b..5fffea52 100644 --- a/aleksis/apps/lesrooster/frontend/components/timebound_course_config/TimeboundCourseConfigRaster.vue +++ b/aleksis/apps/lesrooster/frontend/components/timebound_course_config/TimeboundCourseConfigRaster.vue @@ -55,6 +55,18 @@ import SubjectChip from "aleksis.apps.cursus/components/SubjectChip.vue"; /> </v-col> + <v-col + cols="6" + lg="2" + class="d-flex justify-space-between flex-wrap align-center" + > + <v-switch + v-model="includeChildGroups" + inset + :label="$t('lesrooster.timebound_course_config.filters.include_child_groups')" + ></v-switch> + </v-col> + <v-spacer /> <v-col @@ -234,6 +246,7 @@ export default { currentCourse: null, currentSubject: null, loading: false, + includeChildGroups: true, }; }, methods: { @@ -435,16 +448,25 @@ export default { let groupCombinations = {}; subject.courses.forEach((course) => { - let groupIds = JSON.stringify( - course.groups.map((group) => group.id).sort(), - ); + const ownGroupIDs = course.groups.map((group) => group.id); + let groupIDs; + + if (this.includeChildGroups && ownGroupIDs.some((groupID) => !this.groupIDList.includes(groupID))) { + groupIDs = JSON.stringify( + course.groups.map((group) => group.parentGroups.map((parentGroup) => parentGroup.id)).flat().sort() + ); + } else { + groupIDs = JSON.stringify( + ownGroupIDs.sort() + ); + } - if (!groupCombinations[groupIds]) { - groupCombinations[groupIds] = []; + if (!groupCombinations[groupIDs]) { + groupCombinations[groupIDs] = []; } - if (!groupCombinations[groupIds].find((c) => c.id === course.id)) { - groupCombinations[groupIds].push({ + if (!groupCombinations[groupIDs].find((c) => c.id === course.id)) { + groupCombinations[groupIDs].push({ ...course, }); } @@ -505,6 +527,7 @@ export default { variables() { return { groups: this.groupIDList, + includeChildGroups: this.includeChildGroups, }; }, }, diff --git a/aleksis/apps/lesrooster/frontend/components/timebound_course_config/timeboundCourseConfig.graphql b/aleksis/apps/lesrooster/frontend/components/timebound_course_config/timeboundCourseConfig.graphql index 885c6d33..b0e4aad2 100644 --- a/aleksis/apps/lesrooster/frontend/components/timebound_course_config/timeboundCourseConfig.graphql +++ b/aleksis/apps/lesrooster/frontend/components/timebound_course_config/timeboundCourseConfig.graphql @@ -23,6 +23,11 @@ fragment courseFields on LesroosterExtendedCourseType { id name shortName + parentGroups { + id + name + shortName + } } lessonQuota } @@ -93,11 +98,12 @@ mutation updateTimeboundCourseConfigs( } } -query subjects($orderBy: [String], $filters: JSONString, $groups: [ID]) { +query subjects($orderBy: [String], $filters: JSONString, $groups: [ID], $includeChildGroups: Boolean!) { subjects: lesroosterExtendedSubjects( orderBy: $orderBy filters: $filters groups: $groups + includeChildGroups: $includeChildGroups ) { ...subjectFields courses { diff --git a/aleksis/apps/lesrooster/frontend/messages/en.json b/aleksis/apps/lesrooster/frontend/messages/en.json index 2a5da55f..02c440cd 100644 --- a/aleksis/apps/lesrooster/frontend/messages/en.json +++ b/aleksis/apps/lesrooster/frontend/messages/en.json @@ -81,7 +81,10 @@ "all_teachers": "All teachers", "no_course_selected": "No course selected", "create_timebound_course_config": "Create timebound course config", - "subject": "Subject" + "subject": "Subject", + "filters": { + "include_child_groups": "Include courses from child groups" + } }, "lesson_raster": { "menu_title": "Lesson Raster" diff --git a/aleksis/apps/lesrooster/schema/__init__.py b/aleksis/apps/lesrooster/schema/__init__.py index f432dd7f..3a51a42a 100644 --- a/aleksis/apps/lesrooster/schema/__init__.py +++ b/aleksis/apps/lesrooster/schema/__init__.py @@ -109,7 +109,7 @@ class Query(graphene.ObjectType): current_validity_range = graphene.Field(ValidityRangeType) lesrooster_extended_subjects = FilterOrderList( - LesroosterExtendedSubjectType, groups=graphene.List(graphene.ID) + LesroosterExtendedSubjectType, groups=graphene.List(graphene.ID), include_child_groups=graphene.Boolean(required=True) ) groups_by_time_grid = graphene.List(GroupType, time_grid=graphene.ID(required=True)) @@ -160,13 +160,18 @@ class Query(graphene.ObjectType): return Supervision.objects.all() @staticmethod - def resolve_lesrooster_extended_subjects(root, info, groups): + def resolve_lesrooster_extended_subjects(root, info, groups, include_child_groups): + courses = get_objects_for_user( + info.context.user, "cursus.view_course", Course.objects.all() + ) + if include_child_groups: + courses = courses.filter(Q(groups__in=groups) | Q(groups__parent_groups__in=groups)) + else: + courses = courses.filter(groups__in=groups) subjects = Subject.objects.all().prefetch_related( Prefetch( "courses", - queryset=get_objects_for_user( - info.context.user, "cursus.view_course", Course.objects.all() - ).filter(groups__in=groups), + queryset=courses, ) ) if not info.context.user.has_perm("lesrooster.view_subject_rule"): -- GitLab From df4de0b67b10881be46b06b186ebe6fe2f3c8245 Mon Sep 17 00:00:00 2001 From: Hangzhi Yu <hangzhi@protonmail.com> Date: Sun, 12 May 2024 01:43:01 +0200 Subject: [PATCH 02/34] Add loading indicators --- .../timebound_course_config/TimeboundCourseConfigRaster.vue | 3 +++ 1 file changed, 3 insertions(+) diff --git a/aleksis/apps/lesrooster/frontend/components/timebound_course_config/TimeboundCourseConfigRaster.vue b/aleksis/apps/lesrooster/frontend/components/timebound_course_config/TimeboundCourseConfigRaster.vue index 5fffea52..704b94c8 100644 --- a/aleksis/apps/lesrooster/frontend/components/timebound_course_config/TimeboundCourseConfigRaster.vue +++ b/aleksis/apps/lesrooster/frontend/components/timebound_course_config/TimeboundCourseConfigRaster.vue @@ -15,6 +15,7 @@ import SubjectChip from "aleksis.apps.cursus/components/SubjectChip.vue"; hide-default-footer :headers="headers" :items="tableItems" + :loading="$apollo.queries.subjects.loading" > <template #top> <v-row> @@ -64,6 +65,7 @@ import SubjectChip from "aleksis.apps.cursus/components/SubjectChip.vue"; v-model="includeChildGroups" inset :label="$t('lesrooster.timebound_course_config.filters.include_child_groups')" + :loading="$apollo.queries.subjects.loading" ></v-switch> </v-col> @@ -91,6 +93,7 @@ import SubjectChip from "aleksis.apps.cursus/components/SubjectChip.vue"; !createdCourseConfigs.length && !createdCourses.length " + :loading="loading" @click="save" /> </v-col> -- GitLab From e1720c322b6a82b6d463e51e7f865017ee47c77f Mon Sep 17 00:00:00 2001 From: Hangzhi Yu <hangzhi@protonmail.com> Date: Sun, 12 May 2024 01:54:46 +0200 Subject: [PATCH 03/34] Set fixed width in TCC raster --- .../timebound_course_config/TimeboundCourseConfigRaster.vue | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/aleksis/apps/lesrooster/frontend/components/timebound_course_config/TimeboundCourseConfigRaster.vue b/aleksis/apps/lesrooster/frontend/components/timebound_course_config/TimeboundCourseConfigRaster.vue index 704b94c8..dbdc7cad 100644 --- a/aleksis/apps/lesrooster/frontend/components/timebound_course_config/TimeboundCourseConfigRaster.vue +++ b/aleksis/apps/lesrooster/frontend/components/timebound_course_config/TimeboundCourseConfigRaster.vue @@ -436,7 +436,7 @@ export default { headers() { let groupHeadersWithWidth = this.groupHeaders.map((header) => ({ ...header, - width: `${Math.max(95 / this.groupHeaders.length, 15)}vw`, + width: "20vw", })); return [ { -- GitLab From ee9165da4c20712b9f45c13f546b92c3dc2913de Mon Sep 17 00:00:00 2001 From: Hangzhi Yu <hangzhi@protonmail.com> Date: Sun, 12 May 2024 20:46:01 +0200 Subject: [PATCH 04/34] Compute group combinations more efficiently and without duplicates --- .../TimeboundCourseConfigRaster.vue | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/aleksis/apps/lesrooster/frontend/components/timebound_course_config/TimeboundCourseConfigRaster.vue b/aleksis/apps/lesrooster/frontend/components/timebound_course_config/TimeboundCourseConfigRaster.vue index dbdc7cad..fcda5ff4 100644 --- a/aleksis/apps/lesrooster/frontend/components/timebound_course_config/TimeboundCourseConfigRaster.vue +++ b/aleksis/apps/lesrooster/frontend/components/timebound_course_config/TimeboundCourseConfigRaster.vue @@ -403,10 +403,12 @@ export default { return this.selectedGroups.map((group) => group.id); }, subjectGroupCombinations() { - return [].concat.apply( - [], - this.items.map((subject) => Object.keys(subject.groupCombinations)), + const uniqueCombinations = new Set( + this.items.flatMap(subject => + Object.keys(subject.groupCombinations) + ) ); + return Array.from(uniqueCombinations); }, groupHeaders() { return this.selectedGroups -- GitLab From d454ea9e4deca96a1c4938886838af3d90346491 Mon Sep 17 00:00:00 2001 From: Hangzhi Yu <hangzhi@protonmail.com> Date: Mon, 13 May 2024 13:31:24 +0200 Subject: [PATCH 05/34] Fix subject resolving mechanism --- aleksis/apps/lesrooster/schema/__init__.py | 25 +++++++++++----------- 1 file changed, 12 insertions(+), 13 deletions(-) diff --git a/aleksis/apps/lesrooster/schema/__init__.py b/aleksis/apps/lesrooster/schema/__init__.py index 3a51a42a..67b3f349 100644 --- a/aleksis/apps/lesrooster/schema/__init__.py +++ b/aleksis/apps/lesrooster/schema/__init__.py @@ -161,22 +161,21 @@ class Query(graphene.ObjectType): @staticmethod def resolve_lesrooster_extended_subjects(root, info, groups, include_child_groups): - courses = get_objects_for_user( - info.context.user, "cursus.view_course", Course.objects.all() - ) if include_child_groups: - courses = courses.filter(Q(groups__in=groups) | Q(groups__parent_groups__in=groups)) + courses = Course.objects.filter(Q(groups__in=groups) | Q(groups__parent_groups__in=groups)) else: - courses = courses.filter(groups__in=groups) - subjects = Subject.objects.all().prefetch_related( - Prefetch( - "courses", - queryset=courses, - ) + courses = Course.objects.filter(groups__in=groups) + courses = get_objects_for_user( + info.context.user, "cursus.view_course", courses ) - if not info.context.user.has_perm("lesrooster.view_subject_rule"): - return get_objects_for_user(info.context.user, "cursus.view_subject", subjects) - return subjects + subjects = get_objects_for_user(info.context.user, "cursus.view_subject", Subject.objects.all()) + + return subjects.prefetch_related( + Prefetch( + "courses", + queryset=courses, + ) + ) @staticmethod def resolve_current_validity_range(root, info): -- GitLab From 00139d2487c217ffd9310cec107e8ee4db94d253 Mon Sep 17 00:00:00 2001 From: Hangzhi Yu <hangzhi@protonmail.com> Date: Mon, 13 May 2024 13:36:02 +0200 Subject: [PATCH 06/34] Make header generation mechanism more efficient --- .../TimeboundCourseConfigRaster.vue | 69 ++++++++++--------- 1 file changed, 35 insertions(+), 34 deletions(-) diff --git a/aleksis/apps/lesrooster/frontend/components/timebound_course_config/TimeboundCourseConfigRaster.vue b/aleksis/apps/lesrooster/frontend/components/timebound_course_config/TimeboundCourseConfigRaster.vue index fcda5ff4..9a42bfee 100644 --- a/aleksis/apps/lesrooster/frontend/components/timebound_course_config/TimeboundCourseConfigRaster.vue +++ b/aleksis/apps/lesrooster/frontend/components/timebound_course_config/TimeboundCourseConfigRaster.vue @@ -250,6 +250,8 @@ export default { currentSubject: null, loading: false, includeChildGroups: true, + subjectGroupCombinations: [], + selectedGroupHeaders: [], }; }, methods: { @@ -402,38 +404,22 @@ export default { groupIDList() { return this.selectedGroups.map((group) => group.id); }, - subjectGroupCombinations() { - const uniqueCombinations = new Set( - this.items.flatMap(subject => - Object.keys(subject.groupCombinations) - ) - ); - return Array.from(uniqueCombinations); + groupCombinationHeaders() { + return this.subjectGroupCombinations.map((combination) => { + let parsedCombination = JSON.parse(combination); + return { + text: parsedCombination + .map( + (groupID) => + this.selectedGroups.find((group) => group.id === groupID)?.shortName || this.selectedGroups.find((group) => group.id === groupID)?.shortName, + ) + .join(", "), + value: combination, + }; + }); }, groupHeaders() { - return this.selectedGroups - .map((group) => ({ - text: group.shortName, - value: JSON.stringify([group.id]), - })) - .concat( - this.subjectGroupCombinations.map((combination) => { - let parsedCombination = JSON.parse(combination); - return { - text: parsedCombination - .map( - (groupID) => - this.groups.find((group) => group.id === groupID).shortName, - ) - .join(", "), - value: combination, - }; - }), - ) - .filter( - (obj, index, self) => - index === self.findIndex((o) => o.value === obj.value), - ); + return [...this.selectedGroupHeaders, ...this.groupCombinationHeaders]; }, headers() { let groupHeadersWithWidth = this.groupHeaders.map((header) => ({ @@ -449,7 +435,9 @@ export default { ].concat(groupHeadersWithWidth); }, items() { - return this.subjects.map((subject) => { + let groupCombinationsSet = new Set(); + + const groupedItems = this.subjects.map((subject) => { let groupCombinations = {}; subject.courses.forEach((course) => { @@ -468,6 +456,9 @@ export default { if (!groupCombinations[groupIDs]) { groupCombinations[groupIDs] = []; + if (course.groups.length > 1) { + groupCombinationsSet.add(groupIDs); + } } if (!groupCombinations[groupIDs].find((c) => c.id === course.id)) { @@ -489,6 +480,10 @@ export default { return subject; }); + + this.subjectGroupCombinations = Array.from(groupCombinationsSet); + + return groupedItems; }, tableItems() { return this.items.map((subject) => { @@ -505,6 +500,15 @@ export default { }); }, }, + watch: { + selectedGroups(newValue) { + this.selectedGroupHeaders = newValue + .map((group) => ({ + text: group.shortName, + value: JSON.stringify([group.id]), + })); + }, + }, apollo: { currentValidityRange: { query: gqlCurrentValidityRange, @@ -513,9 +517,6 @@ export default { this.internalValidityRange = data.currentValidityRange; }, }, - groups: { - query: gqlGroups, - }, groupsForPlanning: { query: gqlGroupsForPlanning, result({ data }) { -- GitLab From 4c12d98598653e82eb850b8435a4e922d83ef36e Mon Sep 17 00:00:00 2001 From: Hangzhi Yu <hangzhi@protonmail.com> Date: Mon, 13 May 2024 13:39:34 +0200 Subject: [PATCH 07/34] Reformat --- .../TimeboundCourseConfigRaster.vue | 42 ++++++++++++------- .../timeboundCourseConfig.graphql | 7 +++- aleksis/apps/lesrooster/schema/__init__.py | 24 ++++++----- 3 files changed, 47 insertions(+), 26 deletions(-) diff --git a/aleksis/apps/lesrooster/frontend/components/timebound_course_config/TimeboundCourseConfigRaster.vue b/aleksis/apps/lesrooster/frontend/components/timebound_course_config/TimeboundCourseConfigRaster.vue index 9a42bfee..e6e3f276 100644 --- a/aleksis/apps/lesrooster/frontend/components/timebound_course_config/TimeboundCourseConfigRaster.vue +++ b/aleksis/apps/lesrooster/frontend/components/timebound_course_config/TimeboundCourseConfigRaster.vue @@ -64,7 +64,11 @@ import SubjectChip from "aleksis.apps.cursus/components/SubjectChip.vue"; <v-switch v-model="includeChildGroups" inset - :label="$t('lesrooster.timebound_course_config.filters.include_child_groups')" + :label=" + $t( + 'lesrooster.timebound_course_config.filters.include_child_groups', + ) + " :loading="$apollo.queries.subjects.loading" ></v-switch> </v-col> @@ -409,11 +413,14 @@ export default { let parsedCombination = JSON.parse(combination); return { text: parsedCombination - .map( - (groupID) => - this.selectedGroups.find((group) => group.id === groupID)?.shortName || this.selectedGroups.find((group) => group.id === groupID)?.shortName, + .map( + (groupID) => + this.selectedGroups.find((group) => group.id === groupID) + ?.shortName || + this.selectedGroups.find((group) => group.id === groupID) + ?.shortName, ) - .join(", "), + .join(", "), value: combination, }; }); @@ -444,14 +451,20 @@ export default { const ownGroupIDs = course.groups.map((group) => group.id); let groupIDs; - if (this.includeChildGroups && ownGroupIDs.some((groupID) => !this.groupIDList.includes(groupID))) { + if ( + this.includeChildGroups && + ownGroupIDs.some((groupID) => !this.groupIDList.includes(groupID)) + ) { groupIDs = JSON.stringify( - course.groups.map((group) => group.parentGroups.map((parentGroup) => parentGroup.id)).flat().sort() + course.groups + .map((group) => + group.parentGroups.map((parentGroup) => parentGroup.id), + ) + .flat() + .sort(), ); } else { - groupIDs = JSON.stringify( - ownGroupIDs.sort() - ); + groupIDs = JSON.stringify(ownGroupIDs.sort()); } if (!groupCombinations[groupIDs]) { @@ -502,11 +515,10 @@ export default { }, watch: { selectedGroups(newValue) { - this.selectedGroupHeaders = newValue - .map((group) => ({ - text: group.shortName, - value: JSON.stringify([group.id]), - })); + this.selectedGroupHeaders = newValue.map((group) => ({ + text: group.shortName, + value: JSON.stringify([group.id]), + })); }, }, apollo: { diff --git a/aleksis/apps/lesrooster/frontend/components/timebound_course_config/timeboundCourseConfig.graphql b/aleksis/apps/lesrooster/frontend/components/timebound_course_config/timeboundCourseConfig.graphql index b0e4aad2..4f9ca031 100644 --- a/aleksis/apps/lesrooster/frontend/components/timebound_course_config/timeboundCourseConfig.graphql +++ b/aleksis/apps/lesrooster/frontend/components/timebound_course_config/timeboundCourseConfig.graphql @@ -98,7 +98,12 @@ mutation updateTimeboundCourseConfigs( } } -query subjects($orderBy: [String], $filters: JSONString, $groups: [ID], $includeChildGroups: Boolean!) { +query subjects( + $orderBy: [String] + $filters: JSONString + $groups: [ID] + $includeChildGroups: Boolean! +) { subjects: lesroosterExtendedSubjects( orderBy: $orderBy filters: $filters diff --git a/aleksis/apps/lesrooster/schema/__init__.py b/aleksis/apps/lesrooster/schema/__init__.py index 67b3f349..26e612a0 100644 --- a/aleksis/apps/lesrooster/schema/__init__.py +++ b/aleksis/apps/lesrooster/schema/__init__.py @@ -109,7 +109,9 @@ class Query(graphene.ObjectType): current_validity_range = graphene.Field(ValidityRangeType) lesrooster_extended_subjects = FilterOrderList( - LesroosterExtendedSubjectType, groups=graphene.List(graphene.ID), include_child_groups=graphene.Boolean(required=True) + LesroosterExtendedSubjectType, + groups=graphene.List(graphene.ID), + include_child_groups=graphene.Boolean(required=True), ) groups_by_time_grid = graphene.List(GroupType, time_grid=graphene.ID(required=True)) @@ -162,20 +164,22 @@ class Query(graphene.ObjectType): @staticmethod def resolve_lesrooster_extended_subjects(root, info, groups, include_child_groups): if include_child_groups: - courses = Course.objects.filter(Q(groups__in=groups) | Q(groups__parent_groups__in=groups)) + courses = Course.objects.filter( + Q(groups__in=groups) | Q(groups__parent_groups__in=groups) + ) else: courses = Course.objects.filter(groups__in=groups) - courses = get_objects_for_user( - info.context.user, "cursus.view_course", courses + courses = get_objects_for_user(info.context.user, "cursus.view_course", courses) + subjects = get_objects_for_user( + info.context.user, "cursus.view_subject", Subject.objects.all() ) - subjects = get_objects_for_user(info.context.user, "cursus.view_subject", Subject.objects.all()) return subjects.prefetch_related( - Prefetch( - "courses", - queryset=courses, - ) - ) + Prefetch( + "courses", + queryset=courses, + ) + ) @staticmethod def resolve_current_validity_range(root, info): -- GitLab From f222ea864e2a90a35cb6eb778291a5ec917050b1 Mon Sep 17 00:00:00 2001 From: Hangzhi Yu <hangzhi@protonmail.com> Date: Mon, 13 May 2024 16:23:35 +0200 Subject: [PATCH 08/34] Some optimisations in frontend --- .../TimeboundCourseConfigRaster.vue | 157 +++++++----------- 1 file changed, 62 insertions(+), 95 deletions(-) diff --git a/aleksis/apps/lesrooster/frontend/components/timebound_course_config/TimeboundCourseConfigRaster.vue b/aleksis/apps/lesrooster/frontend/components/timebound_course_config/TimeboundCourseConfigRaster.vue index e6e3f276..b9dcaafc 100644 --- a/aleksis/apps/lesrooster/frontend/components/timebound_course_config/TimeboundCourseConfigRaster.vue +++ b/aleksis/apps/lesrooster/frontend/components/timebound_course_config/TimeboundCourseConfigRaster.vue @@ -14,7 +14,7 @@ import SubjectChip from "aleksis.apps.cursus/components/SubjectChip.vue"; disable-pagination hide-default-footer :headers="headers" - :items="tableItems" + :items="items" :loading="$apollo.queries.subjects.loading" > <template #top> @@ -248,14 +248,14 @@ export default { subjects: [], editedCourseConfigs: [], createdCourseConfigs: [], - newCourses: [], createdCourses: [], currentCourse: null, currentSubject: null, loading: false, includeChildGroups: true, - subjectGroupCombinations: [], selectedGroupHeaders: [], + items: [], + groupCombinationsSet: new Set(), }; }, methods: { @@ -383,30 +383,62 @@ export default { ]; }, addCourse(subject, groups) { - let courseSubjectGroup = this.newCourses.find( - (courseSubject) => courseSubject.subject === subject, - ); - if (courseSubjectGroup) { - if (courseSubjectGroup.groupCombinations) { - this.$set(courseSubjectGroup.groupCombinations, groups, [ + this.$set(this.items.find((i) => i.subject.id === subject), groups, [ { teachers: [], newCourse: true }, - ]); - } else { - courseSubjectGroup.groupCombinations = { - [groups]: [{ teachers: [], newCourse: true }], - }; - } - } else { - this.newCourses.push({ - subject: subject, - groupCombinations: { [groups]: [{ teachers: [], newCourse: true }] }, + ]); + }, + generateTableItems(subjects) { + return subjects.map((subject) => { + let { courses, ...reducedSubject } = subject; + let groupCombinations = {}; + + courses.forEach((course) => { + const ownGroupIDs = course.groups.map((group) => group.id); + let groupIDs; + + if ( + this.includeChildGroups && + ownGroupIDs.some((groupID) => !this.groupIDSet.has(groupID)) + ) { + groupIDs = JSON.stringify( + course.groups + .flatMap((group) => + group.parentGroups.map((parentGroup) => parentGroup.id), + ) + .sort(), + ); + } else { + groupIDs = JSON.stringify(ownGroupIDs.sort()); + } + + if (!groupCombinations[groupIDs]) { + groupCombinations[groupIDs] = []; + if (course.groups.length > 1) { + this.groupCombinationsSet.add(groupIDs); + } + groupCombinations[groupIDs].push({ + ...course, + }); + } else if (!groupCombinations[groupIDs].some((c) => c.id === course.id)) { + groupCombinations[groupIDs].push({ + ...course, + }); + } }); - } + + return { + subject: reducedSubject, + ...Object.fromEntries( + this.groupHeaders.map((header) => [header.value, []]), + ), + ...groupCombinations, + }; + }); }, }, computed: { - groupIDList() { - return this.selectedGroups.map((group) => group.id); + groupIDSet() { + return new Set(this.selectedGroups.map((group) => group.id)); }, groupCombinationHeaders() { return this.subjectGroupCombinations.map((combination) => { @@ -441,76 +473,8 @@ export default { }, ].concat(groupHeadersWithWidth); }, - items() { - let groupCombinationsSet = new Set(); - - const groupedItems = this.subjects.map((subject) => { - let groupCombinations = {}; - - subject.courses.forEach((course) => { - const ownGroupIDs = course.groups.map((group) => group.id); - let groupIDs; - - if ( - this.includeChildGroups && - ownGroupIDs.some((groupID) => !this.groupIDList.includes(groupID)) - ) { - groupIDs = JSON.stringify( - course.groups - .map((group) => - group.parentGroups.map((parentGroup) => parentGroup.id), - ) - .flat() - .sort(), - ); - } else { - groupIDs = JSON.stringify(ownGroupIDs.sort()); - } - - if (!groupCombinations[groupIDs]) { - groupCombinations[groupIDs] = []; - if (course.groups.length > 1) { - groupCombinationsSet.add(groupIDs); - } - } - - if (!groupCombinations[groupIDs].find((c) => c.id === course.id)) { - groupCombinations[groupIDs].push({ - ...course, - }); - } - }); - - subject = { - ...subject, - groupCombinations: { ...groupCombinations }, - newCourses: { - ...this.newCourses.find( - (courseSubject) => courseSubject.subject === subject.id, - )?.groupCombinations, - }, - }; - - return subject; - }); - - this.subjectGroupCombinations = Array.from(groupCombinationsSet); - - return groupedItems; - }, - tableItems() { - return this.items.map((subject) => { - // eslint-disable-next-line no-unused-vars - let { courses, groupCombinations, ...reducedSubject } = subject; - return { - subject: reducedSubject, - ...Object.fromEntries( - this.groupHeaders.map((header) => [header.value, []]), - ), - ...subject.groupCombinations, - ...subject.newCourses, - }; - }); + subjectGroupCombinations() { + return Array.from(this.groupCombinationsSet); }, }, watch: { @@ -533,21 +497,24 @@ export default { query: gqlGroupsForPlanning, result({ data }) { if (!data) return; - console.log(data.groups); this.selectedGroups = data.groupsForPlanning; }, }, subjects: { query: subjects, skip() { - return !this.groupIDList.length; + return !this.groupIDSet.size || !this.internalValidityRange; }, variables() { return { - groups: this.groupIDList, + groups: Array.from(this.groupIDSet), includeChildGroups: this.includeChildGroups, }; }, + result({ data }) { + if (!data) return; + this.items = this.generateTableItems(data.subjects); + }, }, persons: { query: gqlTeachers, -- GitLab From 6cae2debe76c80e8c043c154dec54ac43764efc0 Mon Sep 17 00:00:00 2001 From: Hangzhi Yu <hangzhi@protonmail.com> Date: Mon, 13 May 2024 16:23:57 +0200 Subject: [PATCH 09/34] Reformat --- .../TimeboundCourseConfigRaster.vue | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/aleksis/apps/lesrooster/frontend/components/timebound_course_config/TimeboundCourseConfigRaster.vue b/aleksis/apps/lesrooster/frontend/components/timebound_course_config/TimeboundCourseConfigRaster.vue index b9dcaafc..3b56b428 100644 --- a/aleksis/apps/lesrooster/frontend/components/timebound_course_config/TimeboundCourseConfigRaster.vue +++ b/aleksis/apps/lesrooster/frontend/components/timebound_course_config/TimeboundCourseConfigRaster.vue @@ -383,9 +383,11 @@ export default { ]; }, addCourse(subject, groups) { - this.$set(this.items.find((i) => i.subject.id === subject), groups, [ - { teachers: [], newCourse: true }, - ]); + this.$set( + this.items.find((i) => i.subject.id === subject), + groups, + [{ teachers: [], newCourse: true }], + ); }, generateTableItems(subjects) { return subjects.map((subject) => { @@ -419,7 +421,9 @@ export default { groupCombinations[groupIDs].push({ ...course, }); - } else if (!groupCombinations[groupIDs].some((c) => c.id === course.id)) { + } else if ( + !groupCombinations[groupIDs].some((c) => c.id === course.id) + ) { groupCombinations[groupIDs].push({ ...course, }); -- GitLab From 53a38d26be2cb8b64663c6200b142f95e2194e79 Mon Sep 17 00:00:00 2001 From: Hangzhi Yu <hangzhi@protonmail.com> Date: Tue, 14 May 2024 12:02:12 +0200 Subject: [PATCH 10/34] Move filtering of TCCs for ValidityRange to backend --- .../TimeboundCourseConfigRaster.vue | 17 ++++------------- .../timeboundCourseConfig.graphql | 2 ++ aleksis/apps/lesrooster/schema/__init__.py | 6 ++++-- 3 files changed, 10 insertions(+), 15 deletions(-) diff --git a/aleksis/apps/lesrooster/frontend/components/timebound_course_config/TimeboundCourseConfigRaster.vue b/aleksis/apps/lesrooster/frontend/components/timebound_course_config/TimeboundCourseConfigRaster.vue index 3b56b428..8aee25e3 100644 --- a/aleksis/apps/lesrooster/frontend/components/timebound_course_config/TimeboundCourseConfigRaster.vue +++ b/aleksis/apps/lesrooster/frontend/components/timebound_course_config/TimeboundCourseConfigRaster.vue @@ -264,15 +264,7 @@ export default { }, getCurrentCourseConfig(course) { if (course.lrTimeboundCourseConfigs?.length) { - let currentCourseConfigs = course.lrTimeboundCourseConfigs.filter( - (timeboundConfig) => - timeboundConfig.validityRange.id === this.internalValidityRange.id, - ); - if (currentCourseConfigs.length) { - return currentCourseConfigs[0]; - } else { - return null; - } + return course.lrTimeboundCourseConfigs[0]; } else { return null; } @@ -296,9 +288,7 @@ export default { } } else { if ( - !course.lrTimeboundCourseConfigs?.filter( - (c) => c.validityRange.id === this.internalValidityRange?.id, - ).length + !course.lrTimeboundCourseConfigs?.length ) { let existingCreatedCourseConfig = this.createdCourseConfigs.find( (c) => @@ -507,12 +497,13 @@ export default { subjects: { query: subjects, skip() { - return !this.groupIDSet.size || !this.internalValidityRange; + return !this.groupIDSet.size || !this.internalValidityRange || !this.persons.length; }, variables() { return { groups: Array.from(this.groupIDSet), includeChildGroups: this.includeChildGroups, + validityRange: this.internalValidityRange.id, }; }, result({ data }) { diff --git a/aleksis/apps/lesrooster/frontend/components/timebound_course_config/timeboundCourseConfig.graphql b/aleksis/apps/lesrooster/frontend/components/timebound_course_config/timeboundCourseConfig.graphql index 4f9ca031..64749799 100644 --- a/aleksis/apps/lesrooster/frontend/components/timebound_course_config/timeboundCourseConfig.graphql +++ b/aleksis/apps/lesrooster/frontend/components/timebound_course_config/timeboundCourseConfig.graphql @@ -103,12 +103,14 @@ query subjects( $filters: JSONString $groups: [ID] $includeChildGroups: Boolean! + $validityRange: ID! ) { subjects: lesroosterExtendedSubjects( orderBy: $orderBy filters: $filters groups: $groups includeChildGroups: $includeChildGroups + validityRange: $validityRange ) { ...subjectFields courses { diff --git a/aleksis/apps/lesrooster/schema/__init__.py b/aleksis/apps/lesrooster/schema/__init__.py index 26e612a0..50fff730 100644 --- a/aleksis/apps/lesrooster/schema/__init__.py +++ b/aleksis/apps/lesrooster/schema/__init__.py @@ -112,6 +112,7 @@ class Query(graphene.ObjectType): LesroosterExtendedSubjectType, groups=graphene.List(graphene.ID), include_child_groups=graphene.Boolean(required=True), + validity_range=graphene.ID(required=True) ) groups_by_time_grid = graphene.List(GroupType, time_grid=graphene.ID(required=True)) @@ -162,14 +163,15 @@ class Query(graphene.ObjectType): return Supervision.objects.all() @staticmethod - def resolve_lesrooster_extended_subjects(root, info, groups, include_child_groups): + def resolve_lesrooster_extended_subjects(root, info, groups, include_child_groups, validity_range): if include_child_groups: courses = Course.objects.filter( Q(groups__in=groups) | Q(groups__parent_groups__in=groups) ) else: courses = Course.objects.filter(groups__in=groups) - courses = get_objects_for_user(info.context.user, "cursus.view_course", courses) + course_configs = TimeboundCourseConfig.objects.filter(validity_range=validity_range) + courses = get_objects_for_user(info.context.user, "cursus.view_course", courses).prefetch_related(Prefetch("lr_timebound_course_configs", queryset=course_configs)) subjects = get_objects_for_user( info.context.user, "cursus.view_subject", Subject.objects.all() ) -- GitLab From 303ce99e0f40b8cc61a947e7f2a7f980e336ee2f Mon Sep 17 00:00:00 2001 From: Hangzhi Yu <hangzhi@protonmail.com> Date: Tue, 14 May 2024 12:21:59 +0200 Subject: [PATCH 11/34] Move course name generation to backend --- .../timebound_course_config/TimeboundCourseConfigRaster.vue | 1 - aleksis/apps/lesrooster/schema/timebound_course_config.py | 3 ++- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/aleksis/apps/lesrooster/frontend/components/timebound_course_config/TimeboundCourseConfigRaster.vue b/aleksis/apps/lesrooster/frontend/components/timebound_course_config/TimeboundCourseConfigRaster.vue index d3c3d5d2..e9a902dd 100644 --- a/aleksis/apps/lesrooster/frontend/components/timebound_course_config/TimeboundCourseConfigRaster.vue +++ b/aleksis/apps/lesrooster/frontend/components/timebound_course_config/TimeboundCourseConfigRaster.vue @@ -281,7 +281,6 @@ export default { this.createdCourses.push({ subject: subject.id, groups: JSON.parse(header.value), - name: `${header.text}-${subject.name}`, schoolTerm: this.internalValidityRange.schoolTerm.id, ...newValue, }); diff --git a/aleksis/apps/lesrooster/schema/timebound_course_config.py b/aleksis/apps/lesrooster/schema/timebound_course_config.py index 1d1e07a5..87426363 100644 --- a/aleksis/apps/lesrooster/schema/timebound_course_config.py +++ b/aleksis/apps/lesrooster/schema/timebound_course_config.py @@ -134,7 +134,8 @@ class CourseBatchCreateForSchoolTermMutation(graphene.Mutation): teachers = Person.objects.filter(pk__in=course_input.teachers) course = Course.objects.create( - name=course_input.name, + name=f"""{''.join(groups.values_list('short_name', flat=True) + .order_by('short_name'))}-{subject.name}""", subject=subject, lesson_quota=course_input.lesson_quota or None, ) -- GitLab From d4a3d428d593e0dbc6b85f0eb75beae9d1895e89 Mon Sep 17 00:00:00 2001 From: Hangzhi Yu <hangzhi@protonmail.com> Date: Tue, 14 May 2024 13:57:40 +0200 Subject: [PATCH 12/34] Reformat --- .../TimeboundCourseConfigRaster.vue | 10 ++++++---- aleksis/apps/lesrooster/schema/__init__.py | 10 +++++++--- 2 files changed, 13 insertions(+), 7 deletions(-) diff --git a/aleksis/apps/lesrooster/frontend/components/timebound_course_config/TimeboundCourseConfigRaster.vue b/aleksis/apps/lesrooster/frontend/components/timebound_course_config/TimeboundCourseConfigRaster.vue index e9a902dd..3e1a63f1 100644 --- a/aleksis/apps/lesrooster/frontend/components/timebound_course_config/TimeboundCourseConfigRaster.vue +++ b/aleksis/apps/lesrooster/frontend/components/timebound_course_config/TimeboundCourseConfigRaster.vue @@ -288,9 +288,7 @@ export default { Object.assign(existingCreatedCourse, newValue); } } else { - if ( - !course.lrTimeboundCourseConfigs?.length - ) { + if (!course.lrTimeboundCourseConfigs?.length) { let existingCreatedCourseConfig = this.createdCourseConfigs.find( (c) => c.course === course.id && @@ -498,7 +496,11 @@ export default { subjects: { query: subjects, skip() { - return !this.groupIDSet.size || !this.internalValidityRange || !this.persons.length; + return ( + !this.groupIDSet.size || + !this.internalValidityRange || + !this.persons.length + ); }, variables() { return { diff --git a/aleksis/apps/lesrooster/schema/__init__.py b/aleksis/apps/lesrooster/schema/__init__.py index 05c91b5c..354aea19 100644 --- a/aleksis/apps/lesrooster/schema/__init__.py +++ b/aleksis/apps/lesrooster/schema/__init__.py @@ -113,7 +113,7 @@ class Query(graphene.ObjectType): LesroosterExtendedSubjectType, groups=graphene.List(graphene.ID), include_child_groups=graphene.Boolean(required=True), - validity_range=graphene.ID(required=True) + validity_range=graphene.ID(required=True), ) groups_by_time_grid = graphene.List(GroupType, time_grid=graphene.ID(required=True)) @@ -164,7 +164,9 @@ class Query(graphene.ObjectType): return Supervision.objects.all() @staticmethod - def resolve_lesrooster_extended_subjects(root, info, groups, include_child_groups, validity_range): + def resolve_lesrooster_extended_subjects( + root, info, groups, include_child_groups, validity_range + ): if include_child_groups: courses = Course.objects.filter( Q(groups__in=groups) | Q(groups__parent_groups__in=groups) @@ -172,7 +174,9 @@ class Query(graphene.ObjectType): else: courses = Course.objects.filter(groups__in=groups) course_configs = TimeboundCourseConfig.objects.filter(validity_range=validity_range) - courses = get_objects_for_user(info.context.user, "cursus.view_course", courses).prefetch_related(Prefetch("lr_timebound_course_configs", queryset=course_configs)) + courses = get_objects_for_user( + info.context.user, "cursus.view_course", courses + ).prefetch_related(Prefetch("lr_timebound_course_configs", queryset=course_configs)) subjects = get_objects_for_user( info.context.user, "cursus.view_subject", Subject.objects.all() ) -- GitLab From cbccc175cb71b4d3d1edeba8ad7f41b89188a86b Mon Sep 17 00:00:00 2001 From: Hangzhi Yu <hangzhi@protonmail.com> Date: Wed, 29 Jan 2025 00:04:02 +0100 Subject: [PATCH 13/34] Use active school term for filtering TCCs --- .../TimeboundCourseConfigRaster.vue | 1 - .../timeboundCourseConfig.graphql | 2 -- aleksis/apps/lesrooster/schema/__init__.py | 9 ++++++--- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/aleksis/apps/lesrooster/frontend/components/timebound_course_config/TimeboundCourseConfigRaster.vue b/aleksis/apps/lesrooster/frontend/components/timebound_course_config/TimeboundCourseConfigRaster.vue index 7cb125a6..94890795 100644 --- a/aleksis/apps/lesrooster/frontend/components/timebound_course_config/TimeboundCourseConfigRaster.vue +++ b/aleksis/apps/lesrooster/frontend/components/timebound_course_config/TimeboundCourseConfigRaster.vue @@ -507,7 +507,6 @@ export default { return { groups: Array.from(this.groupIDSet), includeChildGroups: this.includeChildGroups, - validityRange: this.internalValidityRange.id, }; }, result({ data }) { diff --git a/aleksis/apps/lesrooster/frontend/components/timebound_course_config/timeboundCourseConfig.graphql b/aleksis/apps/lesrooster/frontend/components/timebound_course_config/timeboundCourseConfig.graphql index b5a2887b..e0fb4042 100644 --- a/aleksis/apps/lesrooster/frontend/components/timebound_course_config/timeboundCourseConfig.graphql +++ b/aleksis/apps/lesrooster/frontend/components/timebound_course_config/timeboundCourseConfig.graphql @@ -103,14 +103,12 @@ query subjects( $filters: JSONString $groups: [ID] $includeChildGroups: Boolean! - $validityRange: ID! ) { subjects: lesroosterExtendedSubjects( orderBy: $orderBy filters: $filters groups: $groups includeChildGroups: $includeChildGroups - validityRange: $validityRange ) { ...subjectFields courses { diff --git a/aleksis/apps/lesrooster/schema/__init__.py b/aleksis/apps/lesrooster/schema/__init__.py index a9e279b6..333e32d1 100644 --- a/aleksis/apps/lesrooster/schema/__init__.py +++ b/aleksis/apps/lesrooster/schema/__init__.py @@ -129,7 +129,6 @@ class Query(graphene.ObjectType): LesroosterExtendedSubjectType, groups=graphene.List(graphene.ID), include_child_groups=graphene.Boolean(required=True), - validity_range=graphene.ID(required=True), ) groups_by_time_grid = graphene.List(GroupType, time_grid=graphene.ID(required=True)) @@ -207,7 +206,7 @@ class Query(graphene.ObjectType): @staticmethod def resolve_lesrooster_extended_subjects( - root, info, groups, include_child_groups, validity_range + root, info, groups, include_child_groups ): if include_child_groups: courses = Course.objects.filter( @@ -216,7 +215,11 @@ class Query(graphene.ObjectType): else: courses = Course.objects.filter(groups__in=groups) - course_configs = TimeboundCourseConfig.objects.filter(validity_range=validity_range) + course_configs = filter_active_school_term( + info.context, + TimeboundCourseConfig.objects.all(), + "validity_range__school_term", + ) subjects = Subject.objects.all().prefetch_related( Prefetch( -- GitLab From 340ab9f3d2455178ed3d960c365cf58ce75fe264 Mon Sep 17 00:00:00 2001 From: Hangzhi Yu <hangzhi@protonmail.com> Date: Wed, 29 Jan 2025 00:15:54 +0100 Subject: [PATCH 14/34] Remove sending of school term when creating courses in TCC raster --- .../timebound_course_config/TimeboundCourseConfigRaster.vue | 1 - 1 file changed, 1 deletion(-) diff --git a/aleksis/apps/lesrooster/frontend/components/timebound_course_config/TimeboundCourseConfigRaster.vue b/aleksis/apps/lesrooster/frontend/components/timebound_course_config/TimeboundCourseConfigRaster.vue index 94890795..c855f002 100644 --- a/aleksis/apps/lesrooster/frontend/components/timebound_course_config/TimeboundCourseConfigRaster.vue +++ b/aleksis/apps/lesrooster/frontend/components/timebound_course_config/TimeboundCourseConfigRaster.vue @@ -281,7 +281,6 @@ export default { this.createdCourses.push({ subject: subject.id, groups: JSON.parse(header.value), - schoolTerm: this.internalValidityRange.schoolTerm.id, name: `${header.text}-${subject.name}`, ...newValue, }); -- GitLab From ab4c4d61ff6a287665b726234894b48f672baa71 Mon Sep 17 00:00:00 2001 From: Hangzhi Yu <hangzhi@protonmail.com> Date: Wed, 29 Jan 2025 12:49:52 +0100 Subject: [PATCH 15/34] Remove unused copy from last VR button --- .../TimeboundCourseConfigRaster.vue | 13 +------------ 1 file changed, 1 insertion(+), 12 deletions(-) diff --git a/aleksis/apps/lesrooster/frontend/components/timebound_course_config/TimeboundCourseConfigRaster.vue b/aleksis/apps/lesrooster/frontend/components/timebound_course_config/TimeboundCourseConfigRaster.vue index c855f002..e1aedaa2 100644 --- a/aleksis/apps/lesrooster/frontend/components/timebound_course_config/TimeboundCourseConfigRaster.vue +++ b/aleksis/apps/lesrooster/frontend/components/timebound_course_config/TimeboundCourseConfigRaster.vue @@ -74,18 +74,7 @@ import SubjectChip from "aleksis.apps.cursus/components/SubjectChip.vue"; </v-col> <v-spacer /> - - <v-col - cols="8" - lg="3" - class="d-flex justify-space-between flex-wrap align-center" - > - <secondary-action-button - i18n-key="actions.copy_last_configuration" - block - class="mr-4" - /> - </v-col> + <v-col cols="4" lg="1" -- GitLab From 13f2a00510b90da01b5dc150415e55756e7dcbf0 Mon Sep 17 00:00:00 2001 From: Hangzhi Yu <hangzhi@protonmail.com> Date: Wed, 29 Jan 2025 13:06:58 +0100 Subject: [PATCH 16/34] Remove unneeded imports --- .../timebound_course_config/TimeboundCourseConfigRaster.vue | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/aleksis/apps/lesrooster/frontend/components/timebound_course_config/TimeboundCourseConfigRaster.vue b/aleksis/apps/lesrooster/frontend/components/timebound_course_config/TimeboundCourseConfigRaster.vue index e1aedaa2..a4cdfa41 100644 --- a/aleksis/apps/lesrooster/frontend/components/timebound_course_config/TimeboundCourseConfigRaster.vue +++ b/aleksis/apps/lesrooster/frontend/components/timebound_course_config/TimeboundCourseConfigRaster.vue @@ -74,7 +74,7 @@ import SubjectChip from "aleksis.apps.cursus/components/SubjectChip.vue"; </v-col> <v-spacer /> - + <v-col cols="4" lg="1" @@ -205,12 +205,9 @@ import { currentValidityRange as gqlCurrentValidityRange } from "../validity_ran import { gqlGroupsForPlanning, - gqlGroups, gqlTeachers, } from "../helper.graphql"; -import { createCourses } from "aleksis.apps.cursus/components/course.graphql"; - export default { name: "TimeboungCourseConfigRaster", data() { -- GitLab From 77f05c6e66e79e5f987b94040c9a3cbef567566d Mon Sep 17 00:00:00 2001 From: Hangzhi Yu <hangzhi@protonmail.com> Date: Wed, 29 Jan 2025 13:58:29 +0100 Subject: [PATCH 17/34] =?UTF-8?q?Auto-create=20TCC=20when=20using=20course?= =?UTF-8?q?=20p=C3=B6anning=20feature?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../TimeboundCourseConfigRaster.vue | 14 ++++++------- .../timeboundCourseConfig.graphql | 7 +++++-- aleksis/apps/lesrooster/schema/__init__.py | 8 +++---- .../schema/timebound_course_config.py | 21 +++++++++++++------ 4 files changed, 30 insertions(+), 20 deletions(-) diff --git a/aleksis/apps/lesrooster/frontend/components/timebound_course_config/TimeboundCourseConfigRaster.vue b/aleksis/apps/lesrooster/frontend/components/timebound_course_config/TimeboundCourseConfigRaster.vue index a4cdfa41..c8005e10 100644 --- a/aleksis/apps/lesrooster/frontend/components/timebound_course_config/TimeboundCourseConfigRaster.vue +++ b/aleksis/apps/lesrooster/frontend/components/timebound_course_config/TimeboundCourseConfigRaster.vue @@ -49,7 +49,6 @@ import SubjectChip from "aleksis.apps.cursus/components/SubjectChip.vue"; <validity-range-field outlined filled - label="Select Validity Range" hide-details v-model="internalValidityRange" :loading="$apollo.queries.currentValidityRange.loading" @@ -198,15 +197,12 @@ import { subjects, createTimeboundCourseConfigs, updateTimeboundCourseConfigs, - createCoursesForSchoolTerm, + createCoursesForValidityRange, } from "./timeboundCourseConfig.graphql"; import { currentValidityRange as gqlCurrentValidityRange } from "../validity_range/validityRange.graphql"; -import { - gqlGroupsForPlanning, - gqlTeachers, -} from "../helper.graphql"; +import { gqlGroupsForPlanning, gqlTeachers } from "../helper.graphql"; export default { name: "TimeboungCourseConfigRaster", @@ -318,7 +314,7 @@ export default { }, { data: this.createdCourses, - mutation: createCoursesForSchoolTerm, + mutation: createCoursesForValidityRange, }, ]) { if (mutationCombination.data.length) { @@ -327,6 +323,10 @@ export default { mutation: mutationCombination.mutation, variables: { input: mutationCombination.data, + ...(mutationCombination.mutation === + createCoursesForValidityRange && { + validityRange: this.internalValidityRange.id, + }), }, }) .catch(() => {}); // FIXME Error Handling diff --git a/aleksis/apps/lesrooster/frontend/components/timebound_course_config/timeboundCourseConfig.graphql b/aleksis/apps/lesrooster/frontend/components/timebound_course_config/timeboundCourseConfig.graphql index e0fb4042..cb678603 100644 --- a/aleksis/apps/lesrooster/frontend/components/timebound_course_config/timeboundCourseConfig.graphql +++ b/aleksis/apps/lesrooster/frontend/components/timebound_course_config/timeboundCourseConfig.graphql @@ -120,8 +120,11 @@ query subjects( } } -mutation createCoursesForSchoolTerm($input: [CourseInputType]!) { - createCoursesForSchoolTerm(input: $input) { +mutation createCoursesForValidityRange( + $input: [CourseInputType]! + $validityRange: ID! +) { + createCoursesForValidityRange(input: $input, validityRange: $validityRange) { items: courses { id name diff --git a/aleksis/apps/lesrooster/schema/__init__.py b/aleksis/apps/lesrooster/schema/__init__.py index 333e32d1..4e4fd288 100644 --- a/aleksis/apps/lesrooster/schema/__init__.py +++ b/aleksis/apps/lesrooster/schema/__init__.py @@ -58,7 +58,7 @@ from .time_grid import ( ) from .timebound_course_bundle import TimeboundCourseBundleType from .timebound_course_config import ( - CourseBatchCreateForSchoolTermMutation, + CourseBatchCreateForValidityRangeMutation, LesroosterExtendedSubjectType, TimeboundCourseConfigBatchCreateMutation, TimeboundCourseConfigBatchDeleteMutation, @@ -205,9 +205,7 @@ class Query(graphene.ObjectType): return graphene_django_optimizer.query(qs, info) @staticmethod - def resolve_lesrooster_extended_subjects( - root, info, groups, include_child_groups - ): + def resolve_lesrooster_extended_subjects(root, info, groups, include_child_groups): if include_child_groups: courses = Course.objects.filter( Q(groups__in=groups) | Q(groups__parent_groups__in=groups) @@ -449,4 +447,4 @@ class Mutation(graphene.ObjectType): delete_supervisions = SupervisionBatchDeleteMutation.Field() update_supervisions = SupervisionBatchPatchMutation.Field() - create_courses_for_school_term = CourseBatchCreateForSchoolTermMutation.Field() + create_courses_for_validity_range = CourseBatchCreateForValidityRangeMutation.Field() diff --git a/aleksis/apps/lesrooster/schema/timebound_course_config.py b/aleksis/apps/lesrooster/schema/timebound_course_config.py index 52f91d2d..c71d17fe 100644 --- a/aleksis/apps/lesrooster/schema/timebound_course_config.py +++ b/aleksis/apps/lesrooster/schema/timebound_course_config.py @@ -23,7 +23,7 @@ from aleksis.core.util.core_helpers import ( get_site_preferences, ) -from ..models import TimeboundCourseConfig +from ..models import TimeboundCourseConfig, ValidityRange timebound_course_config_filters = {"course": ["in"], "validity_range": ["in"], "teachers": [""]} @@ -113,18 +113,20 @@ class CourseInputType(graphene.InputObjectType): default_room = graphene.ID(required=False) -class CourseBatchCreateForSchoolTermMutation(graphene.Mutation): +class CourseBatchCreateForValidityRangeMutation(graphene.Mutation): class Arguments: input = graphene.List(CourseInputType) + validity_range = graphene.ID() courses = graphene.List(CourseType) @classmethod - def create(cls, info, course_input): + def create(cls, info, course_input, validity_range_id): if info.context.user.has_perm("cursus.create_course_rule"): groups = Group.objects.filter(pk__in=course_input.groups) subject = Subject.objects.get(pk=course_input.subject) teachers = Person.objects.filter(pk__in=course_input.teachers) + validity_range = ValidityRange.objects.get(pk=validity_range_id) course = Course.objects.create( name=f"""{''.join(groups.values_list('short_name', flat=True) @@ -134,6 +136,13 @@ class CourseBatchCreateForSchoolTermMutation(graphene.Mutation): ) course.teachers.set(teachers) + tcc = TimeboundCourseConfig.objects.create( + course=course, + validity_range=validity_range, + lesson_quota=course.lesson_quota, + ) + tcc.teachers.set(course.teachers.all()) + if get_site_preferences()["lesrooster__create_course_group"]: school_term = get_active_school_term(info.context) @@ -164,6 +173,6 @@ class CourseBatchCreateForSchoolTermMutation(graphene.Mutation): return course @classmethod - def mutate(cls, root, info, input): # noqa - objs = [cls.create(info, course_input) for course_input in input] - return CourseBatchCreateForSchoolTermMutation(courses=objs) + def mutate(cls, root, info, input, validity_range): # noqa + objs = [cls.create(info, course_input, validity_range) for course_input in input] + return CourseBatchCreateForValidityRangeMutation(courses=objs) -- GitLab From 6b87f2acd74e46968acd1c027f5ff45717a416c5 Mon Sep 17 00:00:00 2001 From: Hangzhi Yu <hangzhi@protonmail.com> Date: Wed, 29 Jan 2025 13:58:46 +0100 Subject: [PATCH 18/34] Set translated default label for VR field --- .../frontend/components/validity_range/ValidityRangeField.vue | 1 + 1 file changed, 1 insertion(+) diff --git a/aleksis/apps/lesrooster/frontend/components/validity_range/ValidityRangeField.vue b/aleksis/apps/lesrooster/frontend/components/validity_range/ValidityRangeField.vue index 5be88f33..8ff63e95 100644 --- a/aleksis/apps/lesrooster/frontend/components/validity_range/ValidityRangeField.vue +++ b/aleksis/apps/lesrooster/frontend/components/validity_range/ValidityRangeField.vue @@ -9,6 +9,7 @@ import DateField from "aleksis.core/components/generic/forms/DateField.vue"; v-on="$listeners" :fields="headers" create-item-i18n-key="lesrooster.validity_range.create_validity_range" + :label="$t('labels.select_validity_range')" :gql-query="gqlQuery" :gql-create-mutation="gqlCreateMutation" :gql-patch-mutation="{}" -- GitLab From 4ac86f4fbebabc659c4225ece323664a4f5a7d07 Mon Sep 17 00:00:00 2001 From: Hangzhi Yu <hangzhi@protonmail.com> Date: Wed, 29 Jan 2025 14:44:22 +0100 Subject: [PATCH 19/34] Avoid refetch after saving by updating subjects cache with results --- .../TimeboundCourseConfigRaster.vue | 62 +++++++++++++++---- .../timeboundCourseConfig.graphql | 16 ++--- .../schema/timebound_course_config.py | 2 +- 3 files changed, 54 insertions(+), 26 deletions(-) diff --git a/aleksis/apps/lesrooster/frontend/components/timebound_course_config/TimeboundCourseConfigRaster.vue b/aleksis/apps/lesrooster/frontend/components/timebound_course_config/TimeboundCourseConfigRaster.vue index c8005e10..47a88f43 100644 --- a/aleksis/apps/lesrooster/frontend/components/timebound_course_config/TimeboundCourseConfigRaster.vue +++ b/aleksis/apps/lesrooster/frontend/components/timebound_course_config/TimeboundCourseConfigRaster.vue @@ -204,8 +204,11 @@ import { currentValidityRange as gqlCurrentValidityRange } from "../validity_ran import { gqlGroupsForPlanning, gqlTeachers } from "../helper.graphql"; +import mutateMixin from "aleksis.core/mixins/mutateMixin.js"; + export default { name: "TimeboungCourseConfigRaster", + mixins: [mutateMixin], data() { return { i18nKey: "lesrooster.timebound_course_config", @@ -300,6 +303,38 @@ export default { } } }, + updateCourseConfigs(cachedSubjects, incomingCourseConfigs) { + incomingCourseConfigs.forEach((newCourseConfig) => { + const subject = cachedSubjects.find( + (s) => s.id === newCourseConfig.course.subject.id, + ); + if (!subject) { + return; + } + + const course = subject.courses.find( + (c) => c.id === newCourseConfig.course.id, + ); + + course.lrTimeboundCourseConfigs = [newCourseConfig]; + }); + + return cachedSubjects; + }, + updateCreatedCourses(cachedSubjects, incomingCourses) { + incomingCourses.forEach((newCourse) => { + const subject = cachedSubjects.find( + (s) => s.id === newCourse.subject.id, + ); + if (!subject) { + return; + } + + subject.courses.push(newCourse); + }); + + return cachedSubjects; + }, save() { this.loading = true; @@ -307,36 +342,37 @@ export default { { data: this.editedCourseConfigs, mutation: updateTimeboundCourseConfigs, + updateCacheHandler: this.updateCourseConfigs, }, { data: this.createdCourseConfigs, mutation: createTimeboundCourseConfigs, + updateCacheHandler: this.updateCourseConfigs, }, { data: this.createdCourses, mutation: createCoursesForValidityRange, + updateCacheHandler: this.updateCreatedCourses, }, ]) { if (mutationCombination.data.length) { - this.$apollo - .mutate({ - mutation: mutationCombination.mutation, - variables: { - input: mutationCombination.data, - ...(mutationCombination.mutation === - createCoursesForValidityRange && { - validityRange: this.internalValidityRange.id, - }), - }, - }) - .catch(() => {}); // FIXME Error Handling + this.mutate( + mutationCombination.mutation, + { + input: mutationCombination.data, + ...(mutationCombination.mutation === + createCoursesForValidityRange && { + validityRange: this.internalValidityRange.id, + }), + }, + mutationCombination.updateCacheHandler, + ); } } this.editedCourseConfigs = []; this.createdCourseConfigs = []; this.createdCourses = []; - this.$apollo.queries.subjects.refetch(); this.loading = false; }, getTeacherList(subjectTeachers) { diff --git a/aleksis/apps/lesrooster/frontend/components/timebound_course_config/timeboundCourseConfig.graphql b/aleksis/apps/lesrooster/frontend/components/timebound_course_config/timeboundCourseConfig.graphql index cb678603..5f86c9a8 100644 --- a/aleksis/apps/lesrooster/frontend/components/timebound_course_config/timeboundCourseConfig.graphql +++ b/aleksis/apps/lesrooster/frontend/components/timebound_course_config/timeboundCourseConfig.graphql @@ -126,21 +126,13 @@ mutation createCoursesForValidityRange( ) { createCoursesForValidityRange(input: $input, validityRange: $validityRange) { items: courses { - id - name + ...courseFields subject { - id - name - } - teachers { - id - fullName + ...subjectFields } - groups { - id - name + lrTimeboundCourseConfigs { + ...timeboundCourseConfigFields } - lessonQuota canEdit canDelete } diff --git a/aleksis/apps/lesrooster/schema/timebound_course_config.py b/aleksis/apps/lesrooster/schema/timebound_course_config.py index c71d17fe..eb765f02 100644 --- a/aleksis/apps/lesrooster/schema/timebound_course_config.py +++ b/aleksis/apps/lesrooster/schema/timebound_course_config.py @@ -118,7 +118,7 @@ class CourseBatchCreateForValidityRangeMutation(graphene.Mutation): input = graphene.List(CourseInputType) validity_range = graphene.ID() - courses = graphene.List(CourseType) + courses = graphene.List(LesroosterExtendedCourseType) @classmethod def create(cls, info, course_input, validity_range_id): -- GitLab From 333c55fc366386efb18c25e5106be42288345661 Mon Sep 17 00:00:00 2001 From: Hangzhi Yu <hangzhi@protonmail.com> Date: Thu, 27 Feb 2025 00:33:50 +0100 Subject: [PATCH 20/34] Implement TCC raster autosave --- .../TimeboundCourseConfigRaster.vue | 146 ++++++++++-------- .../timeboundCourseConfig.graphql | 2 +- 2 files changed, 83 insertions(+), 65 deletions(-) diff --git a/aleksis/apps/lesrooster/frontend/components/timebound_course_config/TimeboundCourseConfigRaster.vue b/aleksis/apps/lesrooster/frontend/components/timebound_course_config/TimeboundCourseConfigRaster.vue index 47a88f43..4e4542f7 100644 --- a/aleksis/apps/lesrooster/frontend/components/timebound_course_config/TimeboundCourseConfigRaster.vue +++ b/aleksis/apps/lesrooster/frontend/components/timebound_course_config/TimeboundCourseConfigRaster.vue @@ -1,7 +1,5 @@ <script setup> import PositiveSmallIntegerField from "aleksis.core/components/generic/forms/PositiveSmallIntegerField.vue"; -import SaveButton from "aleksis.core/components/generic/buttons/SaveButton.vue"; -import SecondaryActionButton from "aleksis.core/components/generic/buttons/SecondaryActionButton.vue"; import ValidityRangeField from "../validity_range/ValidityRangeField.vue"; import SubjectChip from "aleksis.apps.cursus/components/SubjectChip.vue"; </script> @@ -15,7 +13,7 @@ import SubjectChip from "aleksis.apps.cursus/components/SubjectChip.vue"; hide-default-footer :headers="headers" :items="items" - :loading="$apollo.queries.subjects.loading" + :loading="loading" > <template #top> <v-row> @@ -73,22 +71,6 @@ import SubjectChip from "aleksis.apps.cursus/components/SubjectChip.vue"; </v-col> <v-spacer /> - - <v-col - cols="4" - lg="1" - class="d-flex justify-space-between flex-wrap align-center" - > - <save-button - :disabled=" - !editedCourseConfigs.length && - !createdCourseConfigs.length && - !createdCourses.length - " - :loading="loading" - @click="save" - /> - </v-col> </v-row> </template> @@ -237,7 +219,6 @@ export default { createdCourses: [], currentCourse: null, currentSubject: null, - loading: false, includeChildGroups: true, selectedGroupHeaders: [], items: [], @@ -270,7 +251,9 @@ export default { ...newValue, }); } else { - Object.assign(existingCreatedCourse, newValue); + for (const key in newValue) { + this.$set(existingCreatedCourse, key, newValue[key]); + } } } else { if (!course.lrTimeboundCourseConfigs?.length) { @@ -288,7 +271,9 @@ export default { ...newValue, }); } else { - Object.assign(existingCreatedCourseConfig, newValue); + for (const key in newValue) { + this.$set(existingCreatedCourseConfig, key, newValue[key]); + } } } else { let courseConfigID = course.lrTimeboundCourseConfigs[0].id; @@ -298,7 +283,9 @@ export default { if (!existingEditedCourseConfig) { this.editedCourseConfigs.push({ id: courseConfigID, ...newValue }); } else { - Object.assign(existingEditedCourseConfig, newValue); + for (const key in newValue) { + this.$set(existingEditedCourseConfig, key, newValue[key]); + } } } } @@ -335,46 +322,6 @@ export default { return cachedSubjects; }, - save() { - this.loading = true; - - for (let mutationCombination of [ - { - data: this.editedCourseConfigs, - mutation: updateTimeboundCourseConfigs, - updateCacheHandler: this.updateCourseConfigs, - }, - { - data: this.createdCourseConfigs, - mutation: createTimeboundCourseConfigs, - updateCacheHandler: this.updateCourseConfigs, - }, - { - data: this.createdCourses, - mutation: createCoursesForValidityRange, - updateCacheHandler: this.updateCreatedCourses, - }, - ]) { - if (mutationCombination.data.length) { - this.mutate( - mutationCombination.mutation, - { - input: mutationCombination.data, - ...(mutationCombination.mutation === - createCoursesForValidityRange && { - validityRange: this.internalValidityRange.id, - }), - }, - mutationCombination.updateCacheHandler, - ); - } - } - - this.editedCourseConfigs = []; - this.createdCourseConfigs = []; - this.createdCourses = []; - this.loading = false; - }, getTeacherList(subjectTeachers) { return [ { @@ -491,6 +438,27 @@ export default { subjectGroupCombinations() { return Array.from(this.groupCombinationsSet); }, + createdCoursesReady() { + return !!this.createdCourses.length && this.createdCourses.every((c) => { + return c?.groups?.length && c.lessonQuota && c.name && c.subject && c?.teachers?.length + }); + }, + createdCourseConfigsReady() { + return !!this.createdCourseConfigs.length && this.createdCourseConfigs.every((c) => { + return c.course && c.validityRange && c?.teachers?.length && c.lessonQuota + }); + }, + editedCourseConfigsReady() { + return !!this.editedCourseConfigs.length && this.editedCourseConfigs.every((c) => { + return c.id && (c.lessonQuota || c?.teachers?.length); + }); + }, + expandedQuery() { + return { + ...this.$apollo.queries.subjects.options, + variables: JSON.parse(this.$apollo.queries.subjects.previousVariablesJson), + }; + }, }, watch: { selectedGroups(newValue) { @@ -499,6 +467,55 @@ export default { value: JSON.stringify([group.id]), })); }, + editedCourseConfigs: { + deep: true, + handler(newValue) { + if (this.editedCourseConfigsReady) { + this.mutate( + updateTimeboundCourseConfigs, + { + input: newValue, + }, + this.updateCourseConfigs, + ); + + this.editedCourseConfigs = []; + } + }, + }, + createdCourseConfigs: { + deep: true, + handler(newValue) { + if (this.createdCourseConfigsReady) { + this.mutate( + createTimeboundCourseConfigs, + { + input: newValue, + }, + this.updateCourseConfigs, + ); + + this.createdCourseConfigs = []; + } + }, + }, + createdCourses: { + deep: true, + handler(newValue) { + if (this.createdCoursesReady) { + this.mutate( + createCoursesForValidityRange, + { + input: newValue, + validityRange: this.internalValidityRange.id, + }, + this.updateCreatedCourses, + ); + + this.createdCourses = []; + } + }, + } }, apollo: { currentValidityRange: { @@ -530,9 +547,10 @@ export default { includeChildGroups: this.includeChildGroups, }; }, + update: data => data.items, result({ data }) { if (!data) return; - this.items = this.generateTableItems(data.subjects); + this.items = this.generateTableItems(data.items); }, }, persons: { diff --git a/aleksis/apps/lesrooster/frontend/components/timebound_course_config/timeboundCourseConfig.graphql b/aleksis/apps/lesrooster/frontend/components/timebound_course_config/timeboundCourseConfig.graphql index 5f86c9a8..15d3954d 100644 --- a/aleksis/apps/lesrooster/frontend/components/timebound_course_config/timeboundCourseConfig.graphql +++ b/aleksis/apps/lesrooster/frontend/components/timebound_course_config/timeboundCourseConfig.graphql @@ -104,7 +104,7 @@ query subjects( $groups: [ID] $includeChildGroups: Boolean! ) { - subjects: lesroosterExtendedSubjects( + items: lesroosterExtendedSubjects( orderBy: $orderBy filters: $filters groups: $groups -- GitLab From ee768cd2a060de0fd9fcd5fe6424d8f510d9b2cf Mon Sep 17 00:00:00 2001 From: Hangzhi Yu <hangzhi@protonmail.com> Date: Fri, 28 Feb 2025 19:54:49 +0100 Subject: [PATCH 21/34] Make lesson quota field required & fire setting course data only on change --- .../timebound_course_config/TimeboundCourseConfigRaster.vue | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/aleksis/apps/lesrooster/frontend/components/timebound_course_config/TimeboundCourseConfigRaster.vue b/aleksis/apps/lesrooster/frontend/components/timebound_course_config/TimeboundCourseConfigRaster.vue index 4e4542f7..4d4718c2 100644 --- a/aleksis/apps/lesrooster/frontend/components/timebound_course_config/TimeboundCourseConfigRaster.vue +++ b/aleksis/apps/lesrooster/frontend/components/timebound_course_config/TimeboundCourseConfigRaster.vue @@ -103,7 +103,8 @@ import SubjectChip from "aleksis.apps.cursus/components/SubjectChip.vue"; : course.lessonQuota " :label="$t('lesrooster.timebound_course_config.lesson_quota')" - @input=" + :rules="$rules().required.build()" + @change=" (event) => setCourseConfigData(course, item.subject, header, { lessonQuota: event, @@ -187,10 +188,11 @@ import { currentValidityRange as gqlCurrentValidityRange } from "../validity_ran import { gqlGroupsForPlanning, gqlTeachers } from "../helper.graphql"; import mutateMixin from "aleksis.core/mixins/mutateMixin.js"; +import formRulesMixin from "aleksis.core/mixins/formRulesMixin"; export default { name: "TimeboungCourseConfigRaster", - mixins: [mutateMixin], + mixins: [formRulesMixin, mutateMixin], data() { return { i18nKey: "lesrooster.timebound_course_config", -- GitLab From 7f4c51c2ac535d51fcf2d8780b9c1dfa9ed83ddf Mon Sep 17 00:00:00 2001 From: Hangzhi Yu <hangzhi@protonmail.com> Date: Fri, 28 Feb 2025 20:03:07 +0100 Subject: [PATCH 22/34] Make lessonQuota not required --- .../TimeboundCourseConfigRaster.vue | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/aleksis/apps/lesrooster/frontend/components/timebound_course_config/TimeboundCourseConfigRaster.vue b/aleksis/apps/lesrooster/frontend/components/timebound_course_config/TimeboundCourseConfigRaster.vue index 4d4718c2..7afbc0eb 100644 --- a/aleksis/apps/lesrooster/frontend/components/timebound_course_config/TimeboundCourseConfigRaster.vue +++ b/aleksis/apps/lesrooster/frontend/components/timebound_course_config/TimeboundCourseConfigRaster.vue @@ -103,7 +103,6 @@ import SubjectChip from "aleksis.apps.cursus/components/SubjectChip.vue"; : course.lessonQuota " :label="$t('lesrooster.timebound_course_config.lesson_quota')" - :rules="$rules().required.build()" @change=" (event) => setCourseConfigData(course, item.subject, header, { @@ -442,17 +441,17 @@ export default { }, createdCoursesReady() { return !!this.createdCourses.length && this.createdCourses.every((c) => { - return c?.groups?.length && c.lessonQuota && c.name && c.subject && c?.teachers?.length + return c?.groups?.length && c.name && c.subject && c?.teachers?.length }); }, createdCourseConfigsReady() { return !!this.createdCourseConfigs.length && this.createdCourseConfigs.every((c) => { - return c.course && c.validityRange && c?.teachers?.length && c.lessonQuota + return c.course && c.validityRange && c?.teachers?.length }); }, editedCourseConfigsReady() { return !!this.editedCourseConfigs.length && this.editedCourseConfigs.every((c) => { - return c.id && (c.lessonQuota || c?.teachers?.length); + return c.id && (Object.hasOwn(c, "lessonQuota") || c?.teachers?.length); }); }, expandedQuery() { -- GitLab From 548e9d71bd43c76f99e1df3e5dcd55759bc07b86 Mon Sep 17 00:00:00 2001 From: Hangzhi Yu <hangzhi@protonmail.com> Date: Fri, 28 Feb 2025 20:22:54 +0100 Subject: [PATCH 23/34] Use TeacherField instead of custom autocomplete --- .../TimeboundCourseConfigRaster.vue | 57 +++---------------- 1 file changed, 7 insertions(+), 50 deletions(-) diff --git a/aleksis/apps/lesrooster/frontend/components/timebound_course_config/TimeboundCourseConfigRaster.vue b/aleksis/apps/lesrooster/frontend/components/timebound_course_config/TimeboundCourseConfigRaster.vue index 7afbc0eb..ddcad459 100644 --- a/aleksis/apps/lesrooster/frontend/components/timebound_course_config/TimeboundCourseConfigRaster.vue +++ b/aleksis/apps/lesrooster/frontend/components/timebound_course_config/TimeboundCourseConfigRaster.vue @@ -2,6 +2,7 @@ import PositiveSmallIntegerField from "aleksis.core/components/generic/forms/PositiveSmallIntegerField.vue"; import ValidityRangeField from "../validity_range/ValidityRangeField.vue"; import SubjectChip from "aleksis.apps.cursus/components/SubjectChip.vue"; +import TeacherField from "aleksis.apps.cursus/components/TeacherField.vue" </script> <template> @@ -112,14 +113,9 @@ import SubjectChip from "aleksis.apps.cursus/components/SubjectChip.vue"; /> </v-col> <v-col cols="6"> - <v-autocomplete - counter + <teacher-field dense filled - multiple - :items="getTeacherList(item.subject.teachers)" - item-text="fullName" - item-value="id" class="mx-1" :disabled="loading" :label="$t('lesrooster.timebound_course_config.teachers')" @@ -128,32 +124,15 @@ import SubjectChip from "aleksis.apps.cursus/components/SubjectChip.vue"; ? getCurrentCourseConfig(course).teachers : course.teachers " + :show-subjects="true" + :priority-subject="item.subject" @input=" (event) => setCourseConfigData(course, item.subject, header, { teachers: event, }) " - > - <template #item="data"> - <template v-if="typeof data.item !== 'object'"> - <v-list-item-content>{{ data.item }}</v-list-item-content> - </template> - <template v-else> - <v-list-item-action> - <v-checkbox v-model="data.attrs.inputValue" /> - </v-list-item-action> - <v-list-item-content> - <v-list-item-title>{{ - data.item.fullName - }}</v-list-item-title> - <v-list-item-subtitle v-if="data.item.shortName">{{ - data.item.shortName - }}</v-list-item-subtitle> - </v-list-item-content> - </template> - </template> - </v-autocomplete> + /> </v-col> </v-row> </div> @@ -184,7 +163,7 @@ import { import { currentValidityRange as gqlCurrentValidityRange } from "../validity_range/validityRange.graphql"; -import { gqlGroupsForPlanning, gqlTeachers } from "../helper.graphql"; +import { gqlGroupsForPlanning } from "../helper.graphql"; import mutateMixin from "aleksis.core/mixins/mutateMixin.js"; import formRulesMixin from "aleksis.core/mixins/formRulesMixin"; @@ -323,24 +302,6 @@ export default { return cachedSubjects; }, - getTeacherList(subjectTeachers) { - return [ - { - header: this.$t( - "lesrooster.timebound_course_config.subject_teachers", - ), - }, - ...this.persons.filter((person) => - subjectTeachers.find((teacher) => teacher.id === person.id), - ), - { divider: true }, - { header: this.$t("lesrooster.timebound_course_config.all_teachers") }, - ...this.persons.filter( - (person) => - !subjectTeachers.find((teacher) => teacher.id === person.id), - ), - ]; - }, addCourse(subject, groups) { this.$set( this.items.find((i) => i.subject.id === subject), @@ -538,8 +499,7 @@ export default { skip() { return ( !this.groupIDSet.size || - !this.internalValidityRange || - !this.persons.length + !this.internalValidityRange ); }, variables() { @@ -554,9 +514,6 @@ export default { this.items = this.generateTableItems(data.items); }, }, - persons: { - query: gqlTeachers, - }, }, }; </script> -- GitLab From c58789ee625a12c38885dd4fca4e8fca04b9b4f7 Mon Sep 17 00:00:00 2001 From: Hangzhi Yu <hangzhi@protonmail.com> Date: Fri, 28 Feb 2025 20:23:03 +0100 Subject: [PATCH 24/34] Make TeacherField required --- .../timebound_course_config/TimeboundCourseConfigRaster.vue | 1 + 1 file changed, 1 insertion(+) diff --git a/aleksis/apps/lesrooster/frontend/components/timebound_course_config/TimeboundCourseConfigRaster.vue b/aleksis/apps/lesrooster/frontend/components/timebound_course_config/TimeboundCourseConfigRaster.vue index ddcad459..5c9feed7 100644 --- a/aleksis/apps/lesrooster/frontend/components/timebound_course_config/TimeboundCourseConfigRaster.vue +++ b/aleksis/apps/lesrooster/frontend/components/timebound_course_config/TimeboundCourseConfigRaster.vue @@ -126,6 +126,7 @@ import TeacherField from "aleksis.apps.cursus/components/TeacherField.vue" " :show-subjects="true" :priority-subject="item.subject" + :rules="$rules().isNonEmpty.build()" @input=" (event) => setCourseConfigData(course, item.subject, header, { -- GitLab From f0c2b7f0f77c051d6c49718ce837944f4b43fd5a Mon Sep 17 00:00:00 2001 From: Hangzhi Yu <hangzhi@protonmail.com> Date: Fri, 28 Feb 2025 21:02:13 +0100 Subject: [PATCH 25/34] Properly set data table loading state --- .../timebound_course_config/TimeboundCourseConfigRaster.vue | 3 +++ 1 file changed, 3 insertions(+) diff --git a/aleksis/apps/lesrooster/frontend/components/timebound_course_config/TimeboundCourseConfigRaster.vue b/aleksis/apps/lesrooster/frontend/components/timebound_course_config/TimeboundCourseConfigRaster.vue index 5c9feed7..9eb63604 100644 --- a/aleksis/apps/lesrooster/frontend/components/timebound_course_config/TimeboundCourseConfigRaster.vue +++ b/aleksis/apps/lesrooster/frontend/components/timebound_course_config/TimeboundCourseConfigRaster.vue @@ -422,6 +422,9 @@ export default { variables: JSON.parse(this.$apollo.queries.subjects.previousVariablesJson), }; }, + tableLoading() { + return this.loading || this.$apollo.queries.subjects.loading || this.$apollo.queries.groupsForPlanning.loading || this.$apollo.queries.currentValidityRange.loading; + }, }, watch: { selectedGroups(newValue) { -- GitLab From 1982f79ffa1b021bbc201bee9256868214e6b4bd Mon Sep 17 00:00:00 2001 From: Hangzhi Yu <hangzhi@protonmail.com> Date: Wed, 5 Mar 2025 08:26:48 +0100 Subject: [PATCH 26/34] Move permission check --- aleksis/apps/lesrooster/schema/__init__.py | 9 ++++++++- .../apps/lesrooster/schema/timebound_course_config.py | 8 ++------ 2 files changed, 10 insertions(+), 7 deletions(-) diff --git a/aleksis/apps/lesrooster/schema/__init__.py b/aleksis/apps/lesrooster/schema/__init__.py index 4e4fd288..dc4ea901 100644 --- a/aleksis/apps/lesrooster/schema/__init__.py +++ b/aleksis/apps/lesrooster/schema/__init__.py @@ -217,7 +217,14 @@ class Query(graphene.ObjectType): info.context, TimeboundCourseConfig.objects.all(), "validity_range__school_term", - ) + ) + if not info.context.user.has_perm("lesrooster.view_timeboundcourseconfig_rule"): + course_configs = get_objects_for_user( + info.context.user, + "lesrooster.view_timeboundcourseconfig", + course_configs, + ) + subjects = Subject.objects.all().prefetch_related( Prefetch( diff --git a/aleksis/apps/lesrooster/schema/timebound_course_config.py b/aleksis/apps/lesrooster/schema/timebound_course_config.py index eb765f02..1e96d35d 100644 --- a/aleksis/apps/lesrooster/schema/timebound_course_config.py +++ b/aleksis/apps/lesrooster/schema/timebound_course_config.py @@ -1,3 +1,5 @@ +import time + from django.core.exceptions import ValidationError from django.utils.translation import gettext_lazy as _ @@ -67,12 +69,6 @@ class LesroosterExtendedCourseType(CourseType): @staticmethod def resolve_lr_timebound_course_configs(root, info, **kwargs): - if not info.context.user.has_perm("lesrooster.view_timeboundcourseconfig_rule"): - return get_objects_for_user( - info.context.user, - "lesrooster.view_timeboundcourseconfig", - root.lr_timebound_course_configs.all(), - ) return root.lr_timebound_course_configs.all() -- GitLab From 29e259cabbf6d75c7008d748e8f6e89e5bd09d71 Mon Sep 17 00:00:00 2001 From: Hangzhi Yu <hangzhi@protonmail.com> Date: Thu, 6 Mar 2025 23:44:45 +0100 Subject: [PATCH 27/34] Speed TCC raster up by refactoring components and using v-lazy --- .../TimeboundCourseConfigRaster.vue | 330 ++++++++++-------- .../TimeboundCourseConfigRasterCell.vue | 121 +++++++ 2 files changed, 299 insertions(+), 152 deletions(-) create mode 100644 aleksis/apps/lesrooster/frontend/components/timebound_course_config/TimeboundCourseConfigRasterCell.vue diff --git a/aleksis/apps/lesrooster/frontend/components/timebound_course_config/TimeboundCourseConfigRaster.vue b/aleksis/apps/lesrooster/frontend/components/timebound_course_config/TimeboundCourseConfigRaster.vue index 9eb63604..af322060 100644 --- a/aleksis/apps/lesrooster/frontend/components/timebound_course_config/TimeboundCourseConfigRaster.vue +++ b/aleksis/apps/lesrooster/frontend/components/timebound_course_config/TimeboundCourseConfigRaster.vue @@ -1,157 +1,187 @@ <script setup> -import PositiveSmallIntegerField from "aleksis.core/components/generic/forms/PositiveSmallIntegerField.vue"; import ValidityRangeField from "../validity_range/ValidityRangeField.vue"; import SubjectChip from "aleksis.apps.cursus/components/SubjectChip.vue"; -import TeacherField from "aleksis.apps.cursus/components/TeacherField.vue" +import TimeboundCourseConfigRasterCell from "./TimeboundCourseConfigRasterCell.vue"; </script> <template> - <div> - <v-data-table - disable-sort - disable-filtering - disable-pagination - hide-default-footer - :headers="headers" - :items="items" - :loading="loading" - > - <template #top> - <v-row> - <v-col - cols="6" - lg="3" - class="d-flex justify-space-between flex-wrap align-center" - > - <v-autocomplete - outlined - filled - multiple - hide-details - :items="groupsForPlanning" - item-text="shortName" - item-value="id" - return-object - :disabled="$apollo.queries.groupsForPlanning.loading" - :label="$t('lesrooster.timebound_course_config.groups')" - :loading="$apollo.queries.groupsForPlanning.loading" - v-model="selectedGroups" - class="mr-4" - /> - </v-col> + <v-data-table + disable-sort + disable-filtering + disable-pagination + hide-default-footer + :headers="headers" + :items="items" + :loading="loading" + > + <template #top> + <v-row> + <v-col + cols="6" + lg="3" + class="d-flex justify-space-between flex-wrap align-center" + > + <v-autocomplete + outlined + filled + multiple + hide-details + :items="groupsForPlanning" + item-text="shortName" + item-value="id" + return-object + :disabled="$apollo.queries.groupsForPlanning.loading" + :label="$t('lesrooster.timebound_course_config.groups')" + :loading="$apollo.queries.groupsForPlanning.loading" + v-model="selectedGroups" + class="mr-4" + /> + </v-col> - <v-col - cols="6" - lg="3" - class="d-flex justify-space-between flex-wrap align-center" - > - <validity-range-field - outlined - filled - hide-details - v-model="internalValidityRange" - :loading="$apollo.queries.currentValidityRange.loading" - /> - </v-col> + <v-col + cols="6" + lg="3" + class="d-flex justify-space-between flex-wrap align-center" + > + <validity-range-field + outlined + filled + hide-details + v-model="internalValidityRange" + :loading="$apollo.queries.currentValidityRange.loading" + /> + </v-col> - <v-col - cols="6" - lg="2" - class="d-flex justify-space-between flex-wrap align-center" - > - <v-switch - v-model="includeChildGroups" - inset - :label=" - $t( - 'lesrooster.timebound_course_config.filters.include_child_groups', - ) - " - :loading="$apollo.queries.subjects.loading" - ></v-switch> - </v-col> + <v-col + cols="6" + lg="2" + class="d-flex justify-space-between flex-wrap align-center" + > + <v-switch + v-model="includeChildGroups" + inset + :label=" + $t( + 'lesrooster.timebound_course_config.filters.include_child_groups', + ) + " + :loading="$apollo.queries.subjects.loading" + ></v-switch> + </v-col> - <v-spacer /> - </v-row> - </template> + <v-spacer /> + </v-row> + </template> - <!-- eslint-disable-next-line vue/valid-v-slot --> - <template #item.subject="{ item, value }"> - <subject-chip v-if="value" :subject="value" /> - </template> + <!-- eslint-disable-next-line vue/valid-v-slot --> + <template #item.subject="{ item, value }"> + <subject-chip v-if="value" :subject="value" /> + </template> - <template - v-for="(groupHeader, index) in groupHeaders" - #[tableItemSlotName(groupHeader)]="{ item, value, header }" + <template + v-for="(groupHeader, index) in groupHeaders" + #[tableItemSlotName(groupHeader)]="{ item, value, header }" + > + <timebound-course-config-raster-cell + :value="value" + :subject="item.subject" + :header="header" + :loading="loading" + @addCourse="addCourse" + @setCourseConfigData="setCourseConfigData" + /> + </template> + </v-data-table> + + <!-- <div> + <v-row> + <v-col + cols="6" + lg="3" + class="d-flex justify-space-between flex-wrap align-center" + > + <v-autocomplete + outlined + filled + multiple + hide-details + :items="groupsForPlanning" + item-text="shortName" + item-value="id" + return-object + :disabled="$apollo.queries.groupsForPlanning.loading" + :label="$t('lesrooster.timebound_course_config.groups')" + :loading="$apollo.queries.groupsForPlanning.loading" + v-model="selectedGroups" + class="mr-4" + /> + </v-col> + + <v-col + cols="6" + lg="3" + class="d-flex justify-space-between flex-wrap align-center" > - <div :key="index"> - <div v-if="value.length"> - <v-row - v-for="(course, index) in value" - :key="index" - no-gutters - class="mt-2" - > - <v-col cols="6"> - <positive-small-integer-field - dense - filled - class="mx-1" - :disabled="loading" - :value=" - getCurrentCourseConfig(course) - ? getCurrentCourseConfig(course).lessonQuota - : course.lessonQuota - " - :label="$t('lesrooster.timebound_course_config.lesson_quota')" - @change=" - (event) => - setCourseConfigData(course, item.subject, header, { - lessonQuota: event, - }) - " - /> - </v-col> - <v-col cols="6"> - <teacher-field - dense - filled - class="mx-1" - :disabled="loading" - :label="$t('lesrooster.timebound_course_config.teachers')" - :value=" - getCurrentCourseConfig(course) - ? getCurrentCourseConfig(course).teachers - : course.teachers - " - :show-subjects="true" - :priority-subject="item.subject" - :rules="$rules().isNonEmpty.build()" - @input=" - (event) => - setCourseConfigData(course, item.subject, header, { - teachers: event, - }) - " - /> - </v-col> - </v-row> - </div> - <div v-if="!value.length"> - <v-btn - block - icon - tile - outlined - @click="addCourse(item.subject.id, header.value)" - > - <v-icon>mdi-plus</v-icon> - </v-btn> - </div> - </div> - </template> - </v-data-table> - </div> + <validity-range-field + outlined + filled + hide-details + v-model="internalValidityRange" + :loading="$apollo.queries.currentValidityRange.loading" + /> + </v-col> + + <v-col + cols="6" + lg="2" + class="d-flex justify-space-between flex-wrap align-center" + > + <v-switch + v-model="includeChildGroups" + inset + :label=" + $t( + 'lesrooster.timebound_course_config.filters.include_child_groups', + ) + " + :loading="$apollo.queries.subjects.loading" + ></v-switch> + </v-col> + + <v-spacer /> + </v-row> + <v-simple-table + fixed-header + > + <thead> + <tr> + <th v-for="header in headers" :key="header.value" class="text-left"> + {{ header.text }} + </th> + </tr> + </thead> + <tbody> + <tr + v-for="(item, index) in items" + :key="index" + > + <td> + <subject-chip :subject="item.subject" /> + </td> + <td v-for="header in groupHeaders" :key="header.value"> + <timebound-course-config-raster-cell + :value="item[header.value]" + :subject="item.subject" + :header="header" + :loading="loading" + @addCourse="addCourse" + @setCourseConfigData="setCourseConfigData" + /> + </td> + </tr> + </tbody> + </v-simple-table> + </div> --> </template> <script> @@ -167,11 +197,10 @@ import { currentValidityRange as gqlCurrentValidityRange } from "../validity_ran import { gqlGroupsForPlanning } from "../helper.graphql"; import mutateMixin from "aleksis.core/mixins/mutateMixin.js"; -import formRulesMixin from "aleksis.core/mixins/formRulesMixin"; export default { name: "TimeboungCourseConfigRaster", - mixins: [formRulesMixin, mutateMixin], + mixins: [mutateMixin], data() { return { i18nKey: "lesrooster.timebound_course_config", @@ -210,13 +239,6 @@ export default { tableItemSlotName(header) { return "item." + header.value; }, - getCurrentCourseConfig(course) { - if (course.lrTimeboundCourseConfigs?.length) { - return course.lrTimeboundCourseConfigs[0]; - } else { - return null; - } - }, setCourseConfigData(course, subject, header, newValue) { if (course.newCourse) { let existingCreatedCourse = this.createdCourses.find( @@ -311,7 +333,8 @@ export default { ); }, generateTableItems(subjects) { - return subjects.map((subject) => { + const start = performance.now(); + const subjectsWithSortedCourses = subjects.map((subject) => { let { courses, ...reducedSubject } = subject; let groupCombinations = {}; @@ -359,6 +382,9 @@ export default { ...groupCombinations, }; }); + const end = performance.now(); + console.log(`Execution time: ${end - start} ms`); + return subjectsWithSortedCourses; }, }, computed: { diff --git a/aleksis/apps/lesrooster/frontend/components/timebound_course_config/TimeboundCourseConfigRasterCell.vue b/aleksis/apps/lesrooster/frontend/components/timebound_course_config/TimeboundCourseConfigRasterCell.vue new file mode 100644 index 00000000..71efac96 --- /dev/null +++ b/aleksis/apps/lesrooster/frontend/components/timebound_course_config/TimeboundCourseConfigRasterCell.vue @@ -0,0 +1,121 @@ +<script setup> +import PositiveSmallIntegerField from "aleksis.core/components/generic/forms/PositiveSmallIntegerField.vue"; +import TeacherField from "aleksis.apps.cursus/components/TeacherField.vue" +</script> + +<template> + <v-lazy + v-model="active" + :options="{ + threshold: .5 + }" + transition="fade-transition" + > + <div v-if="value.length"> + <v-row + v-for="(course, index) in value" + :key="index" + no-gutters + class="mt-2" + > + <v-col cols="6"> + <positive-small-integer-field + dense + filled + class="mx-1" + :disabled="loading" + :value=" + getCurrentCourseConfig(course) + ? getCurrentCourseConfig(course).lessonQuota + : course.lessonQuota + " + :label="$t('lesrooster.timebound_course_config.lesson_quota')" + @change=" + (event) => + $emit('setCourseConfigData', course, subject, header, { + lessonQuota: event, + }) + " + /> + </v-col> + <v-col cols="6"> + <teacher-field + dense + filled + class="mx-1" + :disabled="loading" + :label="$t('lesrooster.timebound_course_config.teachers')" + :value=" + getCurrentCourseConfig(course) + ? getCurrentCourseConfig(course).teachers + : course.teachers + " + :show-subjects="true" + :priority-subject="subject" + :rules="$rules().isNonEmpty.build()" + @input=" + (event) => + $emit('setCourseConfigData', course, subject, header, { + teachers: event, + }) + " + /> + </v-col> + </v-row> + </div> + <div v-else> + <v-btn + block + icon + tile + outlined + @click="$emit('addCourse', subject.id, header.value)" + > + <v-icon>mdi-plus</v-icon> + </v-btn> + </div> + </v-lazy> +</template> + +<script> +import formRulesMixin from "aleksis.core/mixins/formRulesMixin"; + +export default { + name: "TimeboundCourseConfigRasterCell", + mixins: [formRulesMixin], + emits: ["addCourse", "setCourseConfigData"], + props: { + value: { + type: Array, + required: true, + }, + subject: { + type: Object, + required: true, + }, + header: { + type: Object, + required: true, + }, + loading: { + type: Boolean, + required: false, + default: false, + }, + }, + data() { + return { + active: false, + }; + }, + methods: { + getCurrentCourseConfig(course) { + if (course.lrTimeboundCourseConfigs?.length) { + return course.lrTimeboundCourseConfigs[0]; + } else { + return null; + } + }, + }, +}; +</script> -- GitLab From 2edc01bf382ade60f6c9bcb6d77aa0fa0949d4dd Mon Sep 17 00:00:00 2001 From: Hangzhi Yu <hangzhi@protonmail.com> Date: Thu, 10 Apr 2025 16:45:27 +0200 Subject: [PATCH 28/34] Rename VR ID function argument --- aleksis/apps/lesrooster/schema/timebound_course_config.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/aleksis/apps/lesrooster/schema/timebound_course_config.py b/aleksis/apps/lesrooster/schema/timebound_course_config.py index 1e96d35d..ae485171 100644 --- a/aleksis/apps/lesrooster/schema/timebound_course_config.py +++ b/aleksis/apps/lesrooster/schema/timebound_course_config.py @@ -117,12 +117,12 @@ class CourseBatchCreateForValidityRangeMutation(graphene.Mutation): courses = graphene.List(LesroosterExtendedCourseType) @classmethod - def create(cls, info, course_input, validity_range_id): + def create(cls, info, course_input, validity_range): if info.context.user.has_perm("cursus.create_course_rule"): groups = Group.objects.filter(pk__in=course_input.groups) subject = Subject.objects.get(pk=course_input.subject) teachers = Person.objects.filter(pk__in=course_input.teachers) - validity_range = ValidityRange.objects.get(pk=validity_range_id) + validity_range = ValidityRange.objects.get(pk=validity_range) course = Course.objects.create( name=f"""{''.join(groups.values_list('short_name', flat=True) -- GitLab From 744ebb137981306826ff0f3575eb3fdc2cfaa20a Mon Sep 17 00:00:00 2001 From: Hangzhi Yu <hangzhi@protonmail.com> Date: Thu, 10 Apr 2025 16:47:46 +0200 Subject: [PATCH 29/34] Re-add perm check --- aleksis/apps/lesrooster/schema/timebound_course_config.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/aleksis/apps/lesrooster/schema/timebound_course_config.py b/aleksis/apps/lesrooster/schema/timebound_course_config.py index ae485171..f3f93d86 100644 --- a/aleksis/apps/lesrooster/schema/timebound_course_config.py +++ b/aleksis/apps/lesrooster/schema/timebound_course_config.py @@ -69,7 +69,9 @@ class LesroosterExtendedCourseType(CourseType): @staticmethod def resolve_lr_timebound_course_configs(root, info, **kwargs): - return root.lr_timebound_course_configs.all() + if info.context.user.has_perm("lesrooster.view_timeboundcourseconfig_rule"): + return root.lr_timebound_course_configs.all() + return [] class LesroosterExtendedSubjectType(SubjectType): -- GitLab From bc78eedf05d7f837147396370459e54cbfdd7d0b Mon Sep 17 00:00:00 2001 From: Hangzhi Yu <hangzhi@protonmail.com> Date: Thu, 10 Apr 2025 16:48:31 +0200 Subject: [PATCH 30/34] Drop commented out code sections --- .../TimeboundCourseConfigRaster.vue | 90 ------------------- 1 file changed, 90 deletions(-) diff --git a/aleksis/apps/lesrooster/frontend/components/timebound_course_config/TimeboundCourseConfigRaster.vue b/aleksis/apps/lesrooster/frontend/components/timebound_course_config/TimeboundCourseConfigRaster.vue index af322060..cef13fe7 100644 --- a/aleksis/apps/lesrooster/frontend/components/timebound_course_config/TimeboundCourseConfigRaster.vue +++ b/aleksis/apps/lesrooster/frontend/components/timebound_course_config/TimeboundCourseConfigRaster.vue @@ -92,96 +92,6 @@ import TimeboundCourseConfigRasterCell from "./TimeboundCourseConfigRasterCell.v /> </template> </v-data-table> - - <!-- <div> - <v-row> - <v-col - cols="6" - lg="3" - class="d-flex justify-space-between flex-wrap align-center" - > - <v-autocomplete - outlined - filled - multiple - hide-details - :items="groupsForPlanning" - item-text="shortName" - item-value="id" - return-object - :disabled="$apollo.queries.groupsForPlanning.loading" - :label="$t('lesrooster.timebound_course_config.groups')" - :loading="$apollo.queries.groupsForPlanning.loading" - v-model="selectedGroups" - class="mr-4" - /> - </v-col> - - <v-col - cols="6" - lg="3" - class="d-flex justify-space-between flex-wrap align-center" - > - <validity-range-field - outlined - filled - hide-details - v-model="internalValidityRange" - :loading="$apollo.queries.currentValidityRange.loading" - /> - </v-col> - - <v-col - cols="6" - lg="2" - class="d-flex justify-space-between flex-wrap align-center" - > - <v-switch - v-model="includeChildGroups" - inset - :label=" - $t( - 'lesrooster.timebound_course_config.filters.include_child_groups', - ) - " - :loading="$apollo.queries.subjects.loading" - ></v-switch> - </v-col> - - <v-spacer /> - </v-row> - <v-simple-table - fixed-header - > - <thead> - <tr> - <th v-for="header in headers" :key="header.value" class="text-left"> - {{ header.text }} - </th> - </tr> - </thead> - <tbody> - <tr - v-for="(item, index) in items" - :key="index" - > - <td> - <subject-chip :subject="item.subject" /> - </td> - <td v-for="header in groupHeaders" :key="header.value"> - <timebound-course-config-raster-cell - :value="item[header.value]" - :subject="item.subject" - :header="header" - :loading="loading" - @addCourse="addCourse" - @setCourseConfigData="setCourseConfigData" - /> - </td> - </tr> - </tbody> - </v-simple-table> - </div> --> </template> <script> -- GitLab From 61a8bd19fb2a02d139d7cbbf078f538b5f35aca1 Mon Sep 17 00:00:00 2001 From: Hangzhi Yu <hangzhi@protonmail.com> Date: Thu, 10 Apr 2025 17:08:49 +0200 Subject: [PATCH 31/34] Reformat --- .../TimeboundCourseConfigRaster.vue | 52 +++++++++++++------ .../TimeboundCourseConfigRasterCell.vue | 4 +- aleksis/apps/lesrooster/schema/__init__.py | 3 +- .../schema/timebound_course_config.py | 2 - 4 files changed, 38 insertions(+), 23 deletions(-) diff --git a/aleksis/apps/lesrooster/frontend/components/timebound_course_config/TimeboundCourseConfigRaster.vue b/aleksis/apps/lesrooster/frontend/components/timebound_course_config/TimeboundCourseConfigRaster.vue index cef13fe7..6f781874 100644 --- a/aleksis/apps/lesrooster/frontend/components/timebound_course_config/TimeboundCourseConfigRaster.vue +++ b/aleksis/apps/lesrooster/frontend/components/timebound_course_config/TimeboundCourseConfigRaster.vue @@ -80,6 +80,7 @@ import TimeboundCourseConfigRasterCell from "./TimeboundCourseConfigRasterCell.v <template v-for="(groupHeader, index) in groupHeaders" + :key="index" #[tableItemSlotName(groupHeader)]="{ item, value, header }" > <timebound-course-config-raster-cell @@ -338,28 +339,48 @@ export default { return Array.from(this.groupCombinationsSet); }, createdCoursesReady() { - return !!this.createdCourses.length && this.createdCourses.every((c) => { - return c?.groups?.length && c.name && c.subject && c?.teachers?.length - }); + return ( + !!this.createdCourses.length && + this.createdCourses.every((c) => { + return ( + c?.groups?.length && c.name && c.subject && c?.teachers?.length + ); + }) + ); }, createdCourseConfigsReady() { - return !!this.createdCourseConfigs.length && this.createdCourseConfigs.every((c) => { - return c.course && c.validityRange && c?.teachers?.length - }); + return ( + !!this.createdCourseConfigs.length && + this.createdCourseConfigs.every((c) => { + return c.course && c.validityRange && c?.teachers?.length; + }) + ); }, editedCourseConfigsReady() { - return !!this.editedCourseConfigs.length && this.editedCourseConfigs.every((c) => { - return c.id && (Object.hasOwn(c, "lessonQuota") || c?.teachers?.length); - }); + return ( + !!this.editedCourseConfigs.length && + this.editedCourseConfigs.every((c) => { + return ( + c.id && (Object.hasOwn(c, "lessonQuota") || c?.teachers?.length) + ); + }) + ); }, expandedQuery() { return { ...this.$apollo.queries.subjects.options, - variables: JSON.parse(this.$apollo.queries.subjects.previousVariablesJson), + variables: JSON.parse( + this.$apollo.queries.subjects.previousVariablesJson, + ), }; }, tableLoading() { - return this.loading || this.$apollo.queries.subjects.loading || this.$apollo.queries.groupsForPlanning.loading || this.$apollo.queries.currentValidityRange.loading; + return ( + this.loading || + this.$apollo.queries.subjects.loading || + this.$apollo.queries.groupsForPlanning.loading || + this.$apollo.queries.currentValidityRange.loading + ); }, }, watch: { @@ -417,7 +438,7 @@ export default { this.createdCourses = []; } }, - } + }, }, apollo: { currentValidityRange: { @@ -437,10 +458,7 @@ export default { subjects: { query: subjects, skip() { - return ( - !this.groupIDSet.size || - !this.internalValidityRange - ); + return !this.groupIDSet.size || !this.internalValidityRange; }, variables() { return { @@ -448,7 +466,7 @@ export default { includeChildGroups: this.includeChildGroups, }; }, - update: data => data.items, + update: (data) => data.items, result({ data }) { if (!data) return; this.items = this.generateTableItems(data.items); diff --git a/aleksis/apps/lesrooster/frontend/components/timebound_course_config/TimeboundCourseConfigRasterCell.vue b/aleksis/apps/lesrooster/frontend/components/timebound_course_config/TimeboundCourseConfigRasterCell.vue index 71efac96..75b7dc15 100644 --- a/aleksis/apps/lesrooster/frontend/components/timebound_course_config/TimeboundCourseConfigRasterCell.vue +++ b/aleksis/apps/lesrooster/frontend/components/timebound_course_config/TimeboundCourseConfigRasterCell.vue @@ -1,13 +1,13 @@ <script setup> import PositiveSmallIntegerField from "aleksis.core/components/generic/forms/PositiveSmallIntegerField.vue"; -import TeacherField from "aleksis.apps.cursus/components/TeacherField.vue" +import TeacherField from "aleksis.apps.cursus/components/TeacherField.vue"; </script> <template> <v-lazy v-model="active" :options="{ - threshold: .5 + threshold: 0.5, }" transition="fade-transition" > diff --git a/aleksis/apps/lesrooster/schema/__init__.py b/aleksis/apps/lesrooster/schema/__init__.py index dc4ea901..12ab60bb 100644 --- a/aleksis/apps/lesrooster/schema/__init__.py +++ b/aleksis/apps/lesrooster/schema/__init__.py @@ -217,7 +217,7 @@ class Query(graphene.ObjectType): info.context, TimeboundCourseConfig.objects.all(), "validity_range__school_term", - ) + ) if not info.context.user.has_perm("lesrooster.view_timeboundcourseconfig_rule"): course_configs = get_objects_for_user( info.context.user, @@ -225,7 +225,6 @@ class Query(graphene.ObjectType): course_configs, ) - subjects = Subject.objects.all().prefetch_related( Prefetch( "courses", diff --git a/aleksis/apps/lesrooster/schema/timebound_course_config.py b/aleksis/apps/lesrooster/schema/timebound_course_config.py index f3f93d86..38c42ca6 100644 --- a/aleksis/apps/lesrooster/schema/timebound_course_config.py +++ b/aleksis/apps/lesrooster/schema/timebound_course_config.py @@ -1,4 +1,3 @@ -import time from django.core.exceptions import ValidationError from django.utils.translation import gettext_lazy as _ @@ -10,7 +9,6 @@ from graphene_django_cud.mutations import ( DjangoBatchDeleteMutation, DjangoBatchPatchMutation, ) -from guardian.shortcuts import get_objects_for_user from aleksis.apps.cursus.models import Course, Subject from aleksis.apps.cursus.schema import CourseType, SubjectType -- GitLab From bcdf89a1ba2101145a122efbe0db85d8f15cffe8 Mon Sep 17 00:00:00 2001 From: Hangzhi Yu <hangzhi@protonmail.com> Date: Thu, 10 Apr 2025 17:21:03 +0200 Subject: [PATCH 32/34] Remove debugging prints --- .../timebound_course_config/TimeboundCourseConfigRaster.vue | 3 --- 1 file changed, 3 deletions(-) diff --git a/aleksis/apps/lesrooster/frontend/components/timebound_course_config/TimeboundCourseConfigRaster.vue b/aleksis/apps/lesrooster/frontend/components/timebound_course_config/TimeboundCourseConfigRaster.vue index 6f781874..94c01f6a 100644 --- a/aleksis/apps/lesrooster/frontend/components/timebound_course_config/TimeboundCourseConfigRaster.vue +++ b/aleksis/apps/lesrooster/frontend/components/timebound_course_config/TimeboundCourseConfigRaster.vue @@ -244,7 +244,6 @@ export default { ); }, generateTableItems(subjects) { - const start = performance.now(); const subjectsWithSortedCourses = subjects.map((subject) => { let { courses, ...reducedSubject } = subject; let groupCombinations = {}; @@ -293,8 +292,6 @@ export default { ...groupCombinations, }; }); - const end = performance.now(); - console.log(`Execution time: ${end - start} ms`); return subjectsWithSortedCourses; }, }, -- GitLab From f45a9e3310b137baa04189165601056ec94663e4 Mon Sep 17 00:00:00 2001 From: Hangzhi Yu <hangzhi@protonmail.com> Date: Thu, 10 Apr 2025 17:27:29 +0200 Subject: [PATCH 33/34] Add comments --- .../TimeboundCourseConfigRaster.vue | 27 +++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/aleksis/apps/lesrooster/frontend/components/timebound_course_config/TimeboundCourseConfigRaster.vue b/aleksis/apps/lesrooster/frontend/components/timebound_course_config/TimeboundCourseConfigRaster.vue index 94c01f6a..6ca10adb 100644 --- a/aleksis/apps/lesrooster/frontend/components/timebound_course_config/TimeboundCourseConfigRaster.vue +++ b/aleksis/apps/lesrooster/frontend/components/timebound_course_config/TimeboundCourseConfigRaster.vue @@ -151,13 +151,16 @@ export default { return "item." + header.value; }, setCourseConfigData(course, subject, header, newValue) { + // Handles input from individual raster cells. if (course.newCourse) { + // No course for the given combination has been created yet. let existingCreatedCourse = this.createdCourses.find( (c) => c.subject === subject.id && JSON.stringify(c.groups) === header.value, ); if (!existingCreatedCourse) { + // Adds new created course object with given data. this.createdCourses.push({ subject: subject.id, groups: JSON.parse(header.value), @@ -165,18 +168,22 @@ export default { ...newValue, }); } else { + // Sets given data in existing created course object. for (const key in newValue) { this.$set(existingCreatedCourse, key, newValue[key]); } } } else { + // Course already exists. if (!course.lrTimeboundCourseConfigs?.length) { + // No TCCs exist for given course. let existingCreatedCourseConfig = this.createdCourseConfigs.find( (c) => c.course === course.id && c.validityRange === this.internalValidityRange?.id, ); if (!existingCreatedCourseConfig) { + // Adds new TCC object with given data. this.createdCourseConfigs.push({ course: course.id, validityRange: this.internalValidityRange?.id, @@ -185,18 +192,22 @@ export default { ...newValue, }); } else { + // Sets given data in existing TCC object. for (const key in newValue) { this.$set(existingCreatedCourseConfig, key, newValue[key]); } } } else { + // TCC already exists let courseConfigID = course.lrTimeboundCourseConfigs[0].id; let existingEditedCourseConfig = this.editedCourseConfigs.find( (c) => c.id === courseConfigID, ); if (!existingEditedCourseConfig) { + // Adds new object representing edits made to existing TCC. this.editedCourseConfigs.push({ id: courseConfigID, ...newValue }); } else { + // Sets given data in existing TCC edit object. for (const key in newValue) { this.$set(existingEditedCourseConfig, key, newValue[key]); } @@ -205,6 +216,8 @@ export default { } }, updateCourseConfigs(cachedSubjects, incomingCourseConfigs) { + // Handles incoming TCCs on partial update after mutation. + // Find related existing course by subject and course ID. incomingCourseConfigs.forEach((newCourseConfig) => { const subject = cachedSubjects.find( (s) => s.id === newCourseConfig.course.subject.id, @@ -223,6 +236,8 @@ export default { return cachedSubjects; }, updateCreatedCourses(cachedSubjects, incomingCourses) { + // Handles incoming courses on partial update after mutation. + // Insert course into existing data by subject ID. incomingCourses.forEach((newCourse) => { const subject = cachedSubjects.find( (s) => s.id === newCourse.subject.id, @@ -237,6 +252,8 @@ export default { return cachedSubjects; }, addCourse(subject, groups) { + // Handles clicks on "+" button. + // Adds course for given subject/groups combination and marks it as newly created. this.$set( this.items.find((i) => i.subject.id === subject), groups, @@ -244,14 +261,17 @@ export default { ); }, generateTableItems(subjects) { + // Generates items for data table, sorted by subjects. const subjectsWithSortedCourses = subjects.map((subject) => { let { courses, ...reducedSubject } = subject; let groupCombinations = {}; courses.forEach((course) => { + // Aggregates all relevant group IDs. const ownGroupIDs = course.groups.map((group) => group.id); let groupIDs; + // If child groups should be included, add them. if ( this.includeChildGroups && ownGroupIDs.some((groupID) => !this.groupIDSet.has(groupID)) @@ -267,6 +287,7 @@ export default { groupIDs = JSON.stringify(ownGroupIDs.sort()); } + // Based on stringified aggregated groups, add group combination entry for subject. if (!groupCombinations[groupIDs]) { groupCombinations[groupIDs] = []; if (course.groups.length > 1) { @@ -297,9 +318,11 @@ export default { }, computed: { groupIDSet() { + // Group ID set without duplicates. return new Set(this.selectedGroups.map((group) => group.id)); }, groupCombinationHeaders() { + // Generates additional table headers based on unique group combinations found from existing courses. return this.subjectGroupCombinations.map((combination) => { let parsedCombination = JSON.parse(combination); return { @@ -324,6 +347,7 @@ export default { ...header, width: "20vw", })); + // Adds column for subjects. return [ { text: this.$t("lesrooster.timebound_course_config.subject"), @@ -336,6 +360,7 @@ export default { return Array.from(this.groupCombinationsSet); }, createdCoursesReady() { + // Indicates whether local created course data is ready to be used in mutation. return ( !!this.createdCourses.length && this.createdCourses.every((c) => { @@ -346,6 +371,7 @@ export default { ); }, createdCourseConfigsReady() { + // Indicates whether local created TCCs data is ready to be used in mutation. return ( !!this.createdCourseConfigs.length && this.createdCourseConfigs.every((c) => { @@ -354,6 +380,7 @@ export default { ); }, editedCourseConfigsReady() { + // Indicates whether local edited TCCs data is ready to be used in mutation. return ( !!this.editedCourseConfigs.length && this.editedCourseConfigs.every((c) => { -- GitLab From 68824efaad97325826fa8af2ba18a9eef75ee699 Mon Sep 17 00:00:00 2001 From: Jonathan Weth <git@jonathanweth.de> Date: Fri, 11 Apr 2025 15:41:39 +0200 Subject: [PATCH 34/34] Fix lint and reformat --- .../TimeboundCourseConfigRaster.vue | 2 +- aleksis/apps/lesrooster/models.py | 10 +++++----- aleksis/apps/lesrooster/schema/__init__.py | 5 +++-- .../schema/timebound_course_bundle.py | 20 +++++++------------ .../schema/timebound_course_config.py | 1 - 5 files changed, 16 insertions(+), 22 deletions(-) diff --git a/aleksis/apps/lesrooster/frontend/components/timebound_course_config/TimeboundCourseConfigRaster.vue b/aleksis/apps/lesrooster/frontend/components/timebound_course_config/TimeboundCourseConfigRaster.vue index 6ca10adb..15d9ba8e 100644 --- a/aleksis/apps/lesrooster/frontend/components/timebound_course_config/TimeboundCourseConfigRaster.vue +++ b/aleksis/apps/lesrooster/frontend/components/timebound_course_config/TimeboundCourseConfigRaster.vue @@ -80,10 +80,10 @@ import TimeboundCourseConfigRasterCell from "./TimeboundCourseConfigRasterCell.v <template v-for="(groupHeader, index) in groupHeaders" - :key="index" #[tableItemSlotName(groupHeader)]="{ item, value, header }" > <timebound-course-config-raster-cell + :key="index" :value="value" :subject="item.subject" :header="header" diff --git a/aleksis/apps/lesrooster/models.py b/aleksis/apps/lesrooster/models.py index 9525137b..012b9ec3 100644 --- a/aleksis/apps/lesrooster/models.py +++ b/aleksis/apps/lesrooster/models.py @@ -480,8 +480,10 @@ class LessonBundle(ExtensibleModel): ) lesson_bundle.lessons.set( - [Lesson.create_from_course(course, validity_range) - for course in course_bundle.courses.all()] + [ + Lesson.create_from_course(course, validity_range) + for course in course_bundle.courses.all() + ] ) return lesson_bundle @@ -553,9 +555,7 @@ class Lesson(TeacherPropertiesMixin, RoomPropertiesMixin, ExtensibleModel): def create_from_course(cls, course: Course, validity_range: ValidityRange) -> "Lesson": """Create a lesson from a course backed by a validity range.""" # Lookup the TCC for the course in the validity_range - tcc = TimeboundCourseConfig.objects.get( - course=course, validity_range=validity_range - ) + tcc = TimeboundCourseConfig.objects.get(course=course, validity_range=validity_range) lesson = cls.objects.create( course=course, subject=course.subject, diff --git a/aleksis/apps/lesrooster/schema/__init__.py b/aleksis/apps/lesrooster/schema/__init__.py index 74942fa8..f67ad826 100644 --- a/aleksis/apps/lesrooster/schema/__init__.py +++ b/aleksis/apps/lesrooster/schema/__init__.py @@ -275,8 +275,9 @@ class Query(graphene.ObjectType): return [] return graphene_django_optimizer.query( - CourseBundle.objects - .filter(Q(courses__groups__id=group) | Q(courses__groups__parent_groups__id=group)) + CourseBundle.objects.filter( + Q(courses__groups__id=group) | Q(courses__groups__parent_groups__id=group) + ) .distinct() .annotate(validity_range_id=Value(validity_range)), info, diff --git a/aleksis/apps/lesrooster/schema/timebound_course_bundle.py b/aleksis/apps/lesrooster/schema/timebound_course_bundle.py index 500be9e8..918e44eb 100644 --- a/aleksis/apps/lesrooster/schema/timebound_course_bundle.py +++ b/aleksis/apps/lesrooster/schema/timebound_course_bundle.py @@ -15,22 +15,16 @@ class TimeboundCourseType(BaseCourseType): def resolve_lesson_quota(root, info): """Resolve lesson_quota from timebound_course_config""" - return ( - TimeboundCourseConfig.objects - .get( - course=root, - validity_range__id=root.validity_range_id, - ) - .lesson_quota - ) + return TimeboundCourseConfig.objects.get( + course=root, + validity_range__id=root.validity_range_id, + ).lesson_quota def resolve_teachers(root, info): """Resolve teachers from timebound_course_config""" - return ( - TimeboundCourseConfig.objects - .get(course=root, validity_range__id=root.validity_range_id) - .teachers.all() - ) + return TimeboundCourseConfig.objects.get( + course=root, validity_range__id=root.validity_range_id + ).teachers.all() class TimeboundCourseBundleType(BaseCourseBundleType): diff --git a/aleksis/apps/lesrooster/schema/timebound_course_config.py b/aleksis/apps/lesrooster/schema/timebound_course_config.py index 38c42ca6..b8a38461 100644 --- a/aleksis/apps/lesrooster/schema/timebound_course_config.py +++ b/aleksis/apps/lesrooster/schema/timebound_course_config.py @@ -1,4 +1,3 @@ - from django.core.exceptions import ValidationError from django.utils.translation import gettext_lazy as _ -- GitLab