diff --git a/aleksis/apps/chronos/frontend/components/Timetable.vue b/aleksis/apps/chronos/frontend/components/Timetable.vue index 83b13708fb6a2ed38c9cd3ee7a8c34f129e1ffee..fbbaaddbb90d8912d0271c0d4ff1f8d97696b0af 100644 --- a/aleksis/apps/chronos/frontend/components/Timetable.vue +++ b/aleksis/apps/chronos/frontend/components/Timetable.vue @@ -1,203 +1,19 @@ +<script setup> +import TimetableWrapper from "./TimetableWrapper.vue"; +</script> <script> -import { gqlAvailableTimetables } from "./timetables.graphql"; -import NoTimetableCard from "./NoTimetableCard.vue"; -import SelectTimetable from "./SelectTimetable.vue"; -import timetableTypes from "./timetableTypes"; - export default { name: "Timetable", - components: { NoTimetableCard, SelectTimetable }, - apollo: { - availableTimetables: { - query: gqlAvailableTimetables, - result() { - if ( - !this.selected && - this.$route.params.id && - this.$route.params.type - ) { - this.selectTimetable( - this.availableTimetables.find( - (t) => - t.objId === this.$route.params.id && - t.type.toLowerCase() === this.$route.params.type, - ), - ); - } - }, - }, - }, - data() { - return { - availableTimetables: [], - selected: null, - search: "", - selectedTypes: ["GROUP", "TEACHER", "ROOM"], - types: timetableTypes, - selectDialog: false, - }; - }, - watch: { - selected(selected) { - // Align navigation with currently selected timetable - if (!selected) { - this.$router.push({ name: "chronos.timetable" }); - } else if ( - selected.objId !== this.$route.params.id || - selected.type.toLowerCase() !== this.$route.params.type - ) { - this.$router.push({ - name: "chronos.timetableWithId", - params: { - type: selected.type.toLowerCase(), - id: selected.objId, - }, - }); - } - }, - }, - methods: { - findNextTimetable(offset = 1) { - const currentIndex = this.availableTimetablesIds.indexOf( - this.selected.id, - ); - const newIndex = currentIndex + offset; - if (newIndex < 0 || newIndex >= this.availableTimetablesIds.length) { - return null; - } - return this.availableTimetables[newIndex]; - }, - selectTimetable(timetable) { - this.selected = timetable; - }, - }, - computed: { - selectedTypesFull() { - return this.selectedTypes.map((type) => { - return this.types[type]; - }); - }, - availableTimetablesFiltered() { - // Filter timetables by selected types - return this.availableTimetables.filter((timetable) => { - return this.selectedTypes.indexOf(timetable.type) !== -1; - }); - }, - availableTimetablesIds() { - return this.availableTimetables.map((timetable) => timetable.id); - }, - prevTimetable() { - return this.findNextTimetable(-1); - }, - nextTimetable() { - return this.findNextTimetable(1); - }, - }, }; </script> <template> - <div> - <v-row> - <v-dialog - v-model="selectDialog" - fullscreen - hide-overlay - transition="dialog-bottom-transition" - > - <v-card> - <v-toolbar dark color="primary"> - <v-toolbar-title>{{ - $t("chronos.timetable.select") - }}</v-toolbar-title> - <v-spacer></v-spacer> - </v-toolbar> - <select-timetable - v-model="selected" - @input="selectDialog = false" - :available-timetables="availableTimetables" - /> - </v-card> - </v-dialog> - - <v-col md="3" lg="3" xl="3" v-if="$vuetify.breakpoint.lgAndUp"> - <v-card> - <select-timetable - v-model="selected" - :available-timetables="availableTimetables" - /> - </v-card> - </v-col> - <v-col sm="12" md="12" lg="9" xl="9" class="full-height"> - <!-- No timetable card--> - <no-timetable-card - v-if="selected == null" - @selectTimetable="selectDialog = true" - /> - - <!-- Calendar card--> - <v-card v-else> - <div class="d-flex flex-column" v-if="$vuetify.breakpoint.smAndDown"> - <v-card-title class="pt-2"> - <v-btn - icon - :disabled="!prevTimetable" - @click="selectTimetable(prevTimetable)" - :title="$t('chronos.timetable.prev')" - class="mr-1" - > - <v-icon>mdi-chevron-left</v-icon> - </v-btn> - <v-spacer /> - <v-chip outlined color="secondary" @click="selectDialog = true"> - {{ selected.name }} - <v-icon right>mdi-chevron-down</v-icon> - </v-chip> - <v-spacer /> - <v-btn - icon - :disabled="!nextTimetable" - @click="selectTimetable(nextTimetable)" - :title="$t('chronos.timetable.next')" - class="ml-1 float-right" - > - <v-icon>mdi-chevron-right</v-icon> - </v-btn> - </v-card-title> - </div> - - <div class="d-flex flex-wrap justify-space-between mb-2" v-else> - <v-card-title> - {{ selected.name }} - </v-card-title> - <div class="pa-2 mt-1"> - <v-btn - icon - :disabled="!prevTimetable" - @click="selectTimetable(prevTimetable)" - :title="$t('chronos.timetable.prev')" - > - <v-icon>mdi-chevron-left</v-icon> - </v-btn> - <v-chip label color="secondary" outlined class="mx-1">{{ - selected.shortName - }}</v-chip> - <v-btn - icon - :disabled="!nextTimetable" - @click="selectTimetable(nextTimetable)" - :title="$t('chronos.timetable.next')" - > - <v-icon>mdi-chevron-right</v-icon> - </v-btn> - </div> - </div> - <calendar-with-controls - :calendar-feeds="[{ name: 'lesson' }, { name: 'supervision' }]" - :params="{ type: selected.type, id: selected.objId }" - /> - </v-card> - </v-col> - </v-row> - </div> + <timetable-wrapper> + <template #default="{ selected }"> + <calendar-with-controls + :calendar-feeds="[{ name: 'lesson' }, { name: 'supervision' }]" + :params="{ type: selected.type, id: selected.objId }" + /> + </template> + </timetable-wrapper> </template> diff --git a/aleksis/apps/chronos/frontend/components/TimetableWrapper.vue b/aleksis/apps/chronos/frontend/components/TimetableWrapper.vue new file mode 100644 index 0000000000000000000000000000000000000000..d3228681ac045848c0d04c98b96f01e7e7bda508 --- /dev/null +++ b/aleksis/apps/chronos/frontend/components/TimetableWrapper.vue @@ -0,0 +1,234 @@ +<script> +import { gqlAvailableTimetables } from "./timetables.graphql"; +import NoTimetableCard from "./NoTimetableCard.vue"; +import SelectTimetable from "./SelectTimetable.vue"; +import timetableTypes from "./timetableTypes"; + +export default { + name: "TimetableWrapper", + components: { NoTimetableCard, SelectTimetable }, + apollo: { + availableTimetables: { + query: gqlAvailableTimetables, + result() { + if ( + !this.selected && + this.$route.params.id && + this.$route.params.type + ) { + this.selectTimetable( + this.availableTimetables.find( + (t) => + t.objId === this.$route.params.id && + t.type.toLowerCase() === this.$route.params.type, + ), + ); + } + }, + }, + }, + data() { + return { + availableTimetables: [], + selected: null, + search: "", + selectedTypes: ["GROUP", "TEACHER", "ROOM"], + types: timetableTypes, + selectDialog: false, + }; + }, + props: { + onSelected: { + type: Function, + required: false, + default: null, + }, + }, + watch: { + selected(selected) { + if (this.onSelected) { + this.onSelected(selected); + return; + } + // Align navigation with currently selected timetable + if (!selected) { + this.$router.push({ name: "chronos.timetable" }); + } else if ( + selected.objId !== this.$route.params.id || + selected.type.toLowerCase() !== this.$route.params.type + ) { + this.$router.push({ + name: "chronos.timetableWithId", + params: { + type: selected.type.toLowerCase(), + id: selected.objId, + }, + }); + } + }, + }, + methods: { + findNextTimetable(offset = 1) { + const currentIndex = this.availableTimetablesIds.indexOf( + this.selected.id, + ); + const newIndex = currentIndex + offset; + if (newIndex < 0 || newIndex >= this.availableTimetablesIds.length) { + return null; + } + return this.availableTimetables[newIndex]; + }, + selectTimetable(timetable) { + this.selected = timetable; + }, + }, + computed: { + selectedTypesFull() { + return this.selectedTypes.map((type) => { + return this.types[type]; + }); + }, + availableTimetablesFiltered() { + // Filter timetables by selected types + return this.availableTimetables.filter((timetable) => { + return this.selectedTypes.indexOf(timetable.type) !== -1; + }); + }, + availableTimetablesIds() { + return this.availableTimetables.map((timetable) => timetable.id); + }, + prevTimetable() { + return this.findNextTimetable(-1); + }, + nextTimetable() { + return this.findNextTimetable(1); + }, + }, +}; +</script> + +<template> + <div> + <v-row> + <v-dialog + v-model="selectDialog" + fullscreen + hide-overlay + transition="dialog-bottom-transition" + > + <v-card> + <v-toolbar dark color="primary"> + <v-btn icon dark @click="selectDialog = false"> + <v-icon>mdi-close</v-icon> + </v-btn> + <v-toolbar-title>{{ + $t("chronos.timetable.select") + }}</v-toolbar-title> + <v-spacer></v-spacer> + </v-toolbar> + <slot + name="additionalSelect" + :selected="selected" + :mobile="true" + ></slot> + <select-timetable + v-model="selected" + @input="selectDialog = false" + :available-timetables="availableTimetables" + /> + </v-card> + </v-dialog> + + <v-col md="3" lg="3" xl="3" v-if="$vuetify.breakpoint.lgAndUp"> + <slot + name="additionalSelect" + :selected="selected" + :mobile="false" + ></slot> + <v-card> + <select-timetable + v-model="selected" + :available-timetables="availableTimetables" + /> + </v-card> + </v-col> + <v-col sm="12" md="12" lg="9" xl="9" class="full-height"> + <!-- No timetable card--> + <no-timetable-card + v-if="selected == null" + @selectTimetable="selectDialog = true" + /> + + <!-- Calendar card--> + <v-card v-else> + <div class="d-flex flex-column" v-if="$vuetify.breakpoint.smAndDown"> + <v-card-title class="pt-2"> + <v-btn + icon + :disabled="!prevTimetable" + @click="selectTimetable(prevTimetable)" + :title="$t('chronos.timetable.prev')" + class="mr-1" + > + <v-icon>mdi-chevron-left</v-icon> + </v-btn> + <v-spacer /> + <v-chip outlined color="secondary" @click="selectDialog = true"> + {{ selected.name }} + <v-icon right>mdi-chevron-down</v-icon> + </v-chip> + <v-spacer /> + <v-btn + icon + :disabled="!nextTimetable" + @click="selectTimetable(nextTimetable)" + :title="$t('chronos.timetable.next')" + class="ml-1 float-right" + > + <v-icon>mdi-chevron-right</v-icon> + </v-btn> + </v-card-title> + <slot + name="additionalButton" + :selected="selected" + :mobile="true" + ></slot> + </div> + + <div class="d-flex flex-wrap justify-space-between mb-2" v-else> + <v-card-title> + {{ selected.name }} + <slot + name="additionalButton" + :selected="selected" + :mobile="false" + ></slot> + </v-card-title> + <div class="pa-2 mt-1"> + <v-btn + icon + :disabled="!prevTimetable" + @click="selectTimetable(prevTimetable)" + :title="$t('chronos.timetable.prev')" + > + <v-icon>mdi-chevron-left</v-icon> + </v-btn> + <v-chip label color="secondary" outlined class="mx-1">{{ + selected.shortName + }}</v-chip> + <v-btn + icon + :disabled="!nextTimetable" + @click="selectTimetable(nextTimetable)" + :title="$t('chronos.timetable.next')" + > + <v-icon>mdi-chevron-right</v-icon> + </v-btn> + </div> + </div> + <slot :selected="selected"></slot> + </v-card> + </v-col> + </v-row> + </div> +</template> diff --git a/aleksis/apps/chronos/frontend/index.js b/aleksis/apps/chronos/frontend/index.js index df633c3f25a0a63864551479aec686398e091ad7..2080d78cfb8899c06979143304062b724a63d068 100644 --- a/aleksis/apps/chronos/frontend/index.js +++ b/aleksis/apps/chronos/frontend/index.js @@ -11,9 +11,6 @@ export default { iconActive: "mdi-school", validators: [hasPersonValidator], }, - props: { - byTheGreatnessOfTheAlmightyAleksolotlISwearIAmWorthyOfUsingTheLegacyBaseTemplate: true, - }, children: [ { path: "timetable/", @@ -22,6 +19,7 @@ export default { meta: { inMenu: true, titleKey: "chronos.timetable.menu_title", + toolbarTitle: "chronos.timetable.menu_title", icon: "mdi-grid", permission: "chronos.view_timetable_overview_rule", fullWidth: true, diff --git a/aleksis/apps/chronos/managers.py b/aleksis/apps/chronos/managers.py index 72b9a30645eebbeba469f33fa886143b3370bde0..dd63807aa5ebee41234a9eaff786b6cb6a5e2e06 100644 --- a/aleksis/apps/chronos/managers.py +++ b/aleksis/apps/chronos/managers.py @@ -876,6 +876,15 @@ class LessonEventQuerySet(PolymorphicQuerySet): ) return self.filter(Q(teachers=teacher) | Q(pk__in=amended)).distinct() + def for_participant(self, person: Union[int, Person]) -> "LessonEventQuerySet": + """Get all lesson events the person participates in (including amends).""" + amended = self.filter( + Q(amended_by__isnull=False) | Q(groups__members=person) + ).values_list("amended_by__pk", flat=True) + return self.filter( + Q(groups__members=person) | Q(pk__in=amended) + ).distinct() + def for_group(self, group: Union[int, Group]) -> "LessonEventQuerySet": """Get all lesson events for a certain group (including amends/as parent group).""" amended = self.filter( diff --git a/aleksis/apps/chronos/models.py b/aleksis/apps/chronos/models.py index 051e6ccecd275aa649535256794e6e06d1359899..d27e4c6a037634afbe1cc43401ab10bbce6203cf 100644 --- a/aleksis/apps/chronos/models.py +++ b/aleksis/apps/chronos/models.py @@ -1559,6 +1559,8 @@ class LessonEvent(CalendarEvent): if type_ and obj_id: if type_ == "TEACHER": return objs.for_teacher(obj_id) + elif type_ == "PARTICIPANT": + return objs.for_participant(obj_id) elif type_ == "GROUP": return objs.for_group(obj_id) elif type_ == "ROOM": diff --git a/aleksis/apps/chronos/util/chronos_helpers.py b/aleksis/apps/chronos/util/chronos_helpers.py index e5d5fe9c340f4a491679bf69d4d3abb358e2787d..8e1640150383c7c00cfb145c74c9b8750c7a0c29 100644 --- a/aleksis/apps/chronos/util/chronos_helpers.py +++ b/aleksis/apps/chronos/util/chronos_helpers.py @@ -35,9 +35,6 @@ def get_el_by_pk( request: HttpRequest, type_: str, pk: int, - year: Optional[int] = None, - week: Optional[int] = None, - regular: Optional[str] = None, prefetch: bool = False, *args, **kwargs,