Skip to content
Snippets Groups Projects
Commit 21021794 authored by Jonathan Weth's avatar Jonathan Weth :keyboard:
Browse files

Merge branch '211-implement-vue-substitution-frontend' into 'master'

Resolve "Implement Vue substitution frontend"

Closes #211

See merge request !310
parents 2743bd0f b8cfaaac
No related branches found
No related tags found
1 merge request!310Resolve "Implement Vue substitution frontend"
Pipeline #177598 failed
<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="deleteEvent = true"
/>
<dialog-object-form
v-model="edit"
:fields="fields"
:is-create="!selectedEvent.meta.amended"
create-item-i18n-key="chronos.event.amend.title"
:gql-create-mutation="gqlCreateMutation"
:get-create-data="transformCreateData"
:default-item="defaultItem"
edit-item-i18n-key="chronos.event.amend.title"
:gql-patch-mutation="gqlPatchMutation"
:get-patch-data="transformPatchData"
:edit-item="initPatchData"
@cancel="open = false"
@save="updateOnSave()"
>
<!-- eslint-disable-next-line vue/valid-v-slot -->
<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>
<!-- eslint-disable-next-line vue/valid-v-slot -->
<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>
<!-- eslint-disable-next-line vue/valid-v-slot -->
<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>
<!-- eslint-disable-next-line vue/valid-v-slot -->
<template #cancelled.field="{ attrs, on }">
<v-checkbox v-bind="attrs" v-on="on" />
</template>
<!-- eslint-disable-next-line vue/valid-v-slot -->
<template #comment.field="{ attrs, on }">
<v-textarea v-bind="attrs" v-on="on" />
</template>
</dialog-object-form>
<delete-dialog
delete-success-message-i18n-key="chronos.event.amend.delete_success"
:gql-delete-mutation="gqlDeleteMutation"
v-model="deleteEvent"
:items="[selectedEvent.meta]"
:get-name-of-item="getLessonDeleteText"
@save="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,
createAmendLessons,
patchAmendLessons,
deleteAmendLessons,
} from "./amendLesson.graphql";
export default {
name: "AmendLesson",
components: {
EditButton,
DialogObjectForm,
DeleteButton,
DeleteDialog,
},
mixins: [permissionsMixin],
props: {
selectedEvent: {
type: Object,
required: true,
},
},
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",
},
],
defaultItem: {
cancelled: this.selectedEvent.meta.cancelled,
comment: this.selectedEvent.meta.comment,
},
gqlCreateMutation: createAmendLessons,
gqlPatchMutation: patchAmendLessons,
deleteEvent: false,
gqlDeleteMutation: deleteAmendLessons,
};
},
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;
},
getLessonDeleteText(item) {
return `${this.selectedEvent.name} · ${this.$d(
this.selectedEvent.start,
"shortDateTime",
)}${this.$d(this.selectedEvent.end, "shortTime")}`;
},
},
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>
query gqlSubjects {
amendableSubjects: subjects {
id
name
}
}
query gqlPersons {
amendableTeachers: persons {
id
fullName
}
}
query gqlRooms {
amendableRooms: rooms {
id
name
}
}
mutation createAmendLessons($input: [BatchCreateLessonEventInput]!) {
createAmendLessons(input: $input) {
items: lessonEvents {
id
amends {
id
}
datetimeStart
datetimeEnd
subject {
id
}
teachers {
id
}
groups {
id
}
rooms {
id
}
cancelled
comment
}
}
}
mutation patchAmendLessons($input: [BatchPatchLessonEventInput]!) {
patchAmendLessons(input: $input) {
items: lessonEvents {
id
subject {
id
}
teachers {
id
}
groups {
id
}
rooms {
id
}
cancelled
comment
}
}
}
mutation deleteAmendLessons($ids: [ID]!) {
deleteAmendLessons(ids: $ids) {
deletionCount
}
}
......@@ -100,6 +100,11 @@
</v-list-item-title>
</v-list-item-content>
</v-list-item>
<amend-lesson
v-if="selectedEvent"
:selected-event="selectedEvent"
@refreshCalendar="$emit('refreshCalendar')"
/>
</template>
</base-calendar-feed-details>
</template>
......@@ -111,8 +116,12 @@ import CalendarStatusChip from "aleksis.core/components/calendar/CalendarStatusC
import CancelledCalendarStatusChip from "aleksis.core/components/calendar/CancelledCalendarStatusChip.vue";
import LessonRelatedObjectChip from "../../LessonRelatedObjectChip.vue";
import lessonEvent from "../mixins/lessonEvent";
import LessonEventSubject from "../../LessonEventSubject.vue";
import AmendLesson from "../../AmendLesson.vue";
export default {
name: "LessonDetails",
components: {
......@@ -121,6 +130,7 @@ export default {
BaseCalendarFeedDetails,
CalendarStatusChip,
CancelledCalendarStatusChip,
AmendLesson,
},
mixins: [calendarFeedDetailsMixin, lessonEvent],
};
......
......@@ -32,7 +32,19 @@
"event": {
"no_teacher": "No teacher",
"no_room": "No room",
"current_changes": "Current changes"
"current_changes": "Current changes",
"amend": {
"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",
"rooms": "Rooms",
"cancelled": "Cancelled",
"comment": "Comment"
}
}
}
}
......@@ -1437,7 +1437,7 @@ class LessonEvent(CalendarEvent):
def subject_name_with_amends(self: LessonEvent) -> str:
"""Get formatted subject name (including amends)."""
my_subject = self.subject.name if self.subject else ""
amended_subject = self.amends.subject.name if self.amends else ""
amended_subject = self.amends.subject.name if self.amends and self.amends.subject else ""
if my_subject and amended_subject:
return _("{} (instead of {})").format(my_subject, amended_subject)
......@@ -1461,8 +1461,10 @@ class LessonEvent(CalendarEvent):
elif request:
title += " · " + reference_object.teacher_names_with_amends
else:
title += f" · {reference_object.group_names} · {reference_object.teacher_names}"
title += (
f" · {reference_object.group_names} · "
+ f"{reference_object.teacher_names_with_amends}"
)
if reference_object.rooms.all().exists():
title += " · " + reference_object.room_names_with_amends
return title
......@@ -1522,6 +1524,7 @@ class LessonEvent(CalendarEvent):
"""
return {
"id": reference_object.id,
"amended": bool(reference_object.amends),
"amends": cls.value_meta(reference_object.amends, request)
if reference_object.amends
......
from datetime import timezone
import graphene
from graphene_django import DjangoObjectType
from graphene_django_cud.mutations import (
DjangoBatchCreateMutation,
DjangoBatchDeleteMutation,
DjangoBatchPatchMutation,
)
from aleksis.core.models import Group, Person, Room
......@@ -28,6 +35,90 @@ class TimetableRoomType(DjangoObjectType):
skip_registry = True
class LessonEventType(DjangoObjectType):
class Meta:
model = LessonEvent
fields = (
"id",
"amends",
"datetime_start",
"datetime_end",
"subject",
"teachers",
"groups",
"rooms",
"cancelled",
"comment",
)
filter_fields = {
"id": ["exact", "lte", "gte"],
}
amends = graphene.Field(lambda: LessonEventType, required=False)
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:
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 AmendLessonBatchCreateMutation(DatetimeTimezoneMixin, DjangoBatchCreateMutation):
class Meta:
model = LessonEvent
permissions = ("chronos.edit_substitution_rule",)
only_fields = (
"amends",
"datetime_start",
"datetime_end",
"subject",
"teachers",
"groups",
"rooms",
"cancelled",
"comment",
)
@classmethod
def before_save(cls, root, info, input, created_objects): # noqa: A002
for obj in created_objects:
obj.timezone = obj.amends.timezone
return created_objects
class AmendLessonBatchPatchMutation(DatetimeTimezoneMixin, DjangoBatchPatchMutation):
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, updated_objects): # noqa: A002
for obj in updated_objects:
obj.timezone = obj.amends.timezone
return updated_objects
class AmendLessonBatchDeleteMutation(DjangoBatchDeleteMutation):
class Meta:
model = LessonEvent
permissions = ("chronos.delete_substitution_rule",)
class TimetableType(graphene.Enum):
TEACHER = "teacher"
GROUP = "group"
......@@ -48,15 +139,6 @@ 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)
......@@ -103,3 +185,9 @@ class Query(graphene.ObjectType):
)
return all_timetables
class Mutation(graphene.ObjectType):
create_amend_lessons = AmendLessonBatchCreateMutation.Field()
patch_amend_lessons = AmendLessonBatchPatchMutation.Field()
delete_amend_lessons = AmendLessonBatchDeleteMutation.Field()
{% if type == "substitution" %}
{% if el.cancelled or el.cancelled_for_teachers %}
{# Canceled lesson: no room #}
{# Cancelled lesson: no room #}
{% elif el.room and el.lesson_period.room %}
{# New and old room available #}
<span class="tooltipped" data-position="bottom"
......
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