Skip to content
Snippets Groups Projects
Commit d1f3853a authored by magicfelix's avatar magicfelix
Browse files

Merge branch '211-implement-vue-substitution-frontend' into engs-4.0

parents 1685212a 48d59b6a
No related branches found
No related tags found
No related merge requests found
<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>
......@@ -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",
......
......@@ -67,3 +67,9 @@ mutation patchAmendLesson($input: PatchLessonEventInput!, $id: ID!) {
}
}
}
mutation deleteAmendLesson($id: ID!) {
deleteAmendLesson(id: $id) {
ok
}
}
......@@ -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>
......@@ -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",
},
},
],
};
......@@ -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",
......
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",
),
],
},
],
}
]
}
......@@ -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()
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment