diff --git a/aleksis/apps/alsijil/frontend/components/coursebook/absences/ManageStudentsDialog.vue b/aleksis/apps/alsijil/frontend/components/coursebook/absences/ManageStudentsDialog.vue index c165cec1f90b936cba67b583564badd0aa6cd92c..2be32b7ff04c7a72e491fc4455b7b55feba54c3f 100644 --- a/aleksis/apps/alsijil/frontend/components/coursebook/absences/ManageStudentsDialog.vue +++ b/aleksis/apps/alsijil/frontend/components/coursebook/absences/ManageStudentsDialog.vue @@ -1,7 +1,6 @@ <script> import AbsenceReasonButtons from "aleksis.apps.kolego/components/AbsenceReasonButtons.vue"; import AbsenceReasonChip from "aleksis.apps.kolego/components/AbsenceReasonChip.vue"; -import AbsenceReasonGroupSelect from "aleksis.apps.kolego/components/AbsenceReasonGroupSelect.vue"; import DialogCloseButton from "aleksis.core/components/generic/buttons/DialogCloseButton.vue"; import SecondaryActionButton from "aleksis.core/components/generic/buttons/SecondaryActionButton.vue"; import MobileFullscreenDialog from "aleksis.core/components/generic/dialogs/MobileFullscreenDialog.vue"; @@ -17,6 +16,7 @@ import TardinessChip from "./TardinessChip.vue"; import TardinessField from "./TardinessField.vue"; import ExtraMarkButtons from "../../extra_marks/ExtraMarkButtons.vue"; import MessageBox from "aleksis.core/components/generic/MessageBox.vue"; +import UpdateParticipation from "./UpdateParticipation.vue"; export default { name: "ManageStudentsDialog", @@ -26,7 +26,6 @@ export default { TardinessChip, ExtraMarkChip, AbsenceReasonChip, - AbsenceReasonGroupSelect, AbsenceReasonButtons, PersonalNotes, PersonalNoteChip, @@ -36,6 +35,7 @@ export default { SecondaryActionButton, SlideIterator, TardinessField, + UpdateParticipation, DialogCloseButton, }, mixins: [updateParticipationMixin, deepSearchMixin], @@ -117,6 +117,11 @@ export default { this.$once("save", this.activateFullDayDialog); } }, + afterInnerSendToServer(_participations, field, value) { + if (field === "absenceReason" && value !== "present") { + this.$refs.editor.$once("save", this.activateFullDayDialog); + } + }, markAsAbsentDayClick() { this.markAsAbsentDay.loading = true; @@ -309,20 +314,14 @@ export default { </v-tooltip> </v-card-title> <v-card-text> - <absence-reason-group-select - allow-empty - :load-selected-chip="loading" - :value="item.absenceReason?.id || 'present'" - :custom-absence-reasons="absenceReasons" - @input="sendToServer([item], 'absenceReason', $event)" - /> - <tardiness-field + <update-participation + ref="editor" v-bind="documentationPartProps" - :loading="loading" - :disabled="loading" - :participations="[item]" - :value="item.tardiness" - @input="sendToServer([item], 'tardiness', $event)" + :participation="item" + :force-loading="loading" + @beforeSendToServer="beforeSendToServer" + @duringSendToServer="duringUpdateSendToServer" + @afterSendToServer="afterInnerSendToServer" /> </v-card-text> <v-divider /> @@ -368,5 +367,3 @@ export default { </template> </mobile-fullscreen-dialog> </template> - -<style scoped></style> diff --git a/aleksis/apps/alsijil/frontend/components/coursebook/absences/TardinessField.vue b/aleksis/apps/alsijil/frontend/components/coursebook/absences/TardinessField.vue index 774decab0a88867af0fdb510ac8813edefec9a14..a2700420cf674a450a617debbeeff1056a001238 100644 --- a/aleksis/apps/alsijil/frontend/components/coursebook/absences/TardinessField.vue +++ b/aleksis/apps/alsijil/frontend/components/coursebook/absences/TardinessField.vue @@ -81,6 +81,10 @@ export default { this.saveValue(this.previousValue); }, processValueObjectOptional(value) { + if (value === null || value === undefined) { + return 0; + } + if (Object.hasOwn(value, "value")) { return value.value; } @@ -159,7 +163,7 @@ export default { {{ $t("alsijil.personal_notes.confirm_delete_tardiness", { tardiness: previousValue, - name: participations.map((p) => p.person.firstName).join(", "), + name: participations.map((p) => p.person?.firstName).join(", "), }) }} </template> diff --git a/aleksis/apps/alsijil/frontend/components/coursebook/absences/UpdateParticipation.vue b/aleksis/apps/alsijil/frontend/components/coursebook/absences/UpdateParticipation.vue new file mode 100644 index 0000000000000000000000000000000000000000..7bdbf627fbb977abf0824e0466a9885cd2db0ebf --- /dev/null +++ b/aleksis/apps/alsijil/frontend/components/coursebook/absences/UpdateParticipation.vue @@ -0,0 +1,65 @@ +<template> + <div> + <absence-reason-group-select + class="mb-2" + allow-empty + :load-selected-chip="loadingIndicator" + :value="participation.absenceReason?.id || 'present'" + :custom-absence-reasons="absenceReasons" + @input="sendToServer([participation], 'absenceReason', $event)" + /> + <tardiness-field + v-bind="documentationPartProps" + :loading="loadingIndicator" + :disabled="loadingIndicator" + :participations="[participation]" + :value="participation.tardiness" + @input="sendToServer([participation], 'tardiness', $event)" + /> + </div> +</template> +<script> +import AbsenceReasonGroupSelect from "aleksis.apps.kolego/components/AbsenceReasonGroupSelect.vue"; +import TardinessField from "./TardinessField.vue"; +import updateParticipationMixin from "./updateParticipationMixin"; + +export default { + name: "UpdateParticipation", + mixins: [updateParticipationMixin], + components: { AbsenceReasonGroupSelect, TardinessField }, + props: { + participation: { + type: Object, + required: true, + }, + forceLoading: { + type: Boolean, + required: false, + default: false, + }, + }, + emits: ["beforeSendToServer", "duringSendToServer", "afterSendToServer"], + methods: { + beforeSendToServer() { + this.$emit("beforeSendToServer"); + }, + duringUpdateSendToServer(participations, field, value, incomingStatuses) { + this.$emit( + "duringSendToServer", + participations, + field, + value, + incomingStatuses, + ); + }, + afterSendToServer(participations, field, value) { + this.$emit("afterSendToServer", participations, field, value); + }, + }, + computed: { + loadingIndicator() { + return this.loading || this.forceLoading; + }, + }, +}; +</script> diff --git a/aleksis/apps/alsijil/frontend/components/coursebook/absences/UpdateParticipations.vue b/aleksis/apps/alsijil/frontend/components/coursebook/absences/UpdateParticipations.vue new file mode 100644 index 0000000000000000000000000000000000000000..1055c424275596d9f26fa87821434fb09abae630 --- /dev/null +++ b/aleksis/apps/alsijil/frontend/components/coursebook/absences/UpdateParticipations.vue @@ -0,0 +1,44 @@ +<script> +import AbsenceReasonButtons from "aleksis.apps.kolego/components/AbsenceReasonButtons.vue"; +import updateParticipationMixin from "./updateParticipationMixin"; + +export default { + name: "UpdateParticipations", + components: { + AbsenceReasonButtons, + }, + mixins: [updateParticipationMixin], + props: { + participationStatuses: { + type: Array, + required: true, + }, + absenceReasons: { + type: Array, + required: true, + }, + }, + emits: ["update:participationStatuses"], + methods: { + afterSendToServer() { + this.$once("save", () => this.$emit("update:participationStatuses", [])); + }, + }, +}; +</script> + +<template> + <v-card> + <v-card-text> + <h4>{{ $t("alsijil.coursebook.participation_status") }}</h4> + <absence-reason-buttons + allow-empty + empty-value="present" + :custom-absence-reasons="absenceReasons" + @input="sendToServer(participationStatuses, 'absenceReason', $event)" + /> + </v-card-text> + </v-card> +</template> + +<style scoped></style> diff --git a/aleksis/apps/alsijil/frontend/components/coursebook/personal_notes/DeleteExtraMarkPersonalNote.vue b/aleksis/apps/alsijil/frontend/components/coursebook/personal_notes/DeleteExtraMarkPersonalNote.vue new file mode 100644 index 0000000000000000000000000000000000000000..7666c60f4fad325c2af92edb90ae5154ab20e1fb --- /dev/null +++ b/aleksis/apps/alsijil/frontend/components/coursebook/personal_notes/DeleteExtraMarkPersonalNote.vue @@ -0,0 +1,70 @@ +<script> +import { deletePersonalNotes } from "./personal_notes.graphql"; +import ConfirmDialog from "aleksis.core/components/generic/dialogs/ConfirmDialog.vue"; +import mutateMixin from "aleksis.core/mixins/mutateMixin.js"; +import personalNoteRelatedMixin from "./personalNoteRelatedMixin"; + +export default { + name: "DeleteAssignedExtraMark", + components: { + ConfirmDialog, + }, + mixins: [mutateMixin, personalNoteRelatedMixin], + props: { + personalNote: { + type: Object, + required: true, + }, + person: { + type: Object, + required: true, + }, + }, + data() { + return { + showDeleteConfirm: false, + }; + }, + methods: { + deleteNote() { + this.mutate( + deletePersonalNotes, + { + ids: [this.personalNote.id], + }, + (storedPersonalNotes) => { + const index = storedPersonalNotes.findIndex( + (n) => n.id === this.personalNote.id, + ); + storedPersonalNotes.splice(index, 1); + + return storedPersonalNotes; + }, + ); + }, + }, +}; +</script> + +<template> + <v-btn color="error" icon @click.prevent.stop="showDeleteConfirm = true"> + <v-icon color="error">$deleteContent</v-icon> + <confirm-dialog + v-model="showDeleteConfirm" + @confirm="deleteNote" + @cancel="showDeleteConfirm = false" + > + <template #title> + {{ $t("alsijil.personal_notes.confirm_delete") }} + </template> + <template #text> + {{ + $t("alsijil.personal_notes.confirm_delete_extra_mark", { + extraMark: personalNote.extraMark.name, + name: person.firstName || person.fullName, + }) + }} + </template> + </confirm-dialog> + </v-btn> +</template> diff --git a/aleksis/apps/alsijil/frontend/components/coursebook/personal_notes/TextNote.vue b/aleksis/apps/alsijil/frontend/components/coursebook/personal_notes/TextNote.vue index 43175c7402100c5ef9e4b84c153a8e79a9bb7f76..f81bb0e2b1e27ae95f2e46db873d01f6b97eac53 100644 --- a/aleksis/apps/alsijil/frontend/components/coursebook/personal_notes/TextNote.vue +++ b/aleksis/apps/alsijil/frontend/components/coursebook/personal_notes/TextNote.vue @@ -17,6 +17,11 @@ export default { type: Object, required: true, }, + person: { + type: Object, + required: false, + default: () => ({ fullName: null }), + }, }, computed: { model: { @@ -148,7 +153,7 @@ export default { {{ $t("alsijil.personal_notes.confirm_delete_explanation", { note: value.note, - name: participation.person.fullName, + name: (participation?.person || person).fullName, }) }} </template> diff --git a/aleksis/apps/alsijil/frontend/components/coursebook/statistics/StatisticsForGroupTab.vue b/aleksis/apps/alsijil/frontend/components/coursebook/statistics/StatisticsForGroupTab.vue index c8bde2e8bed22f4120dec5108f8f2f137bdfefe9..8f7d5b428da74d4bc3dc438dea0e0140678378cd 100644 --- a/aleksis/apps/alsijil/frontend/components/coursebook/statistics/StatisticsForGroupTab.vue +++ b/aleksis/apps/alsijil/frontend/components/coursebook/statistics/StatisticsForGroupTab.vue @@ -62,7 +62,7 @@ <template #actions="{ item }"> <secondary-action-button - i18n-key="alsijil.personal_notes.statistics.person_view_details" + i18n-key="alsijil.coursebook.statistics.person_view_details" icon-text="mdi-open-in-new" :to="{ name: 'alsijil.coursebook_statistics', diff --git a/aleksis/apps/alsijil/frontend/components/coursebook/statistics/StatisticsForPersonPage.vue b/aleksis/apps/alsijil/frontend/components/coursebook/statistics/StatisticsForPersonPage.vue index c5a8ed51e2d89dbdd79b17e7b839141f2452a9cb..684e5dacc774e5891f771bd60da4cc39259edf17 100644 --- a/aleksis/apps/alsijil/frontend/components/coursebook/statistics/StatisticsForPersonPage.vue +++ b/aleksis/apps/alsijil/frontend/components/coursebook/statistics/StatisticsForPersonPage.vue @@ -12,6 +12,7 @@ :enable-create="false" :enable-edit="false" :elevated="false" + @lastQuery="lastQuery = $event" > <template #additionalActions> <v-btn-toggle @@ -37,91 +38,187 @@ text @click="statisticsBottomSheet = !statisticsBottomSheet" > - {{ $t("alsijil.personal_notes.statistics.person_page.summary") }} + {{ $t("alsijil.coursebook.statistics.person_page.summary") }} </v-btn> </template> <template #default="{ items }"> - <v-list> - <v-list-item v-for="item in items" :key="item.id" ripple> - <v-list-item-content> - <v-list-item-title> - <!-- date & timeslot --> - <time - :datetime="item.relatedDocumentation.datetimeStart" - class="text-no-wrap" - > - {{ - $d( - $parseISODate( - item.relatedDocumentation.datetimeStart, - ), - "short", - ) - }} - </time> + <v-expand-transition> + <update-participations + v-show="selected.length > 0" + class="mt-2" + :subjects="[]" + :absence-reasons="absenceReasons" + :participation-statuses.sync="selected" + :extra-marks="[]" + :affected-query="lastQuery" + :documentation="{}" + /> + </v-expand-transition> - <time - :datetime="item.relatedDocumentation.datetimeStart" - class="text-no-wrap" - > - {{ - $d( - $parseISODate( - item.relatedDocumentation.datetimeStart, - ), - "shortTime", - ) - }} - </time> - <span>-</span> - <time - :datetime="item.relatedDocumentation.datetimeEnd" - class="text-no-wrap" - > - {{ - $d( - $parseISODate(item.relatedDocumentation.datetimeEnd), - "shortTime", - ) - }} - </time> - </v-list-item-title> - <v-list-item-subtitle class="overflow-scroll"> - <!-- teacher --> - <person-chip - v-for="teacher in item.relatedDocumentation.teachers" - :key="teacher.id" - :person="teacher" - no-link - small - /> - <!-- group --> - <span> - {{ item.groupShortName }} - </span> - <!-- subject --> - <subject-chip - :subject="item.relatedDocumentation.subject" - small - /> - </v-list-item-subtitle> - </v-list-item-content> - <v-list-item-action> - <!-- chips: absences & extraMarks --> - <absence-reason-chip - v-if="item.absenceReason" - :absence-reason="item.absenceReason" - /> - <extra-mark-chip - v-if="item.extraMark" - :extra-mark="item.extraMark" - /> - <div v-if="item.note"> - {{ item.note }} - </div> - </v-list-item-action> - </v-list-item> - </v-list> + <v-item-group multiple v-model="selected" class="mt-2"> + <v-expansion-panels focusable> + <v-expansion-panel + v-for="item in items" + :key="item.id" + ripple + :readonly="!showEdit(item)" + > + <v-expansion-panel-header + :hide-actions="!showEdit(item) && !showDelete(item)" + disable-icon-rotate + > + <template #actions> + <v-btn v-if="showEdit(item)" color="primary" icon> + <v-icon> $edit </v-icon> + </v-btn> + <delete-assigned-extra-mark + v-if="showDelete(item)" + :personal-note="item" + :participation="item.participation || {}" + :subjects="[]" + :absence-reasons="[]" + :extra-marks="[]" + :affected-query="lastQuery" + :documentation="item.relatedDocumentation" + :person="personName" + /> + </template> + <v-row class="mr-2"> + <v-col cols="12" md="6" class="pa-0 d-flex"> + <v-list-item-avatar + v-if=" + mode === MODE.PARTICIPATIONS && + !$vuetify.breakpoint.mobile + " + > + <v-item v-slot="{ active, toggle }" :value="item.id"> + <v-simple-checkbox + :value="active" + @click="toggle" + /> + </v-item> + </v-list-item-avatar> + <v-list-item-content> + <v-list-item-title> + <!-- date & timeslot --> + <time + :datetime=" + item.relatedDocumentation.datetimeStart + " + class="text-no-wrap" + > + {{ + $d( + $parseISODate( + item.relatedDocumentation.datetimeStart, + ), + "short", + ) + }} + </time> + + <time + :datetime=" + item.relatedDocumentation.datetimeStart + " + class="text-no-wrap" + > + {{ + $d( + $parseISODate( + item.relatedDocumentation.datetimeStart, + ), + "shortTime", + ) + }} + </time> + <span>-</span> + <time + :datetime="item.relatedDocumentation.datetimeEnd" + class="text-no-wrap" + > + {{ + $d( + $parseISODate( + item.relatedDocumentation.datetimeEnd, + ), + "shortTime", + ) + }} + </time> + </v-list-item-title> + <v-list-item-subtitle class="overflow-scroll"> + <!-- teacher --> + <person-chip + v-for="teacher in item.relatedDocumentation + .teachers" + :key="teacher.id" + :person="teacher" + no-link + small + /> + <!-- group --> + <span> + {{ item.groupShortName }} + </span> + <!-- subject --> + <subject-chip + :subject="item.relatedDocumentation.subject" + small + /> + </v-list-item-subtitle> + </v-list-item-content> + </v-col> + <v-col cols="12" md="6" class="pa-0"> + <v-list-item-action + class="flex-row full-width justify-md-end ma-0 align-center fill-height" + > + <!-- chips: absences & extraMarks --> + <absence-reason-chip + v-if="item.absenceReason" + :absence-reason="item.absenceReason" + /> + <tardiness-chip + v-if="item.tardiness" + :tardiness="item.tardiness" + class="ms-1" + /> + <extra-mark-chip + v-if="item.extraMark" + :extra-mark="item.extraMark" + /> + <personal-note-chip v-if="item.note" :note="item" /> + </v-list-item-action> + </v-col> + </v-row> + </v-expansion-panel-header> + <v-expansion-panel-content> + <v-card-text class="pb-0"> + <text-note + v-if="item.note" + :value="item" + :participation="{}" + :person="personName" + :subjects="[]" + :absence-reasons="absenceReasons" + :extra-marks="[]" + :affected-query="lastQuery" + :documentation="item.relatedDocumentation" + /> + <update-participation + v-else + :participation="item" + :subjects="[]" + :absence-reasons="absenceReasons" + :extra-marks="[]" + :affected-query="lastQuery" + :documentation="item.relatedDocumentation" + /> + </v-card-text> + </v-expansion-panel-content> + </v-expansion-panel> + </v-expansion-panels> + </v-item-group> <v-divider></v-divider> </template> </c-r-u-d-iterator> @@ -129,6 +226,7 @@ <statistics-for-person-card v-if="!$vuetify.breakpoint.mobile" class="flex-shrink-1" + style="min-width: 15vw" :compact="false" :person="{ id: personId }" /> @@ -155,6 +253,7 @@ </template> <script> +import { absenceReasons } from "../queries/absenceReasons.graphql"; import AbsenceReasonChip from "aleksis.apps.kolego/components/AbsenceReasonChip.vue"; import ActiveSchoolTermSelect from "aleksis.core/components/school_term/ActiveSchoolTermSelect.vue"; import CRUDIterator from "aleksis.core/components/generic/CRUDIterator.vue"; @@ -171,10 +270,22 @@ import { } from "./statistics.graphql"; import ExtraMarkChip from "../../extra_marks/ExtraMarkChip.vue"; import { MODE } from "./modes.js"; +import PersonalNoteChip from "../personal_notes/PersonalNoteChip.vue"; +import TextNote from "../personal_notes/TextNote.vue"; +import UpdateParticipation from "../absences/UpdateParticipation.vue"; +import TardinessChip from "../absences/TardinessChip.vue"; +import DeleteAssignedExtraMark from "../personal_notes/DeleteExtraMarkPersonalNote.vue"; +import UpdateParticipations from "../absences/UpdateParticipations.vue"; export default { name: "StatisticsForPersonPage", components: { + UpdateParticipations, + DeleteAssignedExtraMark, + TardinessChip, + UpdateParticipation, + TextNote, + PersonalNoteChip, ActiveSchoolTermSelect, ExtraMarkChip, AbsenceReasonChip, @@ -213,10 +324,18 @@ export default { ); }, }, + absenceReasons: { + query: absenceReasons, + update: (data) => data.items, + }, }, data() { return { + personName: {}, statisticsBottomSheet: false, + lastQuery: null, + absenceReasons: [], + selected: [], }; }, computed: { @@ -240,6 +359,8 @@ export default { return; } + this.selected = []; + this.$router.push({ name: "alsijil.coursebook_statistics", params: { @@ -248,6 +369,16 @@ export default { }, }); }, + showEdit(item) { + // Notes with ExtraMark cannot be edited, only deleted + return ( + item.canEdit && (item.note || item.absenceReason || item.tardiness) + ); + }, + showDelete(item) { + // Only ExtraMarks can be deleted + return item.canDelete && item.extraMark; + }, }, }; </script> diff --git a/aleksis/apps/alsijil/frontend/components/coursebook/statistics/statistics.graphql b/aleksis/apps/alsijil/frontend/components/coursebook/statistics/statistics.graphql index 901bcd251829c87783bd432ad90d69a07c2cca65..1cffdc7178defa256f5f9e46707541a526cb1dec 100644 --- a/aleksis/apps/alsijil/frontend/components/coursebook/statistics/statistics.graphql +++ b/aleksis/apps/alsijil/frontend/components/coursebook/statistics/statistics.graphql @@ -65,6 +65,8 @@ query participationsOfPerson($person: ID!) { colourBg } } + canEdit + canDelete } } @@ -98,6 +100,8 @@ query personalNotesForPerson($person: ID!) { colourBg } } + canEdit + canDelete } } diff --git a/aleksis/apps/alsijil/frontend/messages/de.json b/aleksis/apps/alsijil/frontend/messages/de.json index 872b2e07510f802eebe988b4adda0141f404391f..8a1c9997337f613fe285a18ea5517eac65daab8a 100644 --- a/aleksis/apps/alsijil/frontend/messages/de.json +++ b/aleksis/apps/alsijil/frontend/messages/de.json @@ -148,6 +148,7 @@ }, "confirm_delete": "Anmerkung wirklich löschen?", "confirm_delete_explanation": "Die Notiz \"{note}\" für {name} wird entfernt.", + "confirm_delete_extra_mark": "Die Markierung {extraMark} für {name} wird entfernt.", "confirm_delete_tardiness": "Die Verspätung von {name} in Höhe von {tardiness} Minuten wird entfernt.", "create_personal_note": "Weitere Notiz", "late": "Verspätet", diff --git a/aleksis/apps/alsijil/frontend/messages/en.json b/aleksis/apps/alsijil/frontend/messages/en.json index 2294349f2f1a42241e4017865bcd30fda40ee919..a40f8d5475a9c3e9843e4a72ac745c723a1137e3 100644 --- a/aleksis/apps/alsijil/frontend/messages/en.json +++ b/aleksis/apps/alsijil/frontend/messages/en.json @@ -81,8 +81,11 @@ "title": "Coursebook · Statistics" }, "person_page": { - "title": "Statistics" - } + "title": "Coursebook · Statistics · {fullName}", + "summary": "Summary" + }, + "person_view_details": "Details", + "title_plural": "Statistics" }, "notes": { "show_list": "List of participants", @@ -160,18 +163,8 @@ "confirm_delete": "Delete note?", "confirm_delete_explanation": "The note \"{note}\" for {name} will be removed.", "confirm_delete_tardiness": "The tardiness of {tardiness} minutes will be removed for {name}.", + "confirm_delete_extra_mark": "The mark {extraMark} for {name} will be removed.", "no_results": "No search results for {search}", - "statistics": { - "person_compact": { - "title": "Coursebook · Statistics" - }, - "person_page": { - "title": "Coursebook · Statistics · {fullName}", - "summary": "Summary" - }, - "person_view_details": "Details", - "title_plural": "Statistics" - }, "personal_notes": "Personal Notes" } }, diff --git a/aleksis/apps/alsijil/schema/__init__.py b/aleksis/apps/alsijil/schema/__init__.py index 604cda6cd229533086533d855563a892f440d572..284d8da2d3a2570e57ae902b14302048eba7f3b8 100644 --- a/aleksis/apps/alsijil/schema/__init__.py +++ b/aleksis/apps/alsijil/schema/__init__.py @@ -309,8 +309,8 @@ class Query(graphene.ObjectType): school_term = get_active_school_term(info.context) return graphene_django_optimizer.query( ParticipationStatus.objects.filter( + Q(absence_reason__isnull=False) | Q(tardiness__isnull=False), person=person, - absence_reason__isnull=False, datetime_start__date__gte=school_term.date_start, datetime_end__date__lte=school_term.date_end, ).order_by("-related_documentation__datetime_start"),