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/16] 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/16] 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/16] 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/16] 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/16] 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/16] 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/16] 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/16] 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/16] 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/16] 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/16] 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 e9e81d97d0bef41a0aac3d461429a6d0e3931c52 Mon Sep 17 00:00:00 2001
From: Hangzhi Yu <hangzhi@protonmail.com>
Date: Tue, 14 May 2024 12:24:56 +0200
Subject: [PATCH 12/16] Remove unused copy from different 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 e9a902dd..69e3d4d5 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 cb30b7ae75da04ed507854e58a3f24da928c8046 Mon Sep 17 00:00:00 2001
From: Hangzhi Yu <hangzhi@protonmail.com>
Date: Tue, 14 May 2024 12:41:14 +0200
Subject: [PATCH 13/16] Make rows in course planning more dense

---
 .../TimeboundCourseConfigRaster.vue           | 19 +++++++++++--------
 .../apps/lesrooster/frontend/messages/en.json |  2 +-
 2 files changed, 12 insertions(+), 9 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 69e3d4d5..2117db79 100644
--- a/aleksis/apps/lesrooster/frontend/components/timebound_course_config/TimeboundCourseConfigRaster.vue
+++ b/aleksis/apps/lesrooster/frontend/components/timebound_course_config/TimeboundCourseConfigRaster.vue
@@ -108,12 +108,13 @@ import SubjectChip from "aleksis.apps.cursus/components/SubjectChip.vue";
               v-for="(course, index) in value"
               :key="index"
               no-gutters
-              class="mt-2"
+              class="my-1"
             >
-              <v-col cols="6">
+              <v-col cols="4">
                 <positive-small-integer-field
                   dense
-                  filled
+                  outlined
+                  hide-details
                   class="mx-1"
                   :disabled="loading"
                   :value="
@@ -130,14 +131,16 @@ import SubjectChip from "aleksis.apps.cursus/components/SubjectChip.vue";
                   "
                 />
               </v-col>
-              <v-col cols="6">
+              <v-col cols="8">
                 <v-autocomplete
-                  counter
                   dense
-                  filled
+                  outlined
                   multiple
+                  chips
+                  small-chips
+                  hide-details
                   :items="getTeacherList(item.subject.teachers)"
-                  item-text="fullName"
+                  item-text="shortName"
                   item-value="id"
                   class="mx-1"
                   :disabled="loading"
