diff --git a/aleksis/apps/chronos/frontend/components/SubstitutionOverview.vue b/aleksis/apps/chronos/frontend/components/SubstitutionOverview.vue index 59b5a12b9d3fed15a9766af09e4040b3ef8b2e25..35831f8c20b90c9afcc969af4efe7eae69deced1 100644 --- a/aleksis/apps/chronos/frontend/components/SubstitutionOverview.vue +++ b/aleksis/apps/chronos/frontend/components/SubstitutionOverview.vue @@ -5,7 +5,7 @@ import SubjectChip from "aleksis.apps.cursus/components/SubjectChip.vue"; import { amendedLessonsFromAbsences, - patchAmendLessonsWithAmends, + createOrUpdateSubstitutions, groupsByOwner, } from "./amendLesson.graphql"; @@ -118,7 +118,7 @@ export default { data() { return { gqlQuery: amendedLessonsFromAbsences, - gqlPatchMutation: patchAmendLessonsWithAmends, + gqlPatchMutation: createOrUpdateSubstitutions, groups: [], }; }, diff --git a/aleksis/apps/chronos/frontend/components/amendLesson.graphql b/aleksis/apps/chronos/frontend/components/amendLesson.graphql index 3f69d827ecb35ecb5cdcd28c340cd05255f85b9d..12b8db72de8e9de8004ecd368e760e257c141e10 100644 --- a/aleksis/apps/chronos/frontend/components/amendLesson.graphql +++ b/aleksis/apps/chronos/frontend/components/amendLesson.graphql @@ -83,9 +83,9 @@ mutation patchAmendLessons($input: [BatchPatchLessonEventInput]!) { } } -mutation patchAmendLessonsWithAmends($input: [BatchPatchLessonEventInput]!) { - patchAmendLessonsWithAmends(input: $input) { - items: lessonEvents { +mutation createOrUpdateSubstitutions($input: [SubstitutionInputType]!) { + createOrUpdateSubstitutions(input: $input) { + items: substitutions { id subject { id diff --git a/aleksis/apps/chronos/models.py b/aleksis/apps/chronos/models.py index 4334f3ef4e83cd94c586cde3650d13bd5c2ed44c..051e6ccecd275aa649535256794e6e06d1359899 100644 --- a/aleksis/apps/chronos/models.py +++ b/aleksis/apps/chronos/models.py @@ -1605,29 +1605,44 @@ class LessonEvent(CalendarEvent): ) # 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) + # if so, add it to a list, if not, create a dummy one - amended_lessons = [] + substitutions = [] for event in events: reference_obj = event["REFERENCE_OBJECT"] + datetime_start = event["DTSTART"].dt + datetime_end = event["DTEND"].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) + Q(kolego_absences__datetime_start__lte=datetime_end) + & Q(kolego_absences__datetime_end__gte=datetime_start) ) if affected_teachers.exists(): - obj, created = LessonEvent.objects.update_or_create( - amends=reference_obj, + existing_substitutions = reference_obj.amended_by.filter( 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 + if existing_substitutions.exists(): + substitution = existing_substitutions.first() + # if incomplete and doc.topic: + # continue + substitutions.append(substitution) + + else: + substitutions.append( + cls( + pk=f"DUMMY;{reference_obj.id};{datetime_start.isoformat()};{datetime_end.isoformat()}", + amends=reference_obj, + datetime_start=datetime_start, + datetime_end=datetime_end, + ) + ) + + return substitutions class Meta: verbose_name = _("Lesson Event") diff --git a/aleksis/apps/chronos/schema/__init__.py b/aleksis/apps/chronos/schema/__init__.py index fdda397a3ffbdd762dceb46d1cd3d93cb8a22803..6898fb5ae6314f853d4de7b7b2eb4a2dfa141659 100644 --- a/aleksis/apps/chronos/schema/__init__.py +++ b/aleksis/apps/chronos/schema/__init__.py @@ -12,6 +12,7 @@ from graphene_django_cud.mutations import ( DjangoBatchDeleteMutation, DjangoBatchPatchMutation, ) +from reversion import create_revision, set_comment, set_user from aleksis.core.models import CalendarEvent, Group, Person, Room from aleksis.core.schema.base import DeleteMutation, FilterOrderList @@ -63,6 +64,35 @@ class LessonEventType(DjangoObjectType): amends = graphene.Field(lambda: LessonEventType, required=False) + @staticmethod + def resolve_teachers(root: LessonEvent, info, **kwargs): + if not str(root.pk).startswith("DUMMY") and hasattr(root, "teachers"): + return root.teachers + elif root.amends: + affected_teachers = root.amends.teachers.filter( + Q(kolego_absences__datetime_start__lte=root.datetime_end) + & Q(kolego_absences__datetime_end__gte=root.datetime_start) + ) + return root.amends.teachers.exclude(pk__in=affected_teachers) + return [] + + @staticmethod + def resolve_groups(root: LessonEvent, info, **kwargs): + if not str(root.pk).startswith("DUMMY") and hasattr(root, "groups"): + return root.groups + elif root.amends: + root.amends.groups + return [] + + @staticmethod + def resolve_rooms(root: LessonEvent, info, **kwargs): + if not str(root.pk).startswith("DUMMY") and hasattr(root, "rooms"): + return root.rooms + elif root.amends: + root.amends.rooms + return [] + + class DatetimeTimezoneMixin: """Handle datetimes for mutations with CalendarEvent objects. @@ -126,6 +156,89 @@ class AmendLessonBatchDeleteMutation(DjangoBatchDeleteMutation): permissions = ("chronos.delete_substitution_rule",) +class SubstitutionInputType(graphene.InputObjectType): + id = graphene.ID(required=True) + subject = graphene.ID(required=False) + teachers = graphene.List(graphene.ID, required=False) + rooms = graphene.List(graphene.ID, required=False) + + comment = graphene.String(required=False) + cancelled = graphene.Boolean(required=False) + + +class SubstitutionBatchCreateOrUpdateMutation(graphene.Mutation): + class Arguments: + input = graphene.List(SubstitutionInputType) + + substitutions = graphene.List(LessonEventType) + + @classmethod + def create_or_update(cls, info, substitution): + _id = substitution.id + + # Sadly, we can't use the update_or_create method since create_defaults + # is only introduced in Django 5.0 + if _id.startswith("DUMMY"): + dummy, amended_lesson_event_id, datetime_start_iso, datetime_end_iso = _id.split(";") + amended_lesson_event = LessonEvent.objects.get(id=amended_lesson_event_id) + + datetime_start = datetime.fromisoformat(datetime_start_iso).astimezone( + amended_lesson_event.timezone + ) + datetime_end = datetime.fromisoformat(datetime_end_iso).astimezone( + amended_lesson_event.timezone + ) + + if info.context.user.has_perm( + "chronos.change_lessonsubstitution" + ): + obj = LessonEvent.objects.create( + datetime_start=datetime_start, + datetime_end=datetime_end, + amends=amended_lesson_event, + subject=substitution.subject, + comment=substitution.comment or "", + cancelled=substitution.cancelled, + ) + if substitution.teachers is not None: + obj.teachers.set(Person.objects.filter(pk__in=substitution.teachers)) + if substitution.rooms is not None: + obj.rooms.set(Room.objects.filter(pk__in=substitution.rooms)) + obj.save() + return obj + raise PermissionDenied() + else: + obj = LessonEvent.objects.get(id=_id) + + if not info.context.user.has_perm("chronos.edit_substitution_rule", obj): + raise PermissionDenied() + + if substitution.subject is not None: + obj.subject = Subject.objects.get(pk=substitution.subject) + if substitution.teachers is not None: + obj.teachers.set(Person.objects.filter(pk__in=substitution.teachers)) + if substitution.rooms is not None: + obj.rooms.set(Room.objects.filter(pk__in=substitution.rooms)) + + if substitution.cancelled is not None: + obj.cancelled = substitution.cancelled + if substitution.comment is not None: + obj.comment = substitution.comment + + obj.save() + return obj + + @classmethod + def mutate(cls, root, info, input): # noqa + with create_revision(): + set_user(info.context.user) + set_comment("Updated in substitution overview") + objs = [cls.create_or_update(info, substitution) for substitution in input] + + return SubstitutionBatchCreateOrUpdateMutation(substitutions=objs) + + + class TimetableType(graphene.Enum): TEACHER = "teacher" GROUP = "group" @@ -226,3 +339,5 @@ class Mutation(graphene.ObjectType): patch_amend_lessons = AmendLessonBatchPatchMutation.Field() patch_amend_lessons_with_amends = AmendLessonBatchPatchMutation.Field() delete_amend_lessons = AmendLessonBatchDeleteMutation.Field() + + create_or_update_substitutions = SubstitutionBatchCreateOrUpdateMutation.Field()