Skip to content
Snippets Groups Projects
Commit fa84c9d8 authored by Hangzhi Yu's avatar Hangzhi Yu
Browse files

WIP: Add very basic substitution to-do-list

parent 56621109
No related branches found
No related tags found
1 merge request!329Introduce substitution to do list
<script setup>
import AmendedLessonCard from "./AmendedLessonCard.vue";
import CRUDIterator from "aleksis.core/components/generic/CRUDIterator.vue";
import DateField from "aleksis.core/components/generic/forms/DateField.vue";
import SubjectChip from "aleksis.apps.cursus/components/SubjectChip.vue";
import { DateTime } from "luxon";
import { amendedLessonsFromAbsences, batchPatchAmendLessons, gqlGroups } from "./amendLesson.graphql";
</script>
<template>
<c-r-u-d-iterator
:gql-query="gqlQuery"
:gql-patch-mutation="gqlPatchMutation"
:get-patch-data="gqlGetPatchData"
:gql-filters="gqlFilters"
i18n-key="test"
:enable-search="false"
:enable-filter="true"
:enable-create="false"
:show-create="false"
:enable-delete="false"
:enable-edit="true"
:headers="headers"
:force-model-item-update="true"
@lastQuery="lastQuery = $event"
>
<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" />
</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: {
},
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,
};
},
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)
.sort()
.map((key) => byDay[key]);
},
gqlGetPatchData(item) {
return { id: item.id, teachers: item.teachers }
},
},
apollo: {
groups: gqlGroups,
}
};
</script>
<script setup>
import DeleteButton from "aleksis.core/components/generic/buttons/DeleteButton.vue";
import LessonInformation from "./LessonInformation.vue";
import LessonRelatedObjectChip from "./LessonRelatedObjectChip.vue";
import { gqlPersons } from "./amendLesson.graphql";
import createOrPatchMixin from "aleksis.core/mixins/createOrPatchMixin.js";
</script>
<template>
<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">
<lesson-information
class="flex-grow-1"
:lesson="$attrs['amended-lesson']"
/>
<v-autocomplete
v-model="teachers"
multiple
chips
deletable-chips
:items="amendableTeachers"
item-text="fullName"
item-value="id"
class="flex-grow-1 flex-shrink-0 mx-2"
@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>
</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>
</div>
<v-divider/>
<!--<v-card-actions>-->
<!-- <v-spacer/>-->
<!-- <cancel-button @click="$emit('close')" :disabled="loading" />-->
<!-- <save-button-->
<!-- @click="save"-->
<!-- :loading="loading"-->
<!-- />-->
<!--</v-card-actions>-->
</v-card>
</template>
<script>
//import SaveButton from "aleksis.core/components/generic/buttons/SaveButton.vue";
//import CancelButton from "aleksis.core/components/generic/buttons/CancelButton.vue";
//import CancelButton from "aleksis.core/components/generic/buttons/CancelButton.vue";
//import { createOrUpdateDocumentations } from "../coursebook.graphql";
export default {
name: "AmendedLessonCard",
emits: ["open", "close"],
mixins: [createOrPatchMixin],
data() {
return {
loading: false,
teachers: [],
};
},
methods: {
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 };
});
return teachersWithStatus;
},
save() {
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,
}]);
},
},
apollo: {
amendableTeachers: gqlPersons,
},
mounted() {
this.teachers = this.$attrs["amended-lesson"].teachers.map((teacher) => teacher.id);
},
};
</script>
<script setup>
import CancelledCalendarStatusChip from "aleksis.core/components/calendar/CancelledCalendarStatusChip.vue";
import SubjectChip from "aleksis.apps.cursus/components/SubjectChip.vue";
import { DateTime } from "luxon";
</script>
<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 }">
{{ $d(toDateTime(lesson.datetimeStart), "shortTime") }}
{{ $d(toDateTime(lesson.datetimeEnd), "shortTime") }}
{{ getCourse(lesson)?.name }}
</div>
<subject-chip
v-if="getSubject(lesson)"
:subject="getSubject(lesson)"
/>
</v-card-text>
</template>
<script>
export default {
name: "LessonInformation",
props: {
lesson: {
type: Object,
required: true,
},
cancelled: {
type: Boolean,
required: false,
default: false,
},
},
methods: {
toDateTime(dateString) {
return DateTime.fromISO(dateString);
},
getSubject(lesson) {
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;
},
},
};
</script>
......@@ -19,6 +19,14 @@ query gqlRooms {
}
}
query gqlGroups {
groups: groups {
id
name
shortName
}
}
mutation createAmendLesson($input: CreateLessonEventInput!) {
createAmendLesson(input: $input) {
lessonEvent {
......@@ -68,8 +76,133 @@ mutation patchAmendLesson($input: PatchLessonEventInput!, $id: ID!) {
}
}
mutation batchPatchAmendLessons($input: [BatchPatchLessonEventInput]!) {
batchPatchAmendLessons(input: $input) {
items: lessonEvents {
id
subject {
id
shortName
name
colourFg
colourBg
}
teachers {
id
shortName
fullName
}
groups {
id
}
rooms {
id
}
course {
id
subject {
id
shortName
name
colourFg
colourBg
}
}
realAmends {
id
teachers {
id
shortName
fullName
}
subject {
id
shortName
name
colourFg
colourBg
}
groups {
id
shortName
}
course {
id
name
}
}
datetimeStart
datetimeEnd
cancelled
comment
}
}
}
mutation deleteAmendLesson($id: ID!) {
deleteAmendLesson(id: $id) {
ok
}
}
query amendedLessonsFromAbsences($filters: JSONString!) {
items: amendedLessonsFromAbsences(filters: $filters) {
id
subject {
id
shortName
name
colourFg
colourBg
}
teachers {
id
shortName
fullName
}
groups {
id
}
course {
id
subject {
id
shortName
name
colourFg
colourBg
}
name
}
rooms {
id
}
realAmends {
id
teachers {
id
shortName
fullName
}
subject {
id
shortName
name
colourFg
colourBg
}
groups {
id
shortName
}
course {
id
name
}
}
datetimeStart
datetimeEnd
cancelled
comment
}
}
import { hasPersonValidator } from "aleksis.core/routeValidators";
import Timetable from "./components/Timetable.vue";
import AmendLessonOverview from "./components/AmendLessonOverview.vue"
export default {
meta: {
......@@ -31,5 +32,15 @@ export default {
permission: "chronos.view_timetable_overview_rule",
},
},
{
path: "amend_lesson_overview/",
component: AmendLessonOverview,
name: "chronos.amendLessonOverview",
meta: {
inMenu: true,
titleKey: "chronos.amendLessonOverview.menu_title",
icon: "mdi-grid",
},
},
],
};
......@@ -895,3 +895,16 @@ class LessonEventQuerySet(PolymorphicQuerySet):
return self.filter(
Q(teachers=person) | Q(groups__members=person) | Q(pk__in=amended)
).distinct()
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__lte=F("datetime_end"))
& Q(teachers__kolego_absences__datetime_end__gte=F("datetime_start"))
& Q(amends__isnull=True)
& Q(amended_by__isnull=True)
)
......@@ -11,7 +11,7 @@ from django.contrib.contenttypes.models import ContentType
from django.core.exceptions import ValidationError
from django.core.validators import MinValueValidator
from django.db import models
from django.db.models import Max, Min, Q
from django.db.models import F, Max, Min, Q
from django.db.models.functions import Coalesce
from django.dispatch import receiver
from django.forms import Media
......@@ -1424,7 +1424,7 @@ class LessonEvent(CalendarEvent):
@property
def subject_name_with_amends(self: LessonEvent) -> str:
my_subject = self.subject.name
my_subject = self.subject.name if self.subject else ""
amended_subject = self.real_amends.subject.name if self.amends else ""
if my_subject and amended_subject:
......@@ -1543,6 +1543,10 @@ class LessonEvent(CalendarEvent):
if params:
obj_id = int(params.get("id", 0))
type_ = params.get("type", None)
prefetch_absences = params.get("prefetch_absences", False)
if prefetch_absences:
objs = objs.prefetch_related("teachers__kolego_absences")
if type_ and obj_id:
if type_ == "TEACHER":
......@@ -1553,6 +1557,42 @@ class LessonEvent(CalendarEvent):
return objs.for_room(obj_id)
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:
"""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
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)
# (1.5 filter them by permissions)
...
# 2. For each lessonEvent → check if there are any teachers with absences that overlap the lesson & if yes, check if there is already an amendment for that lesson
# if so, add it to a list, if not, create a new one (no dummy creation here possible since teachers is a m2m field)
amended_lessons = []
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))
if affected_teachers.exists():
obj, created = LessonEvent.objects.update_or_create(
amends=reference_obj,
datetime_start=event["DTSTART"].dt,
datetime_end=event["DTEND"].dt,
)
if created:
obj.teachers.set(reference_obj.teachers.exclude(pk__in=affected_teachers))
amended_lessons.append(obj)
return amended_lessons
class Meta:
verbose_name = _("Lesson Event")
verbose_name_plural = _("Lesson Events")
......
from datetime import timezone
from datetime import date, datetime, timezone
from functools import reduce
from operator import and_
from django.db.models import F, ManyToManyField, OuterRef, Subquery, Q, Prefetch
import graphene
from graphene_django import DjangoObjectType
from graphene_django_cud.mutations import 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.apps.kolego.models import Absence
from ..models import LessonEvent
from ..util.chronos_helpers import get_classes, get_rooms, get_teachers
......@@ -53,11 +59,32 @@ class LessonEventType(DjangoObjectType):
"teachers",
"groups",
"rooms",
"course",
"cancelled",
"comment",
)
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)
class DatetimeTimezoneMixin:
"""Handle datetimes for mutations with CalendarEvent objects.
......@@ -114,6 +141,13 @@ class AmendLessonPatchMutation(DatetimeTimezoneMixin, DjangoPatchMutation):
only_fields = ("subject", "teachers", "groups", "rooms", "cancelled", "comment")
class AmendLessonBatchPatchMutation(DjangoBatchPatchMutation):
class Meta:
model = LessonEvent
permissions = ("chronos.edit_substitution_rule",)
only_fields = ("id", "subject", "teachers", "groups", "rooms", "cancelled", "comment")
class AmendLessonDeleteMutation(DeleteMutation):
klass = LessonEvent
permission_required = "chronos.edit_substitution_rule"
......@@ -145,6 +179,11 @@ class Query(graphene.ObjectType):
timetable_rooms = graphene.List(TimetableRoomType)
available_timetables = graphene.List(TimetableObjectType)
amended_lessons_from_absences = graphene.List(
LessonEventTypeWithRealAmends,
filters=graphene.JSONString(required=True),
)
def resolve_timetable_teachers(self, info, **kwargs):
return get_teachers(info.context.user)
......@@ -186,8 +225,21 @@ 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())
group_id = filters.get("group_id")
# 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)
class Mutation(graphene.ObjectType):
create_amend_lesson = AmendLessonCreateMutation.Field()
patch_amend_lesson = AmendLessonPatchMutation.Field()
batch_patch_amend_lessons = AmendLessonBatchPatchMutation.Field()
delete_amend_lesson = AmendLessonDeleteMutation.Field()
......@@ -54,6 +54,7 @@ calendarweek = "^0.5.0"
aleksis-core = "^4.0.0.dev2"
aleksis-app-resint = "^4.0.0.dev1"
aleksis-app-cursus = "^0.1.dev0"
aleksis-app-kolego = "^0.1.dev0"
[tool.poetry.plugins."aleksis.app"]
......
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