@@ -447,7 +450,7 @@ export default {
     headers() {
       let groupHeadersWithWidth = this.groupHeaders.map((header) => ({
         ...header,
-        width: "20vw",
+        width: "15vw",
       }));
       return [
         {
diff --git a/aleksis/apps/lesrooster/frontend/messages/en.json b/aleksis/apps/lesrooster/frontend/messages/en.json
index 02c440cd..8bf279d3 100644
--- a/aleksis/apps/lesrooster/frontend/messages/en.json
+++ b/aleksis/apps/lesrooster/frontend/messages/en.json
@@ -72,7 +72,7 @@
       "raster_menu_title": "Plan courses",
       "title": "Timebound course config",
       "title_plural": "Timebound course configs",
-      "lesson_quota": "Scheduled lesson quota",
+      "lesson_quota": "Lessons",
       "course": "Course",
       "groups": "Groups",
       "teachers": "Teachers",
-- 
GitLab


From 2c0743cc61ed0091b0cbd4e4dd29b264e8b181d1 Mon Sep 17 00:00:00 2001
From: Hangzhi Yu <hangzhi@protonmail.com>
Date: Tue, 14 May 2024 13:12:05 +0200
Subject: [PATCH 14/16] Use i18n key for VR select label

---
 .../timebound_course_config/TimeboundCourseConfigRaster.vue    | 2 +-
 aleksis/apps/lesrooster/frontend/messages/en.json              | 3 ++-
 2 files changed, 3 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 2117db79..ee32df73 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,7 @@ import SubjectChip from "aleksis.apps.cursus/components/SubjectChip.vue";
             <validity-range-field
               outlined
               filled
-              label="Select Validity Range"
+              :label="$t('lesrooster.timebound_course_config.filters.validity_range')"
               hide-details
               v-model="internalValidityRange"
               :loading="$apollo.queries.currentValidityRange.loading"
diff --git a/aleksis/apps/lesrooster/frontend/messages/en.json b/aleksis/apps/lesrooster/frontend/messages/en.json
index 8bf279d3..5044cf19 100644
--- a/aleksis/apps/lesrooster/frontend/messages/en.json
+++ b/aleksis/apps/lesrooster/frontend/messages/en.json
@@ -83,7 +83,8 @@
       "create_timebound_course_config": "Create timebound course config",
       "subject": "Subject",
       "filters": {
-        "include_child_groups": "Include courses from child groups"
+        "include_child_groups": "Include courses from child groups",
+        "validity_range": "Validity range"
       }
     },
     "lesson_raster": {
-- 
GitLab


From 95fcef558584753a340e6921e07814f26e8e86ee Mon Sep 17 00:00:00 2001
From: Hangzhi Yu <hangzhi@protonmail.com>
Date: Tue, 14 May 2024 13:31:54 +0200
Subject: [PATCH 15/16] Make group select field more compact

---
 .../TimeboundCourseConfigRaster.vue           | 24 +++++++++++++++++--
 .../apps/lesrooster/frontend/messages/de.json | 12 ++++++++--
 .../apps/lesrooster/frontend/messages/en.json |  8 +++++--
 3 files changed, 38 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 ee32df73..90938cb3 100644
--- a/aleksis/apps/lesrooster/frontend/components/timebound_course_config/TimeboundCourseConfigRaster.vue
+++ b/aleksis/apps/lesrooster/frontend/components/timebound_course_config/TimeboundCourseConfigRaster.vue
@@ -34,11 +34,31 @@ import SubjectChip from "aleksis.apps.cursus/components/SubjectChip.vue";
               item-value="id"
               return-object
               :disabled="$apollo.queries.groupsForPlanning.loading"
-              :label="$t('lesrooster.timebound_course_config.groups')"
+              :label="$t('lesrooster.timebound_course_config.filters.groups.label')"
               :loading="$apollo.queries.groupsForPlanning.loading"
               v-model="selectedGroups"
               class="mr-4"
-            />
+            >
+              <template #selection="{ item, index }">
+                <div v-if="selectedGroups.length === groupsForPlanning.length">
+                  <span
+                    v-if="index === 0"
+                    class="grey--text"
+                  >
+                    {{ $t("lesrooster.timebound_course_config.filters.groups.all_groups") }}
+                  </span>
+                </div>
+                <div v-else>
+                  <span v-if="index === 0">{{ item.shortName }}</span>
+                  <span
+                    v-if="index === 1"
+                    class="grey--text text-caption"
+                  >
+                    {{ $t("lesrooster.timebound_course_config.filters.groups.other_groups", { groupCount: selectedGroups.length - 1 }) }}
+                  </span>
+                </div>
+              </template>
+            </v-autocomplete>
           </v-col>
 
           <v-col
diff --git a/aleksis/apps/lesrooster/frontend/messages/de.json b/aleksis/apps/lesrooster/frontend/messages/de.json
index 7f151145..5a72ae73 100644
--- a/aleksis/apps/lesrooster/frontend/messages/de.json
+++ b/aleksis/apps/lesrooster/frontend/messages/de.json
@@ -74,14 +74,22 @@
       "title_plural": "Kurskonfigurationen",
       "lesson_quota": "Stundenpensum",
       "course": "Kurs",
-      "groups": "Gruppen",
       "teachers": "Lehrkräfte",
       "teachers_for": "Lehrkräfte für",
       "subject_teachers": "Fachlehrkräfte",
       "all_teachers": "Alle Lehrkräfte",
       "no_course_selected": "Kein Kurs ausgewählt",
       "create_timebound_course_config": "Kurskonfiguration erstellen",
-      "subject": "Fach"
+      "subject": "Fach",
+      "filters": {
+        "include_child_groups": "Kurse von Kindgruppen einbeziehen",
+        "validity_range": "Gültigkeitszeitraum",
+        "groups": {
+          "label": "Gruppen",
+          "other_groups": " (+{groupCount} andere)",
+          "all_groups": "Alle ausgewählt"
+        }
+      }
     },
     "lesson_raster": {
       "menu_title": "Stundenraster"
diff --git a/aleksis/apps/lesrooster/frontend/messages/en.json b/aleksis/apps/lesrooster/frontend/messages/en.json
index 5044cf19..233fa044 100644
--- a/aleksis/apps/lesrooster/frontend/messages/en.json
+++ b/aleksis/apps/lesrooster/frontend/messages/en.json
@@ -74,7 +74,6 @@
       "title_plural": "Timebound course configs",
       "lesson_quota": "Lessons",
       "course": "Course",
-      "groups": "Groups",
       "teachers": "Teachers",
       "teachers_for": "Teachers for",
       "subject_teachers": "Teachers for this subject",
@@ -84,7 +83,12 @@
       "subject": "Subject",
       "filters": {
         "include_child_groups": "Include courses from child groups",
-        "validity_range": "Validity range"
+        "validity_range": "Validity range",
+        "groups": {
+          "label": "Groups",
+          "other_groups": " (+{groupCount} others)",
+          "all_groups": "All selected"
+        }
       }
     },
     "lesson_raster": {
-- 
GitLab


From 3d6488a5c246e141b0721452f7947ad7d0a60f8a Mon Sep 17 00:00:00 2001
From: Hangzhi Yu <hangzhi@protonmail.com>
Date: Tue, 14 May 2024 13:57:11 +0200
Subject: [PATCH 16/16] Reformat

---
 .../TimeboundCourseConfigRaster.vue           | 45 +++++++++++--------
 aleksis/apps/lesrooster/schema/__init__.py    | 10 +++--
 2 files changed, 34 insertions(+), 21 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 90938cb3..32fa24b2 100644
--- a/aleksis/apps/lesrooster/frontend/components/timebound_course_config/TimeboundCourseConfigRaster.vue
+++ b/aleksis/apps/lesrooster/frontend/components/timebound_course_config/TimeboundCourseConfigRaster.vue
@@ -34,27 +34,32 @@ import SubjectChip from "aleksis.apps.cursus/components/SubjectChip.vue";
               item-value="id"
               return-object
               :disabled="$apollo.queries.groupsForPlanning.loading"
-              :label="$t('lesrooster.timebound_course_config.filters.groups.label')"
-              :loading="$apollo.queries.groupsForPlanning.loading"
+              :label="
+                $t('lesrooster.timebound_course_config.filters.groups.label')
+              "
+              :loading="$apollo.queries.grouptemplatesForPlanning.loading"
               v-model="selectedGroups"
               class="mr-4"
             >
               <template #selection="{ item, index }">
                 <div v-if="selectedGroups.length === groupsForPlanning.length">
-                  <span
-                    v-if="index === 0"
-                    class="grey--text"
-                  >
-                    {{ $t("lesrooster.timebound_course_config.filters.groups.all_groups") }}
+                  <span v-if="index === 0" class="grey--text">
+                    {{
+                      $t(
+                        "lesrooster.timebound_course_config.filters.groups.all_groups",
+                      )
+                    }}
                   </span>
                 </div>
                 <div v-else>
                   <span v-if="index === 0">{{ item.shortName }}</span>
-                  <span
-                    v-if="index === 1"
-                    class="grey--text text-caption"
-                  >
-                    {{ $t("lesrooster.timebound_course_config.filters.groups.other_groups", { groupCount: selectedGroups.length - 1 }) }}
+                  <span v-if="index === 1" class="grey--text text-caption">
+                    {{
+                      $t(
+                        "lesrooster.timebound_course_config.filters.groups.other_groups",
+                        { groupCount: selectedGroups.length - 1 },
+                      )
+                    }}
                   </span>
                 </div>
               </template>
@@ -69,7 +74,9 @@ import SubjectChip from "aleksis.apps.cursus/components/SubjectChip.vue";
             <validity-range-field
               outlined
               filled
-              :label="$t('lesrooster.timebound_course_config.filters.validity_range')"
+              :label="
+                $t('lesrooster.timebound_course_config.filters.validity_range')
+              "
               hide-details
               v-model="internalValidityRange"
               :loading="$apollo.queries.currentValidityRange.loading"
@@ -94,7 +101,7 @@ import SubjectChip from "aleksis.apps.cursus/components/SubjectChip.vue";
           </v-col>
 
           <v-spacer />
-          
+
           <v-col
             cols="4"
             lg="1"
@@ -300,9 +307,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 &&
@@ -510,7 +515,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