diff --git a/aleksis/apps/alsijil/frontend/components/coursebook/absences/AbsenceCreationDialog.vue b/aleksis/apps/alsijil/frontend/components/coursebook/absences/AbsenceCreationDialog.vue index d203ad680dcae02fda4fc736e2099845c8f5ce3b..3263225dc890242ed038f8b5f58153278e5aea64 100644 --- a/aleksis/apps/alsijil/frontend/components/coursebook/absences/AbsenceCreationDialog.vue +++ b/aleksis/apps/alsijil/frontend/components/coursebook/absences/AbsenceCreationDialog.vue @@ -93,6 +93,7 @@ import SecondaryActionButton from "aleksis.core/components/generic/buttons/Secon import loadingMixin from "aleksis.core/mixins/loadingMixin.js"; import permissionsMixin from "aleksis.core/mixins/permissions.js"; import mutateMixin from "aleksis.core/mixins/mutateMixin.js"; +import { DateTime } from "luxon"; import { createAbsencesForPersons } from "./absenceCreation.graphql"; @@ -122,6 +123,7 @@ export default { }, mounted() { this.addPermissions(["alsijil.view_register_absence_rule"]); + this.clearForm(); }, methods: { cancel() { @@ -131,8 +133,12 @@ export default { }, clearForm() { this.persons = []; - this.startDate = ""; - this.endDate = ""; + this.startDate = DateTime.now() + .startOf("day") + .toISO({ suppressSeconds: true }); + this.endDate = DateTime.now() + .endOf("day") + .toISO({ suppressSeconds: true }); this.comment = ""; this.absenceReason = ""; }, @@ -142,8 +148,8 @@ export default { createAbsencesForPersons, { persons: this.persons.map((p) => p.id), - start: this.startDate, - end: this.endDate, + start: this.$toUTCISO(this.$parseISODate(this.startDate)), + end: this.$toUTCISO(this.$parseISODate(this.endDate)), comment: this.comment, reason: this.absenceReason, }, diff --git a/aleksis/apps/alsijil/frontend/components/coursebook/absences/AbsenceCreationForm.vue b/aleksis/apps/alsijil/frontend/components/coursebook/absences/AbsenceCreationForm.vue index 559824bdf4cb8bb214d1be3acebba38c08c836cd..239c83d6bbd48700773c024967636a82f3d3c4a8 100644 --- a/aleksis/apps/alsijil/frontend/components/coursebook/absences/AbsenceCreationForm.vue +++ b/aleksis/apps/alsijil/frontend/components/coursebook/absences/AbsenceCreationForm.vue @@ -26,9 +26,10 @@ <v-row> <v-col cols="12" :sm="6" class="pl-0"> <div aria-required="true"> - <date-field + <date-time-field :label="$t('forms.labels.start')" - :max="endDate" + :max-date="endDate" + :max-time="maxStartTime" :rules="$rules().required.build()" :value="startDate" @input="$emit('start-date', $event)" @@ -37,9 +38,10 @@ </v-col> <v-col cols="12" :sm="6" class="pr-0"> <div aria-required="true"> - <date-field + <date-time-field :label="$t('forms.labels.end')" - :min="startDate" + :min-date="startDate" + :min-time="minEndTime" :rules="$rules().required.build()" :value="endDate" @input="$emit('end-date', $event)" @@ -69,15 +71,16 @@ <script> import AbsenceReasonGroupSelect from "aleksis.apps.kolego/components/AbsenceReasonGroupSelect.vue"; -import DateField from "aleksis.core/components/generic/forms/DateField.vue"; +import DateTimeField from "aleksis.core/components/generic/forms/DateTimeField.vue"; import { persons } from "./absenceCreation.graphql"; import formRulesMixin from "aleksis.core/mixins/formRulesMixin.js"; +import { DateTime } from "luxon"; export default { name: "AbsenceCreationForm", components: { AbsenceReasonGroupSelect, - DateField, + DateTimeField, }, mixins: [formRulesMixin], emits: [ @@ -113,5 +116,25 @@ export default { required: true, }, }, + computed: { + maxStartTime() { + // Only if on the same day + const start = DateTime.fromISO(this.startDate); + const end = DateTime.fromISO(this.endDate); + + if (start.day !== end.day) return; + + return end.minus({ minutes: 5 }).toFormat("HH:mm"); + }, + minEndTime() { + // Only if on the same day + const start = DateTime.fromISO(this.startDate); + const end = DateTime.fromISO(this.endDate); + + if (start.day !== end.day) return; + + return start.plus({ minutes: 5 }).toFormat("HH:mm"); + }, + }, }; </script> diff --git a/aleksis/apps/alsijil/frontend/components/coursebook/absences/absenceCreation.graphql b/aleksis/apps/alsijil/frontend/components/coursebook/absences/absenceCreation.graphql index 21ce30d0b871bcfbd9a5b40b671a8594948a2569..5a520453f35062d49edd5eb82aca291384e2f739 100644 --- a/aleksis/apps/alsijil/frontend/components/coursebook/absences/absenceCreation.graphql +++ b/aleksis/apps/alsijil/frontend/components/coursebook/absences/absenceCreation.graphql @@ -6,7 +6,7 @@ query persons { } } -query lessonsForPersons($persons: [ID]!, $start: Date!, $end: Date!) { +query lessonsForPersons($persons: [ID]!, $start: DateTime!, $end: DateTime!) { items: lessonsForPersons(persons: $persons, start: $start, end: $end) { id lessons { @@ -31,8 +31,8 @@ query lessonsForPersons($persons: [ID]!, $start: Date!, $end: Date!) { # Use absencesInputType? mutation createAbsencesForPersons( $persons: [ID]! - $start: Date! - $end: Date! + $start: DateTime! + $end: DateTime! $comment: String $reason: ID! ) { diff --git a/aleksis/apps/alsijil/managers.py b/aleksis/apps/alsijil/managers.py index 7d0130805275359672d1337133eacd1b0a2b62ad..dc29fae25c189c7c2250279a6e8d86206f52e520 100644 --- a/aleksis/apps/alsijil/managers.py +++ b/aleksis/apps/alsijil/managers.py @@ -11,7 +11,7 @@ from django.utils.translation import gettext as _ from calendarweek import CalendarWeek from aleksis.apps.chronos.managers import DateRangeQuerySetMixin -from aleksis.core.managers import AlekSISBaseManagerWithoutMigrations, PolymorphicBaseManager +from aleksis.core.managers import AlekSISBaseManagerWithoutMigrations, RecurrencePolymorphicManager if TYPE_CHECKING: from aleksis.core.models import Group @@ -189,7 +189,7 @@ class GroupRoleAssignmentQuerySet(DateRangeQuerySetMixin, QuerySet): return self.filter(Q(groups=group) | Q(groups__child_groups=group)) -class DocumentationManager(PolymorphicBaseManager): +class DocumentationManager(RecurrencePolymorphicManager): """Manager adding specific methods to documentations.""" def get_queryset(self): @@ -205,9 +205,7 @@ class DocumentationManager(PolymorphicBaseManager): ) -class ParticipationStatusManager(PolymorphicBaseManager): +class ParticipationStatusManager(RecurrencePolymorphicManager): """Manager adding specific methods to participation statuses.""" - def get_queryset(self): - """Ensure often used related data are loaded as well.""" - return super().get_queryset().select_related("person", "absence_reason", "base_absence") + pass diff --git a/aleksis/apps/alsijil/schema/__init__.py b/aleksis/apps/alsijil/schema/__init__.py index f63b8c2c9b8c5c029371e4663f6d70b2e5271bb8..ab87c4306cd144d4c5f27c00914aceeb865584a7 100644 --- a/aleksis/apps/alsijil/schema/__init__.py +++ b/aleksis/apps/alsijil/schema/__init__.py @@ -63,8 +63,8 @@ class Query(graphene.ObjectType): lessons_for_persons = graphene.List( LessonsForPersonType, persons=graphene.List(graphene.ID, required=True), - start=graphene.Date(required=True), - end=graphene.Date(required=True), + start=graphene.DateTime(required=True), + end=graphene.DateTime(required=True), ) extra_marks = FilterOrderList(ExtraMarkType) @@ -213,8 +213,8 @@ class Query(graphene.ObjectType): for person in persons: docs, dummies = Documentation.get_documentations_for_person( person, - datetime.combine(start, datetime.min.time()), - datetime.combine(end, datetime.max.time()), + start, + end, ) lessons_for_person.append(LessonsForPersonType(id=person, lessons=docs + dummies)) diff --git a/aleksis/apps/alsijil/schema/absences.py b/aleksis/apps/alsijil/schema/absences.py index d05ac55214a36430808bd17ec22cc2f4c6767313..ab006e1d875e053e061d04c9c800ae0c3ef15f2e 100644 --- a/aleksis/apps/alsijil/schema/absences.py +++ b/aleksis/apps/alsijil/schema/absences.py @@ -1,6 +1,8 @@ -from datetime import datetime +import datetime +from typing import List from django.core.exceptions import PermissionDenied +from django.db.models import Q import graphene @@ -14,8 +16,8 @@ from .participation_status import ParticipationStatusType class AbsencesForPersonsCreateMutation(graphene.Mutation): class Arguments: persons = graphene.List(graphene.ID, required=True) - start = graphene.Date(required=True) - end = graphene.Date(required=True) + start = graphene.DateTime(required=True) + end = graphene.DateTime(required=True) comment = graphene.String(required=False) reason = graphene.ID(required=True) @@ -23,7 +25,16 @@ class AbsencesForPersonsCreateMutation(graphene.Mutation): participation_statuses = graphene.List(ParticipationStatusType) @classmethod - def mutate(cls, root, info, persons, start, end, comment, reason): # noqa + def mutate( + cls, + root, + info, + persons: List[str | int], + start: datetime.datetime, + end: datetime.datetime, + comment: str, + reason: str | int, + ): participation_statuses = [] persons = Person.objects.filter(pk__in=persons) @@ -31,17 +42,30 @@ class AbsencesForPersonsCreateMutation(graphene.Mutation): for person in persons: if not info.context.user.has_perm("alsijil.register_absence_rule", person): raise PermissionDenied() - kolego_absence, __ = Absence.objects.get_or_create( - date_start=start, - date_end=end, + + # Check if there is an existing absence with overlapping datetime + absences = Absence.objects.filter( + Q(datetime_start__lte=start) | Q(date_start__lte=start.date()), + Q(datetime_end__gte=end) | Q(date_end__gte=end.date()), reason_id=reason, person=person, - defaults={"comment": comment}, ) + if len(absences) > 0: + kolego_absence = absences.first() + else: + # Check for same times and create otherwise + kolego_absence, __ = Absence.objects.get_or_create( + datetime_start=start, + datetime_end=end, + reason_id=reason, + person=person, + defaults={"comment": comment}, + ) + events = ParticipationStatus.get_single_events( - datetime.combine(start, datetime.min.time()), - datetime.combine(end, datetime.max.time()), + start, + end, None, {"person": person}, with_reference_object=True,