diff --git a/aleksis/apps/chronos/frontend/components/AmendLesson.vue b/aleksis/apps/chronos/frontend/components/AmendLesson.vue new file mode 100644 index 0000000000000000000000000000000000000000..f43faa3a31d0b4d44a8459a63a0b99ae68228743 --- /dev/null +++ b/aleksis/apps/chronos/frontend/components/AmendLesson.vue @@ -0,0 +1,187 @@ +<template> + <v-card-actions v-if="checkPermission('chronos.edit_substitution_rule')"> + <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="delete = true" + /> + <dialog-object-form + v-model="edit" + :fields="fields" + :is-create="!selectedEvent.meta.amended" + createItemI18nKey="chronos.event.amend.title" + :gql-create-mutation="gqlCreateMutation" + :get-create-data="transformCreateData" + :default-item="default" + editItemI18nKey="chronos.event.amend.title" + :gql-patch-mutation="gqlPatchMutation" + :get-patch-data="transformPatchData" + :edit-item="initPatchData" + @cancel="open = false" + @save="updateOnSave()" + > + <template #subject.field="{ attrs, on, item }"> + <v-autocomplete + :disabled="item.cancelled" + :items="amendableSubjects" + item-text="name" + item-value="id" + v-bind="attrs" + v-on="on" + /> + </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 #rooms.field="{ attrs, on, item }"> + <v-autocomplete + :disabled="item.cancelled" + multiple + :items="amendableRooms" + item-text="name" + item-value="id" + v-bind="attrs" + v-on="on" + chips + deletable-chips + /> + </template> + <template #cancelled.field="{ attrs, on }"> + <v-checkbox v-bind="attrs" v-on="on" /> + </template> + <template #comment.field="{ attrs, on }"> + <v-textarea v-bind="attrs" v-on="on" /> + </template> + </dialog-object-form> + <delete-dialog + deleteSuccessMessageI18nKey="chronos.event.amend.delete_success" + :gql-mutation="gqlDeleteMutation" + v-model="delete" + :item="selectedEvent.meta" + @success="updateOnSave()" + > + <template #title> + {{ $t("chronos.event.amend.delete_dialog") }} + </template> + </delete-dialog> + </v-card-actions> +</template> + +<script> +import permissionsMixin from "aleksis.core/mixins/permissions.js"; +import EditButton from "aleksis.core/components/generic/buttons/EditButton.vue"; +import DialogObjectForm from "aleksis.core/components/generic/dialogs/DialogObjectForm.vue"; +import DeleteButton from "aleksis.core/components/generic/buttons/DeleteButton.vue"; +import DeleteDialog from "aleksis.core/components/generic/dialogs/DeleteDialog.vue"; +import { + gqlSubjects, + gqlPersons, + gqlRooms, + createAmendLesson, + patchAmendLesson, + deleteAmendLesson, +} from "./amendLesson.graphql"; + +export default { + name: "AmendLesson", + components: { + EditButton, + DialogObjectForm, + DeleteButton, + DeleteDialog, + }, + mixins: [permissionsMixin], + data() { + return { + edit: false, + fields: [ + { + text: this.$t("chronos.event.amend.subject"), + value: "subject", + }, + { + text: this.$t("chronos.event.amend.teachers"), + value: "teachers", + }, + { + text: this.$t("chronos.event.amend.rooms"), + value: "rooms", + }, + { + text: this.$t("chronos.event.amend.cancelled"), + value: "cancelled", + }, + { + text: this.$t("chronos.event.amend.comment"), + value: "comment", + }, + ], + default: { + cancelled: this.selectedEvent.meta.cancelled, + comment: this.selectedEvent.meta.comment, + }, + gqlCreateMutation: createAmendLesson, + gqlPatchMutation: patchAmendLesson, + delete: false, + gqlDeleteMutation: deleteAmendLesson, + }; + }, + methods: { + transformCreateData(item) { + return { + ...item, + amends: this.selectedEvent.meta.id, + datetimeStart: this.selectedEvent.startDateTime.toUTC().toISO(), + datetimeEnd: this.selectedEvent.endDateTime.toUTC().toISO(), + }; + }, + transformPatchData(item) { + let { id, __typename, cancelled, ...patchItem } = item; + return { + ...patchItem, + // Normalize cancelled, v-checkbox returns null & does not + // honor false-value. + cancelled: cancelled ? true : false, + }; + }, + updateOnSave() { + this.$emit('refreshCalendar'); + this.model = false; + }, + }, + computed: { + initPatchData() { + return { + id: this.selectedEvent.meta.id, + subject: this.selectedEvent.meta.subject?.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, + }; + }, + }, + apollo: { + amendableSubjects: gqlSubjects, + amendableTeachers: gqlPersons, + amendableRooms: gqlRooms, + }, + mounted() { + this.addPermissions(["chronos.edit_substitution_rule"]); + }, +}; +</script> diff --git a/aleksis/apps/chronos/frontend/components/Timetable.vue b/aleksis/apps/chronos/frontend/components/Timetable.vue index 6cd8e875adf52c96bc8062c4e1ed0001480ef16b..d685684be91e9e63b7c1e2dc0e991ee7b098b7cf 100644 --- a/aleksis/apps/chronos/frontend/components/Timetable.vue +++ b/aleksis/apps/chronos/frontend/components/Timetable.vue @@ -10,6 +10,21 @@ export default { 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() { @@ -29,7 +44,7 @@ export default { this.$router.push({ name: "chronos.timetable" }); } else if ( selected.objId !== this.$route.params.id || - selected.type !== this.$route.params.type + selected.type.toLowerCase() !== this.$route.params.type ) { this.$router.push({ name: "chronos.timetableWithId", diff --git a/aleksis/apps/chronos/frontend/components/amendLesson.graphql b/aleksis/apps/chronos/frontend/components/amendLesson.graphql index bf6d16e4820b1f31da4b33a28660ad7036287d33..a93e89186df05efdfa77473b7f1a1751eb614c24 100644 --- a/aleksis/apps/chronos/frontend/components/amendLesson.graphql +++ b/aleksis/apps/chronos/frontend/components/amendLesson.graphql @@ -67,3 +67,9 @@ mutation patchAmendLesson($input: PatchLessonEventInput!, $id: ID!) { } } } + +mutation deleteAmendLesson($id: ID!) { + deleteAmendLesson(id: $id) { + ok + } +} diff --git a/aleksis/apps/chronos/frontend/components/calendar_feeds/details/LessonDetails.vue b/aleksis/apps/chronos/frontend/components/calendar_feeds/details/LessonDetails.vue index 2ef03c095e4e46a10fc52c9cc67a0e126a3a1d2e..c8df491e7f6c182785f91103f9c08c6382788a7e 100644 --- a/aleksis/apps/chronos/frontend/components/calendar_feeds/details/LessonDetails.vue +++ b/aleksis/apps/chronos/frontend/components/calendar_feeds/details/LessonDetails.vue @@ -2,7 +2,8 @@ <base-calendar-feed-details v-bind="$props" :color="currentSubject ? currentSubject.colour_bg : null" - > + without-location + > <template #title> <div :style="{ @@ -95,94 +96,23 @@ </v-list-item-title> </v-list-item-content> </v-list-item> - <v-card-actions v-if="checkPermission('chronos.edit_substitution_rule')"> - <edit-button - i18n-key="chronos.event.amend.button" - @click="amendEvent.open = true" - /> - <dialog-object-form - v-model="amendEvent.open" - :fields="amendEvent.fields" - :is-create="!selectedEvent.meta.amended" - createItemI18nKey="chronos.event.amend.title" - :gql-create-mutation="amendEvent.gqlCreateMutation" - :get-create-data="transformCreateData" - :default-item="amendEvent.default" - editItemI18nKey="chronos.event.amend.title" - :gql-patch-mutation="amendEvent.gqlPatchMutation" - :get-patch-data="transformPatchData" - :edit-item="initPatchData" - @cancel="amendEvent.open = false" - @save="onAmendSave()" - > - <template #subject.field="{ attrs, on, item }"> - <v-autocomplete - :disabled="item.cancelled" - :items="amendableSubjects" - item-text="name" - item-value="id" - v-bind="attrs" - v-on="on" - /> - </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 #rooms.field="{ attrs, on, item }"> - <v-autocomplete - :disabled="item.cancelled" - multiple - :items="amendableRooms" - item-text="name" - item-value="id" - v-bind="attrs" - v-on="on" - chips - deletable-chips - /> - </template> - <template #cancelled.field="{ attrs, on }"> - <v-checkbox v-bind="attrs" v-on="on" /> - </template> - <template #comment.field="{ attrs, on }"> - <v-textarea v-bind="attrs" v-on="on" /> - </template> - </dialog-object-form> - </v-card-actions> + <amend-lesson /> </template> </base-calendar-feed-details> </template> <script> -import permissionsMixin from "aleksis.core/mixins/permissions.js"; import calendarFeedDetailsMixin from "aleksis.core/mixins/calendarFeedDetails.js"; import BaseCalendarFeedDetails from "aleksis.core/components/calendar/BaseCalendarFeedDetails.vue"; import CalendarStatusChip from "aleksis.core/components/calendar/CalendarStatusChip.vue"; import CancelledCalendarStatusChip from "aleksis.core/components/calendar/CancelledCalendarStatusChip.vue"; -import EditButton from "aleksis.core/components/generic/buttons/EditButton.vue"; -import DialogObjectForm from "aleksis.core/components/generic/dialogs/DialogObjectForm.vue"; import LessonRelatedObjectChip from "../../LessonRelatedObjectChip.vue"; import lessonEvent from "../mixins/lessonEvent"; import LessonEventSubject from "../../LessonEventSubject.vue"; -import { - gqlSubjects, - gqlPersons, - gqlRooms, - createAmendLesson, - patchAmendLesson, -} from "../../amendLesson.graphql"; + +import AmendLesson from "../../AmendLesson.vue"; export default { name: "LessonDetails", @@ -192,88 +122,8 @@ export default { BaseCalendarFeedDetails, CalendarStatusChip, CancelledCalendarStatusChip, - EditButton, - DialogObjectForm, - }, - mixins: [permissionsMixin, calendarFeedDetailsMixin, lessonEvent], - data() { - return { - amendEvent: { - open: false, - fields: [ - { - text: this.$t("chronos.event.amend.subject"), - value: "subject", - }, - { - text: this.$t("chronos.event.amend.teachers"), - value: "teachers", - }, - { - text: this.$t("chronos.event.amend.rooms"), - value: "rooms", - }, - { - text: this.$t("chronos.event.amend.cancelled"), - value: "cancelled", - }, - { - text: this.$t("chronos.event.amend.comment"), - value: "comment", - }, - ], - default: { - cancelled: this.selectedEvent.meta.cancelled, - comment: this.selectedEvent.meta.comment, - }, - gqlCreateMutation: createAmendLesson, - gqlPatchMutation: patchAmendLesson, - }, - }; - }, - methods: { - transformCreateData(item) { - return { - ...item, - amends: this.selectedEvent.meta.id, - // LessonEvent has datetime in UTC & graphql does not like the Z timezone info - datetimeStart: this.selectedEvent.start.toISOString().replace("Z", ""), - datetimeEnd: this.selectedEvent.end.toISOString().replace("Z", ""), - }; - }, - transformPatchData(item) { - let { id, __typename, cancelled, ...patchItem } = item; - return { - ...patchItem, - // Normalize cancelled, v-checkbox returns null & does not - // honor false-value. - cancelled: cancelled ? true : false, - }; - }, - onAmendSave() { - this.$emit('refreshCalendar'); - this.model = false; - }, - }, - computed: { - initPatchData() { - return { - id: this.selectedEvent.meta.id, - subject: this.selectedEvent.meta.subject?.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, - }; - }, - }, - apollo: { - amendableSubjects: gqlSubjects, - amendableTeachers: gqlPersons, - amendableRooms: gqlRooms, - }, - mounted() { - this.addPermissions(["chronos.edit_substitution_rule"]); + AmendLesson, }, + mixins: [calendarFeedDetailsMixin, lessonEvent], }; </script> diff --git a/aleksis/apps/chronos/frontend/index.js b/aleksis/apps/chronos/frontend/index.js index 3727ed27f35735198c26699771cf17809093a8b6..30451f93c2f0dfcb769a4ab92385b50fe1bfe61c 100644 --- a/aleksis/apps/chronos/frontend/index.js +++ b/aleksis/apps/chronos/frontend/index.js @@ -20,13 +20,16 @@ export default { inMenu: true, titleKey: "chronos.timetable.menu_title", icon: "mdi-grid", - permission: "chronos.view_timetables_rule", + permission: "chronos.view_timetable_overview_rule", }, }, { path: "timetable/:type/:id/", component: Timetable, name: "chronos.timetableWithId", + meta: { + permission: "chronos.view_timetable_overview_rule", + }, }, ], }; diff --git a/aleksis/apps/chronos/frontend/messages/en.json b/aleksis/apps/chronos/frontend/messages/en.json index 90f35ab79ad1d6a5173b866a17b350dff9a68a29..b995b749a1677cdfbdbfef9888f4bd427062717c 100644 --- a/aleksis/apps/chronos/frontend/messages/en.json +++ b/aleksis/apps/chronos/frontend/messages/en.json @@ -28,7 +28,10 @@ "no_room": "No room", "current_changes": "Current changes", "amend": { - "button": "Change", + "edit_button": "Change", + "delete_button": "Reset", + "delete_dialog": " Are you sure you want to delete this substitution?", + "delete_success": "The substitution was deleted successfully.", "title": "Change lesson", "subject": "Subject", "teachers": "Teachers", diff --git a/aleksis/apps/chronos/menus.py b/aleksis/apps/chronos/menus.py deleted file mode 100644 index 810e88c6d75a4cefb95210234fb70d690bb32ea1..0000000000000000000000000000000000000000 --- a/aleksis/apps/chronos/menus.py +++ /dev/null @@ -1,73 +0,0 @@ -from django.utils.translation import gettext_lazy as _ - -MENUS = { - "NAV_MENU_CORE": [ - { - "name": _("Timetables"), - "url": "#", - "svg_icon": "mdi:school-outline", - "root": True, - "validators": [ - "menu_generator.validators.is_authenticated", - "aleksis.core.util.core_helpers.has_person", - ], - "submenu": [ - { - "name": _("My timetable"), - "url": "my_timetable", - "svg_icon": "mdi:account-outline", - "validators": [ - ( - "aleksis.core.util.predicates.permission_validator", - "chronos.view_my_timetable_rule", - ), - ], - }, - { - "name": _("All timetables"), - "url": "all_timetables", - "svg_icon": "mdi:grid", - "validators": [ - ( - "aleksis.core.util.predicates.permission_validator", - "chronos.view_timetable_overview_rule", - ), - ], - }, - { - "name": _("Daily lessons"), - "url": "lessons_day", - "svg_icon": "mdi:calendar-outline", - "validators": [ - ( - "aleksis.core.util.predicates.permission_validator", - "chronos.view_lessons_day_rule", - ), - ], - }, - { - "name": _("Daily supervisions"), - "url": "supervisions_day", - "svg_icon": "mdi:calendar-outline", - "validators": [ - ( - "aleksis.core.util.predicates.permission_validator", - "chronos.view_supervisions_day_rule", - ), - ], - }, - { - "name": _("Substitutions"), - "url": "substitutions", - "svg_icon": "mdi:update", - "validators": [ - ( - "aleksis.core.util.predicates.permission_validator", - "chronos.view_substitutions_rule", - ), - ], - }, - ], - } - ] -} diff --git a/aleksis/apps/chronos/schema/__init__.py b/aleksis/apps/chronos/schema/__init__.py index 4abeaef132e5bec552c53e1fa385f47205f467e3..f7cc25ff46c7fe7018876cc782fb6ad4e92e4eca 100644 --- a/aleksis/apps/chronos/schema/__init__.py +++ b/aleksis/apps/chronos/schema/__init__.py @@ -5,6 +5,7 @@ from graphene_django import DjangoObjectType from graphene_django_cud.mutations import DjangoCreateMutation, DjangoPatchMutation from aleksis.core.models import CalendarEvent, Group, Person, Room +from aleksis.core.schema.base import DeleteMutation from ..models import LessonEvent from ..util.chronos_helpers import get_classes, get_rooms, get_teachers @@ -57,21 +58,14 @@ class LessonEventType(DjangoObjectType): ) -class AmendLessonCreateMutation(DjangoCreateMutation): - class Meta: - model = LessonEvent - permissions = ("chronos.edit_substitution_rule",) - only_fields = ( - "amends", - "datetime_start", - "datetime_end", - "subject", - "teachers", - "groups", - "rooms", - "cancelled", - "comment", - ) +class DatetimeTimezoneMixin: + """Handle datetimes for mutations with CalendarEvent objects. + + This is necessary because the client sends timezone information as + ISO string which only includes an offset (+00:00 UTC) and an + offset is not a valid timezone. Instead we set UTC as timezone + here directly. + """ @classmethod def handle_datetime_start(cls, value, name, info) -> int: @@ -84,31 +78,45 @@ class AmendLessonCreateMutation(DjangoCreateMutation): return value @classmethod - def before_save(cls, root, info, input, obj): + def before_save(cls, root, info, input, obj, patch_obj=False): + # before_save has different signatures for different mutations + # This handles create & patch + # https://graphene-django-cud.readthedocs.io/en/latest/guide/other-hooks.html?highlight=before_save#before-save + + if patch_obj: + obj = patch_obj + obj.timezone = obj.amends.timezone return obj -class AmendLessonPatchMutation(DjangoPatchMutation): +class AmendLessonCreateMutation(DatetimeTimezoneMixin, DjangoCreateMutation): class Meta: model = LessonEvent permissions = ("chronos.edit_substitution_rule",) - only_fields = ("subject", "teachers", "groups", "rooms", "cancelled", "comment") + only_fields = ( + "amends", + "datetime_start", + "datetime_end", + "subject", + "teachers", + "groups", + "rooms", + "cancelled", + "comment", + ) - @classmethod - def handle_datetime_start(cls, value, name, info) -> int: - value = value.replace(tzinfo=timezone.utc) - return value - @classmethod - def handle_datetime_end(cls, value, name, info) -> int: - value = value.replace(tzinfo=timezone.utc) - return value +class AmendLessonPatchMutation(DatetimeTimezoneMixin, DjangoPatchMutation): + class Meta: + model = LessonEvent + permissions = ("chronos.edit_substitution_rule",) + only_fields = ("subject", "teachers", "groups", "rooms", "cancelled", "comment") - @classmethod - def before_save(cls, root, info, input, id, obj): - obj.timezone = obj.amends.timezone - return obj + +class AmendLessonDeleteMutation(DeleteMutation): + klass = LessonEvent + permission_required = "chronos.edit_substitution_rule" class TimetableType(graphene.Enum): @@ -182,3 +190,4 @@ class Query(graphene.ObjectType): class Mutation(graphene.ObjectType): create_amend_lesson = AmendLessonCreateMutation.Field() patch_amend_lesson = AmendLessonPatchMutation.Field() + delete_amend_lesson = AmendLessonDeleteMutation.Field()