From d118ed16df060f65c4857e1e9347164975ddc083 Mon Sep 17 00:00:00 2001 From: Hangzhi Yu <hangzhi@protonmail.com> Date: Sat, 24 Feb 2024 02:16:24 +0100 Subject: [PATCH] Reformat, add translations and fix gql queries --- .../frontend/components/AmendLesson.vue | 22 +- .../components/AmendLessonOverview.vue | 206 +++++++++--------- .../frontend/components/AmendedLessonCard.vue | 80 ++++--- .../frontend/components/LessonInformation.vue | 31 ++- .../frontend/components/amendLesson.graphql | 19 +- aleksis/apps/chronos/frontend/index.js | 36 ++- .../apps/chronos/frontend/messages/de.json | 13 ++ .../apps/chronos/frontend/messages/en.json | 13 ++ aleksis/apps/chronos/managers.py | 17 +- aleksis/apps/chronos/model_extensions.py | 4 + aleksis/apps/chronos/models.py | 33 ++- aleksis/apps/chronos/rules.py | 16 +- aleksis/apps/chronos/schema/__init__.py | 76 +++---- aleksis/apps/chronos/util/predicates.py | 32 ++- 14 files changed, 379 insertions(+), 219 deletions(-) diff --git a/aleksis/apps/chronos/frontend/components/AmendLesson.vue b/aleksis/apps/chronos/frontend/components/AmendLesson.vue index 587ccdf0..8565ed6c 100644 --- a/aleksis/apps/chronos/frontend/components/AmendLesson.vue +++ b/aleksis/apps/chronos/frontend/components/AmendLesson.vue @@ -3,12 +3,12 @@ <edit-button i18n-key="chronos.event.amend.edit_button" @click="edit = true" - /> + /> <delete-button v-if="selectedEvent.meta.amended" i18n-key="chronos.event.amend.delete_button" @click="deleteEvent = true" - /> + /> <dialog-object-form v-model="edit" :fields="fields" @@ -23,7 +23,7 @@ :edit-item="initPatchData" @cancel="open = false" @save="updateOnSave()" - > + > <template #subject.field="{ attrs, on, item }"> <v-autocomplete :disabled="item.cancelled" @@ -32,7 +32,7 @@ item-value="id" v-bind="attrs" v-on="on" - /> + /> </template> <template #teachers.field="{ attrs, on, item }"> <v-autocomplete @@ -45,7 +45,7 @@ v-on="on" chips deletable-chips - /> + /> </template> <template #rooms.field="{ attrs, on, item }"> <v-autocomplete @@ -58,7 +58,7 @@ v-on="on" chips deletable-chips - /> + /> </template> <template #cancelled.field="{ attrs, on }"> <v-checkbox v-bind="attrs" v-on="on" /> @@ -73,7 +73,7 @@ v-model="deleteEvent" :item="selectedEvent.meta" @success="updateOnSave()" - > + > <template #title> {{ $t("chronos.event.amend.delete_dialog") }} </template> @@ -109,7 +109,7 @@ export default { selectedEvent: { type: Object, required: true, - } + }, }, data() { return { @@ -165,7 +165,7 @@ export default { }; }, updateOnSave() { - this.$emit('refreshCalendar'); + this.$emit("refreshCalendar"); this.model = false; }, }, @@ -174,7 +174,9 @@ export default { return { id: this.selectedEvent.meta.id, subject: this.selectedEvent.meta.subject?.id.toString(), - teachers: this.selectedEvent.meta.teachers.map((teacher) => teacher.id.toString()), + teachers: this.selectedEvent.meta.teachers.map((teacher) => + teacher.id.toString(), + ), rooms: this.selectedEvent.meta.rooms.map((room) => room.id.toString()), cancelled: this.selectedEvent.meta.cancelled, comment: this.selectedEvent.meta.comment, diff --git a/aleksis/apps/chronos/frontend/components/AmendLessonOverview.vue b/aleksis/apps/chronos/frontend/components/AmendLessonOverview.vue index 15b32e03..1a9e57bd 100644 --- a/aleksis/apps/chronos/frontend/components/AmendLessonOverview.vue +++ b/aleksis/apps/chronos/frontend/components/AmendLessonOverview.vue @@ -6,148 +6,136 @@ import SubjectChip from "aleksis.apps.cursus/components/SubjectChip.vue"; import { DateTime } from "luxon"; -import { amendedLessonsFromAbsences, batchPatchAmendLessons, gqlGroups } from "./amendLesson.graphql"; +import { + amendedLessonsFromAbsences, + batchPatchAmendLessons, + groupsByOwner, +} from "./amendLesson.graphql"; </script> <template> <c-r-u-d-iterator :gql-query="gqlQuery" + :gql-additional-query-args="gqlQueryArgs" :gql-patch-mutation="gqlPatchMutation" :get-patch-data="gqlGetPatchData" - :gql-filters="gqlFilters" - i18n-key="test" - :enable-search="false" - :enable-filter="true" + i18n-key="chronos.amend_lesson.overview" + :enable-search="true" :enable-create="false" :show-create="false" :enable-delete="false" :enable-edit="true" - :headers="headers" :force-model-item-update="true" @lastQuery="lastQuery = $event" > + <template #additionalActions="{ attrs, on }"> + <v-autocomplete + :items="groups" + item-text="name" + clearable + return-object + filled + dense + hide-details + :placeholder="$t('chronos.amend_lesson.overview.filter.groups')" + :loading="$apollo.queries.groups.loading" + :value="currentObj" + @input="changeSelection" + @click:clear="changeSelection" + /> + </template> + <template #default="{ items }"> <v-list-item v-for="day in groupAmendedLessonsByDay(items)" two-line> <v-list-item-content> <v-list-item-title>{{ $d(day[0], "short") }}</v-list-item-title> <v-list> <v-list-item v-for="amendedLesson in day.slice(1)"> - <amended-lesson-card :amended-lesson="amendedLesson" :affected-query="lastQuery" :is-create="false" :gql-patch-mutation="batchPatchAmendLessons" /> + <amended-lesson-card + :amended-lesson="amendedLesson" + :affected-query="lastQuery" + :is-create="false" + :gql-patch-mutation="batchPatchAmendLessons" + /> </v-list-item> </v-list> </v-list-item-content> </v-list-item> </template> - - <!--<template #groups="{ item }">--> - <!-- <lesson-related-object-chip--> - <!-- v-for="group in item.realAmends.groups"--> - <!-- :key="group.id"--> - <!-- >--> - <!-- {{ group.shortName }}--> - <!-- </lesson-related-object-chip--> - <!-- >--> - <!--</template>--> - - <!--<template #teachers.field="{ attrs, on, item }">--> - <!-- <v-autocomplete--> - <!-- :disabled="item.cancelled"--> - <!-- multiple--> - <!-- :items="amendableTeachers"--> - <!-- item-text="fullName"--> - <!-- item-value="id"--> - <!-- v-bind="attrs"--> - <!-- v-on="on"--> - <!-- chips--> - <!-- deletable-chips--> - <!-- />--> - <!--</template>--> - - <template #filters="{ attrs, on }"> - <date-field - v-bind="attrs('date_start')" - v-on="on('date_start')" - :label="$t('start')" - /> - - <date-field - v-bind="attrs('date_end')" - v-on="on('date_end')" - :label="$t('end')" - /> - - <v-autocomplete - v-bind="attrs('group_id')" - v-on="on('group_id')" - :label="$t('group')" - :items="groups" - item-text="shortName" - item-value="id" - /> - </template> </c-r-u-d-iterator> </template> <script> - export default { - props: { +export default { + props: { + objId: { + type: [Number, String], + required: false, + default: null, }, - data() { - return { - gqlQuery: amendedLessonsFromAbsences, - gqlPatchMutation: batchPatchAmendLessons, - gqlFilters: { - group_id: 2, - }, - headers: [ - { - text: "date & time start", - value: "datetimeStart", - disableEdit: true, - }, - { - text: "date & time end", - value: "datetimeEnd", - disableEdit: true, - }, - { - text: "subject", - value: "subject", - disableEdit: true, - }, - { - text: "groups", - value: "groups", - disableEdit: true, - }, - { - text: "teachers", - value: "teachers", - cols: 12, - } - ], - lastQuery: null, - }; + // Next two in ISODate + dateStart: { + type: String, + required: false, + default: "", + }, + dateEnd: { + type: String, + required: false, + default: "", }, - methods: { - groupAmendedLessonsByDay(amendedLessons) { - const byDay = amendedLessons.reduce((byDay, amendedLesson) => { - const day = DateTime.fromISO(amendedLesson.datetimeStart).startOf("day"); - byDay[day] ??= [day]; - byDay[day].push(amendedLesson); - return byDay; - }, {}); + }, + data() { + return { + gqlQuery: amendedLessonsFromAbsences, + gqlPatchMutation: batchPatchAmendLessons, + lastQuery: null, + groups: [], + }; + }, + methods: { + groupAmendedLessonsByDay(amendedLessons) { + const byDay = amendedLessons.reduce((byDay, amendedLesson) => { + const day = DateTime.fromISO(amendedLesson.datetimeStart).startOf( + "day", + ); + byDay[day] ??= [day]; + byDay[day].push(amendedLesson); + return byDay; + }, {}); - return Object.keys(byDay) + return Object.keys(byDay) .sort() .map((key) => byDay[key]); - }, - gqlGetPatchData(item) { - return { id: item.id, teachers: item.teachers } - }, }, - apollo: { - groups: gqlGroups, - } - }; + gqlGetPatchData(item) { + return { id: item.id, teachers: item.teachers }; + }, + changeSelection(selection) { + this.$router.push({ + name: "chronos.amendLessonOvervievByTypeAndDate", + params: { + objId: selection.id, + dateStart: this.dateStart, + dateEnd: this.dateEnd, + }, + }); + }, + }, + computed: { + gqlQueryArgs() { + return { + objId: this.objId ? Number(this.objId) : null, + dateStart: this.dateStart, + dateEnd: this.dateEnd, + }; + }, + currentObj() { + return this.groups.find((o) => o.id === this.objId); + }, + }, + apollo: { + groups: groupsByOwner, + }, +}; </script> diff --git a/aleksis/apps/chronos/frontend/components/AmendedLessonCard.vue b/aleksis/apps/chronos/frontend/components/AmendedLessonCard.vue index a29a1838..982515a8 100644 --- a/aleksis/apps/chronos/frontend/components/AmendedLessonCard.vue +++ b/aleksis/apps/chronos/frontend/components/AmendedLessonCard.vue @@ -12,12 +12,14 @@ import createOrPatchMixin from "aleksis.core/mixins/createOrPatchMixin.js"; <v-card class="my-2 full-width"> <!-- flex-md-row zeile ab medium --> <!-- align-stretch - stretch full-width --> - <div class="full-width d-flex flex-md-row flex-column align-center justify-space-between"> + <div + class="full-width d-flex flex-md-row flex-column align-center justify-space-between" + > <lesson-information class="flex-grow-1" :lesson="$attrs['amended-lesson']" /> - + <v-autocomplete v-model="teachers" multiple @@ -30,13 +32,29 @@ import createOrPatchMixin from "aleksis.core/mixins/createOrPatchMixin.js"; @input="save" > <template #prepend-inner> - <v-chip v-for="teacher in teachersWithStatus($attrs['amended-lesson']).filter((t) => t.status === 'removed')" class="text-decoration-line-through text--secondary mb-2">{{ teacher.fullName }}</v-chip> + <v-chip + v-for="teacher in teachersWithStatus( + $attrs['amended-lesson'], + ).filter((t) => t.status === 'removed')" + class="text-decoration-line-through text--secondary mb-2" + >{{ teacher.fullName }}</v-chip + > </template> </v-autocomplete> - - <delete-button class="flex-grow-1 mx-2" color="red white--text" @click="toggleCancel">{{ $attrs['amended-lesson'].cancelled ? "de-cancel" : "cancel" }}</delete-button> + + <delete-button + class="flex-grow-1 mx-2" + color="red white--text" + outlined + @click="toggleCancel" + >{{ + $attrs["amended-lesson"].cancelled + ? $t("chronos.amend_lesson.overview.cancel.decancel") + : $t("chronos.amend_lesson.overview.cancel.cancel") + }}</delete-button + > </div> - <v-divider/> + <v-divider /> <!--<v-card-actions>--> <!-- <v-spacer/>--> <!-- <cancel-button @click="$emit('close')" :disabled="loading" />--> @@ -69,38 +87,46 @@ export default { teachersWithStatus(lesson) { let oldIds = lesson.realAmends.teachers.map((teacher) => teacher.id); let newIds = lesson.teachers.map((teacher) => teacher.id); - let teachersWithStatus = lesson.realAmends.teachers.concat(lesson.teachers).map((teacher) => { - let status = "regular"; - if (newIds.includes(teacher.id) && !oldIds.includes(teacher.id)) { - status = "new"; - } else if ( - !newIds.includes(teacher.id) && - oldIds.includes(teacher.id) - ) { - status = "removed"; - } - return { ...teacher, status: status }; - }); + let teachersWithStatus = lesson.realAmends.teachers + .concat(lesson.teachers) + .map((teacher) => { + let status = "regular"; + if (newIds.includes(teacher.id) && !oldIds.includes(teacher.id)) { + status = "new"; + } else if ( + !newIds.includes(teacher.id) && + oldIds.includes(teacher.id) + ) { + status = "removed"; + } + return { ...teacher, status: status }; + }); return teachersWithStatus; }, save() { - this.createOrPatch([{ - id: this.$attrs["amended-lesson"].id, - teachers: this.teachers, - }]); + this.createOrPatch([ + { + id: this.$attrs["amended-lesson"].id, + teachers: this.teachers, + }, + ]); }, toggleCancel() { - this.createOrPatch([{ - id: this.$attrs["amended-lesson"].id, - cancelled: !this.$attrs["amended-lesson"].cancelled, - }]); + this.createOrPatch([ + { + id: this.$attrs["amended-lesson"].id, + cancelled: !this.$attrs["amended-lesson"].cancelled, + }, + ]); }, }, apollo: { amendableTeachers: gqlPersons, }, mounted() { - this.teachers = this.$attrs["amended-lesson"].teachers.map((teacher) => teacher.id); + this.teachers = this.$attrs["amended-lesson"].teachers.map( + (teacher) => teacher.id, + ); }, }; </script> diff --git a/aleksis/apps/chronos/frontend/components/LessonInformation.vue b/aleksis/apps/chronos/frontend/components/LessonInformation.vue index a43ab8b9..e89e58af 100644 --- a/aleksis/apps/chronos/frontend/components/LessonInformation.vue +++ b/aleksis/apps/chronos/frontend/components/LessonInformation.vue @@ -7,19 +7,18 @@ import { DateTime } from "luxon"; <template> <v-card-text> - <cancelled-calendar-status-chip - v-if="lesson.cancelled" - class="mr-2" - /> - <div :class="{ 'text-decoration-line-through': lesson.cancelled, 'text--secondary': lesson.cancelled }"> + <cancelled-calendar-status-chip v-if="lesson.cancelled" class="mr-2" /> + <div + :class="{ + 'text-decoration-line-through': lesson.cancelled, + 'text--secondary': lesson.cancelled, + }" + > {{ $d(toDateTime(lesson.datetimeStart), "shortTime") }} – {{ $d(toDateTime(lesson.datetimeEnd), "shortTime") }} {{ getCourse(lesson)?.name }} </div> - <subject-chip - v-if="getSubject(lesson)" - :subject="getSubject(lesson)" - /> + <subject-chip v-if="getSubject(lesson)" :subject="getSubject(lesson)" /> </v-card-text> </template> @@ -42,10 +41,20 @@ export default { return DateTime.fromISO(dateString); }, getSubject(lesson) { - return lesson.subject ? lesson.subject : lesson.course?.subject ? lesson.course.subject : lesson.realAmends?.subject ? lesson.realAmends.subject : undefined; + return lesson.subject + ? lesson.subject + : lesson.course?.subject + ? lesson.course.subject + : lesson.realAmends?.subject + ? lesson.realAmends.subject + : undefined; }, getCourse(lesson) { - return lesson.course ? lesson.course : lesson.realAmends?.course ? lesson.realAmends.course : undefined; + return lesson.course + ? lesson.course + : lesson.realAmends?.course + ? lesson.realAmends.course + : undefined; }, }, }; diff --git a/aleksis/apps/chronos/frontend/components/amendLesson.graphql b/aleksis/apps/chronos/frontend/components/amendLesson.graphql index 1868fca4..09312c24 100644 --- a/aleksis/apps/chronos/frontend/components/amendLesson.graphql +++ b/aleksis/apps/chronos/frontend/components/amendLesson.graphql @@ -27,6 +27,13 @@ query gqlGroups { } } +query groupsByOwner { + groups: groupsByOwner { + id + name + } +} + mutation createAmendLesson($input: CreateLessonEventInput!) { createAmendLesson(input: $input) { lessonEvent { @@ -145,8 +152,16 @@ mutation deleteAmendLesson($id: ID!) { } } -query amendedLessonsFromAbsences($filters: JSONString!) { - items: amendedLessonsFromAbsences(filters: $filters) { +query amendedLessonsFromAbsences( + $objId: ID + $dateStart: Date! + $dateEnd: Date! +) { + items: amendedLessonsFromAbsences( + objId: $objId + dateStart: $dateStart + dateEnd: $dateEnd + ) { id subject { id diff --git a/aleksis/apps/chronos/frontend/index.js b/aleksis/apps/chronos/frontend/index.js index 494a0e90..9bcdf971 100644 --- a/aleksis/apps/chronos/frontend/index.js +++ b/aleksis/apps/chronos/frontend/index.js @@ -1,6 +1,8 @@ import { hasPersonValidator } from "aleksis.core/routeValidators"; import Timetable from "./components/Timetable.vue"; -import AmendLessonOverview from "./components/AmendLessonOverview.vue" +import AmendLessonOverview from "./components/AmendLessonOverview.vue"; + +import { DateTime } from "luxon"; export default { meta: { @@ -34,13 +36,39 @@ export default { }, { path: "amend_lesson_overview/", - component: AmendLessonOverview, + component: () => import("./components/AmendLessonOverview.vue"), + redirect: (to) => { + return { + name: "chronos.amendLessonOvervievByTypeAndDate", + params: { + dateStart: DateTime.now().toISODate(), + dateEnd: DateTime.now().plus({ weeks: 1 }).toISODate(), + }, + }; + }, name: "chronos.amendLessonOverview", + props: true, meta: { inMenu: true, - titleKey: "chronos.amendLessonOverview.menu_title", - icon: "mdi-grid", + icon: "mdi-account-convert-outline", + iconActive: "mdi-account-convert", + titleKey: "chronos.amend_lesson.overview.menu_title", + toolbarTitle: "chronos.amend_lesson.overview.menu_title", + permission: "chronos.view_substitution_overview_rule", }, + children: [ + { + path: ":dateStart(\\d\\d\\d\\d-\\d\\d-\\d\\d)/:dateEnd(\\d\\d\\d\\d-\\d\\d-\\d\\d)/:objId(\\d+)?/", + component: () => import("./components/AmendLessonOverview.vue"), + name: "chronos.amendLessonOvervievByTypeAndDate", + meta: { + titleKey: "chronos.amend_lesson.overview.menu_title", + toolbarTitle: "chronos.amend_lesson.overview.menu_title", + permission: "chronos.view_substitution_overview_rule", + fullWidth: true, + }, + }, + ], }, ], }; diff --git a/aleksis/apps/chronos/frontend/messages/de.json b/aleksis/apps/chronos/frontend/messages/de.json index 86148a7b..cd6011a3 100644 --- a/aleksis/apps/chronos/frontend/messages/de.json +++ b/aleksis/apps/chronos/frontend/messages/de.json @@ -32,6 +32,19 @@ "no_teacher": "Keine Lehrkraft", "no_room": "Kein Raum", "current_changes": "Aktuelle Änderungen" + }, + "amend_lesson": { + "overview": { + "menu_title": "Vertretungsstunden planen", + "title_plural": "Vertretungsstunden planen", + "filter": { + "groups": "Nach Gruppen filtern" + }, + "cancel": { + "cancel": "Stunde ausfallen lassen", + "decancel": "Stunde nicht ausfallen lassen" + } + } } } } diff --git a/aleksis/apps/chronos/frontend/messages/en.json b/aleksis/apps/chronos/frontend/messages/en.json index ee23c28f..0b5c7fa8 100644 --- a/aleksis/apps/chronos/frontend/messages/en.json +++ b/aleksis/apps/chronos/frontend/messages/en.json @@ -44,6 +44,19 @@ "cancelled": "Cancelled", "comment": "Comment" } + }, + "amend_lesson": { + "overview": { + "menu_title": "Amend lessons", + "title_plural": "Amend lessons", + "filter": { + "groups": "Filter by groups" + }, + "cancel": { + "cancel": "Mark lesson as cancelled", + "decancel": "Mark lesson as not cancelled" + } + } } } } diff --git a/aleksis/apps/chronos/managers.py b/aleksis/apps/chronos/managers.py index 12c52170..391ffb5c 100644 --- a/aleksis/apps/chronos/managers.py +++ b/aleksis/apps/chronos/managers.py @@ -905,17 +905,22 @@ class LessonEventQuerySet(PolymorphicQuerySet): def affected_by_absences(self, datetime_start: datetime, datetime_end: datetime): return self.filter( - ((Q(teachers__kolego_absences__datetime_start__gte=datetime_start) - & Q(teachers__kolego_absences__datetime_start__lte=datetime_end)) - | (Q(teachers__kolego_absences__datetime_end__gte=datetime_start) - & Q(teachers__kolego_absences__datetime_end__lte=datetime_end)) - ) + ( + ( + Q(teachers__kolego_absences__datetime_start__gte=datetime_start) + & Q(teachers__kolego_absences__datetime_start__lte=datetime_end) + ) + | ( + Q(teachers__kolego_absences__datetime_end__gte=datetime_start) + & Q(teachers__kolego_absences__datetime_end__lte=datetime_end) + ) + ) & Q(teachers__kolego_absences__datetime_start__lte=F("datetime_end")) & Q(teachers__kolego_absences__datetime_end__gte=F("datetime_start")) & Q(amends__isnull=True) & Q(amended_by__isnull=True) ) - + def related_to_person(self, person: Union[int, Person]): amended = self.filter( Q(amended_by__isnull=False) diff --git a/aleksis/apps/chronos/model_extensions.py b/aleksis/apps/chronos/model_extensions.py index 8f43350e..ff8e745d 100644 --- a/aleksis/apps/chronos/model_extensions.py +++ b/aleksis/apps/chronos/model_extensions.py @@ -144,6 +144,10 @@ Group.add_permission( "view_group_timetable", _("Can view group timetable"), ) +Group.add_permission( + "manage_group_substitutions", + _("Can manage group substitutions"), +) Person.add_permission( "view_person_timetable", _("Can view person timetable"), diff --git a/aleksis/apps/chronos/models.py b/aleksis/apps/chronos/models.py index 0ee5cfc9..09353bbc 100644 --- a/aleksis/apps/chronos/models.py +++ b/aleksis/apps/chronos/models.py @@ -5,7 +5,7 @@ import itertools from collections.abc import Iterable, Iterator from datetime import date, datetime, time, timedelta from itertools import chain -from typing import Any +from typing import Any, Optional from django.contrib.contenttypes.models import ContentType from django.core.exceptions import PermissionDenied, ValidationError @@ -1581,15 +1581,36 @@ class LessonEvent(CalendarEvent): return objs.for_person(request.user.person) @classmethod - def get_for_substitution_overview(cls, obj_type: str, obj_id: str, date_start: datetime, date_end: datetime, request: HttpRequest) -> list: + def get_for_substitution_overview( + cls, + date_start: datetime, + date_end: datetime, + request: HttpRequest, + obj_type: Optional[str], + obj_id: Optional[str], + ) -> list: """Get all the amended lessons for an object and a time frame. obj_type may be one of TEACHER, GROUP, ROOM, COURSE """ # 1. Find all LessonEvents for all Lessons of this Group in this date range and which are not themselves amending another lessonEvent + event_params = { + "own": False, + "not_amending": True, + "prefetch_absences": True, + } + if obj_type is not None and obj_id is not None: + event_params.update( + { + "type": obj_type, + "id": obj_id, + } + ) - events = LessonEvent.get_single_events(date_start, date_end, request, {"type": obj_type, "id": obj_id, "not_amending": True, "prefetch_absences": True}, with_reference_object=True) + events = LessonEvent.get_single_events( + date_start, date_end, request, event_params, with_reference_object=True + ) # (1.5 filter them by permissions) ... @@ -1601,8 +1622,10 @@ class LessonEvent(CalendarEvent): for event in events: reference_obj = event["REFERENCE_OBJECT"] - affected_teachers = reference_obj.teachers.filter(Q(kolego_absences__datetime_start__lte=event["DTEND"].dt) - & Q(kolego_absences__datetime_end__gte=event["DTSTART"].dt)) + affected_teachers = reference_obj.teachers.filter( + Q(kolego_absences__datetime_start__lte=event["DTEND"].dt) + & Q(kolego_absences__datetime_end__gte=event["DTSTART"].dt) + ) if affected_teachers.exists(): obj, created = LessonEvent.objects.update_or_create( diff --git a/aleksis/apps/chronos/rules.py b/aleksis/apps/chronos/rules.py index 85a0d1c4..03ef88c2 100644 --- a/aleksis/apps/chronos/rules.py +++ b/aleksis/apps/chronos/rules.py @@ -8,7 +8,13 @@ from aleksis.core.util.predicates import ( ) from .models import LessonSubstitution -from .util.predicates import has_any_timetable_object, has_room_timetable_perm, has_timetable_perm +from .util.predicates import ( + has_any_group_substitution_perm, + has_any_timetable_object, + has_group_substitution_perm, + has_room_timetable_perm, + has_timetable_perm, +) # View timetable overview view_timetable_overview_predicate = has_person & ( @@ -27,6 +33,14 @@ add_perm("chronos.view_timetable_rule", view_timetable_predicate) view_lessons_day_predicate = has_person & has_global_perm("chronos.view_lessons_day") add_perm("chronos.view_lessons_day_rule", view_lessons_day_predicate) +# View substitution management overview page +view_substitution_overview_predicate = has_person & has_any_group_substitution_perm +add_perm("chronos.view_substitution_overview_rule", view_substitution_overview_predicate) + +# Manage substitutions for a group +manage_substitutions_for_group_predicate = has_person & has_group_substitution_perm +add_perm("chronos.manage_substitutions_for_group_rule", manage_substitutions_for_group_predicate) + # Edit substition edit_substitution_predicate = has_person & ( has_global_perm("chronos.change_lessonsubstitution") diff --git a/aleksis/apps/chronos/schema/__init__.py b/aleksis/apps/chronos/schema/__init__.py index d4251761..76963150 100644 --- a/aleksis/apps/chronos/schema/__init__.py +++ b/aleksis/apps/chronos/schema/__init__.py @@ -2,14 +2,19 @@ from datetime import date, datetime, timezone from functools import reduce from operator import and_ +from django.core.exceptions import PermissionDenied from django.db.models import F, ManyToManyField, OuterRef, Subquery, Q, Prefetch import graphene from graphene_django import DjangoObjectType -from graphene_django_cud.mutations import DjangoBatchPatchMutation, DjangoCreateMutation, DjangoPatchMutation +from graphene_django_cud.mutations import ( + DjangoBatchPatchMutation, + DjangoCreateMutation, + DjangoPatchMutation, +) from aleksis.core.models import CalendarEvent, Group, Person, Room -from aleksis.core.schema.base import DeleteMutation +from aleksis.core.schema.base import DeleteMutation, FilterOrderList from aleksis.apps.kolego.models import Absence @@ -63,26 +68,11 @@ class LessonEventType(DjangoObjectType): "cancelled", "comment", ) + filter_fields = { + "id": ["exact", "lte", "gte"], + } - -class LessonEventTypeWithRealAmends(DjangoObjectType): - class Meta: - model = LessonEvent - fields = ( - "id", - "amends", - "datetime_start", - "datetime_end", - "subject", - "teachers", - "groups", - "rooms", - "course", - "cancelled", - "comment", - ) - - real_amends = graphene.Field(LessonEventType, required=False) + real_amends = graphene.Field(lambda: LessonEventType, required=False) class DatetimeTimezoneMixin: @@ -112,7 +102,7 @@ class DatetimeTimezoneMixin: if patch_obj: obj = patch_obj - + obj.timezone = obj.amends.timezone return obj @@ -173,24 +163,18 @@ class TimetableObjectType(graphene.ObjectType): return f"{root.type.value}-{root.id}" -class LessonEventType(DjangoObjectType): - class Meta: - model = LessonEvent - fields = ("id",) - filter_fields = { - "id": ["exact", "lte", "gte"], - } - - class Query(graphene.ObjectType): timetable_teachers = graphene.List(TimetablePersonType) timetable_groups = graphene.List(TimetableGroupType) timetable_rooms = graphene.List(TimetableRoomType) available_timetables = graphene.List(TimetableObjectType) - amended_lessons_from_absences = graphene.List( - LessonEventTypeWithRealAmends, - filters=graphene.JSONString(required=True), + amended_lessons_from_absences = FilterOrderList( + LessonEventType, + obj_type=graphene.String(required=False), + obj_id=graphene.ID(required=False), + date_start=graphene.Date(required=True), + date_end=graphene.Date(required=True), ) def resolve_timetable_teachers(self, info, **kwargs): @@ -234,17 +218,23 @@ class Query(graphene.ObjectType): return all_timetables - def resolve_amended_lessons_from_absences(root, info, filters, **kwargs): - if isinstance(filters, str): - filters = json.loads(filters) - - datetime_start = datetime.combine(date.fromisoformat(filters.get("date_start", datetime.now().date().isoformat())) , datetime.min.time()) - datetime_end = datetime.combine(date.fromisoformat(filters.get("date_end", datetime.now().date().isoformat())), datetime.max.time()) + def resolve_amended_lessons_from_absences( + root, info, date_start, date_end, obj_type="GROUP", obj_id=None, **kwargs + ): + datetime_start = datetime.combine(date_start, datetime.min.time()) + datetime_end = datetime.combine(date_end, datetime.max.time()) - group_id = filters.get("group_id") + if ( + obj_id + and not info.context.user.has_perm( + "chronos.manage_substitutions_for_group_rule", Group.objects.get(id=obj_id) + ) + ) or (not obj_id and not info.context.user.has_perm("chronos.view_substitution_overview_rule")): + raise PermissionDenied() - # TODO: later on, allow getting amended lessons for other types than courses, e.g. groups or persons - return LessonEvent.get_for_substitution_overview("GROUP", group_id, datetime_start, datetime_end, info.context) + return LessonEvent.get_for_substitution_overview( + datetime_start, datetime_end, info.context, obj_type, obj_id + ) class Mutation(graphene.ObjectType): diff --git a/aleksis/apps/chronos/util/predicates.py b/aleksis/apps/chronos/util/predicates.py index d99ac20a..bef69857 100644 --- a/aleksis/apps/chronos/util/predicates.py +++ b/aleksis/apps/chronos/util/predicates.py @@ -4,7 +4,7 @@ from django.db.models import Model from rules import predicate from aleksis.core.models import Group, Person, Room -from aleksis.core.util.predicates import has_global_perm, has_object_perm +from aleksis.core.util.predicates import has_any_object, has_global_perm, has_object_perm from .chronos_helpers import get_classes, get_rooms, get_teachers @@ -44,6 +44,36 @@ def has_group_timetable_perm(user: User, obj: Group) -> bool: ) +@predicate +def has_group_substitution_perm(user: User, obj: Group) -> bool: + """ + Check if can access/edit group substitutions. + + Predicate which checks whether the user is allowed + to access/edit the requested group substitutions. + """ + return ( + obj in user.person.owner_of.all() + or has_global_perm("chronos.view_lessonsubstitution")(user) + or has_object_perm("core.manage_group_substitutions")(user, obj) + ) + + +@predicate +def has_any_group_substitution_perm(user: User) -> bool: + """ + Check if can access/edit any group substitutions. + + Predicate which checks whether the user is allowed + to access/edit any group substitutions. + """ + return ( + user.person.owner_of.exists() + or has_global_perm("chronos.view_lessonsubstitution")(user) + or has_any_object("core.manage_group_substitutions")(user, Group) + ) + + @predicate def has_person_timetable_perm(user: User, obj: Person) -> bool: """ -- GitLab