Skip to content
Snippets Groups Projects

Compare revisions

Changes are shown as if the source revision was being merged into the target revision. Learn more about comparing revisions.

Source

Select target project
No results found

Target

Select target project
  • AlekSIS/official/AlekSIS-App-Alsijil
  • sunweaver/AlekSIS-App-Alsijil
  • 8tincsoVluke/AlekSIS-App-Alsijil
  • perfreicpo/AlekSIS-App-Alsijil
  • noifobarep/AlekSIS-App-Alsijil
  • 7ingannisdo/AlekSIS-App-Alsijil
  • unmruntartpa/AlekSIS-App-Alsijil
  • balrorebta/AlekSIS-App-Alsijil
  • comliFdifwa/AlekSIS-App-Alsijil
  • 3ranaadza/AlekSIS-App-Alsijil
10 results
Show changes
Commits on Source (37)
Showing
with 590 additions and 139 deletions
<script>
import AbsenceReasonChip from "aleksis.apps.kolego/components/AbsenceReasonChip.vue";
import AbsenceReasonGroupSelect from "aleksis.apps.kolego/components/AbsenceReasonGroupSelect.vue";
import CancelButton from "aleksis.core/components/generic/buttons/CancelButton.vue";
import MobileFullscreenDialog from "aleksis.core/components/generic/dialogs/MobileFullscreenDialog.vue";
import mutateMixin from "aleksis.core/mixins/mutateMixin.js";
import documentationPartMixin from "../documentation/documentationPartMixin";
import LessonInformation from "../documentation/LessonInformation.vue";
import updateParticipationStatuses from "./participationStatus.graphql";
import SlideIterator from "aleksis.core/components/generic/SlideIterator.vue";
export default {
name: "ManageStudentsDialog",
extends: MobileFullscreenDialog,
components: {
AbsenceReasonChip,
AbsenceReasonGroupSelect,
CancelButton,
LessonInformation,
MobileFullscreenDialog,
SlideIterator,
},
mixins: [documentationPartMixin, mutateMixin],
data() {
return {
dialog: false,
search: "",
selected: [],
isExpanded: false,
};
},
computed: {
items() {
return this.documentation.participations;
},
},
methods: {
sendToServer(participation, field, value) {
if (field !== "absenceReason") return;
this.mutate(
updateParticipationStatuses,
{
input: [
{
id: participation.id,
absenceReason: value === "present" ? null : value,
},
],
},
(storedDocumentations, incomingStatuses) => {
const newStatus = incomingStatuses[0];
const documentation = storedDocumentations.find(
(doc) => doc.id === newStatus.relatedDocumentation.id,
);
const participationStatus = documentation.participations.find(
(part) => part.id === newStatus.id,
);
participationStatus.absenceReason = newStatus.absenceReason;
participationStatus.isOptimistic = newStatus.isOptimistic;
return storedDocumentations;
},
// {
// optimisticResponse: {
// updateParticipationStatuses: {
// items: [
// {
// id: participation.id,
// isOptimistic: true,
// relatedDocumentation: {
// id: this.documentation.id,
// __typename: "DocumentationType",
// },
// absenceReason: value === "present" ? null : {
// id: value,
// name: "",
// shortName: "",
// __typename: "AbsenceReasonType",
// },
// __typename: "ParticipationStatusType",
// },
// ],
// __typename: "ParticipationStatusBatchPatchMutation",
// },
// },
// },
);
},
},
};
</script>
<template>
<mobile-fullscreen-dialog
scrollable
v-bind="$attrs"
v-on="$listeners"
v-model="dialog"
>
<template #activator="activator">
<slot name="activator" v-bind="activator" />
</template>
<template #title>
<lesson-information v-bind="documentationPartProps" />
<v-slide-x-transition leave-absolute>
<v-text-field
v-show="!isExpanded"
type="search"
v-model="search"
clearable
rounded
filled
hide-details
single-line
prepend-inner-icon="$search"
dense
outlined
:placeholder="$t('actions.search')"
class="pt-4"
/>
</v-slide-x-transition>
</template>
<template #content>
<slide-iterator
v-model="selected"
:items="items"
:search="search"
:item-key-getter="
(item) => 'documentation-' + documentation.id + '-student-' + item.id
"
:is-expanded.sync="isExpanded"
>
<template #listItemContent="{ item }">
<v-list-item-title>
{{ item.person.fullName }}
</v-list-item-title>
<v-list-item-subtitle v-if="item.absenceReason">
<absence-reason-chip dense :absence-reason="item.absenceReason" />
</v-list-item-subtitle>
</template>
<template #expandedItem="{ item, close }">
<v-card-title>
<v-tooltip bottom>
<template #activator="{ on, attrs }">
<v-btn v-bind="attrs" v-on="on" icon @click="close">
<v-icon>$prev</v-icon>
</v-btn>
</template>
<span v-t="'actions.back_to_overview'" />
</v-tooltip>
{{ item.person.fullName }}
</v-card-title>
<v-card-text>
<absence-reason-group-select
allow-empty
empty-value="present"
:loadSelectedChip="loading"
:value="item.absenceReason?.id || 'present'"
@input="sendToServer(item, 'absenceReason', $event)"
/>
</v-card-text>
</template>
</slide-iterator>
</template>
<template #actions>
<cancel-button
@click="dialog = false"
i18n-key="actions.close"
v-show="$vuetify.breakpoint.mobile"
/>
</template>
</mobile-fullscreen-dialog>
</template>
<style scoped></style>
<script>
import { DateTime } from "luxon";
import ManageStudentsDialog from "./ManageStudentsDialog.vue";
import documentationPartMixin from "../documentation/documentationPartMixin";
export default {
name: "ManageStudentsTrigger",
components: { ManageStudentsDialog },
mixins: [documentationPartMixin],
data() {
return {
canOpenParticipation: false,
timeout: null,
};
},
mounted() {
const lessonStart = DateTime.fromISO(this.documentation.datetimeStart);
const now = DateTime.now();
this.canOpenParticipation = now >= lessonStart;
if (!this.canOpenParticipation) {
this.timeout = setTimeout(
() => (this.canOpenParticipation = true),
lessonStart.diff(now).toObject().milliseconds,
);
}
},
beforeDestroy() {
if (this.timeout) {
clearTimeout(this.timeout);
}
},
};
</script>
<template>
<manage-students-dialog v-bind="documentationPartProps" @update="() => null">
<template #activator="{ attrs, on }">
<v-chip
dense
color="primary"
outlined
:disabled="!canOpenParticipation"
v-bind="attrs"
v-on="on"
>
<v-icon>$edit</v-icon>
</v-chip>
</template>
</manage-students-dialog>
</template>
<style scoped></style>
mutation updateParticipationStatuses(
$input: [BatchPatchParticipationStatusInput]!
) {
updateParticipationStatuses(input: $input) {
items: participationStatuses {
id
isOptimistic
relatedDocumentation {
id
}
absenceReason {
id
name
shortName
}
}
}
}
......@@ -70,6 +70,19 @@ query documentationsForCoursebook(
colourFg
colourBg
}
participations {
id
person {
id
fullName
}
absenceReason {
id
name
shortName
}
isOptimistic
}
topic
homework
groupNote
......
......@@ -5,9 +5,9 @@
<!-- eslint-disable @intlify/vue-i18n/no-raw-text -->
<v-chip dense color="success">
<v-chip small dense class="mr-2" color="green darken-3 white--text"
>26</v-chip
>{{ documentation.participations.length }}</v-chip
>
von 30 anwesend
Schüler
</v-chip>
<v-chip dense color="warning">
<v-chip small dense class="mr-2" color="orange darken-3 white--text"
......@@ -27,18 +27,18 @@
>
Hausaufgaben vergessen
</v-chip>
<v-chip dense color="primary" outlined>
<v-icon>$edit</v-icon>
</v-chip>
<!-- eslint-enable @intlify/vue-i18n/no-raw-text -->
<manage-students-trigger v-bind="documentationPartProps" />
</div>
</template>
<script>
import documentationPartMixin from "./documentationPartMixin";
import ManageStudentsTrigger from "../absences/ManageStudentsTrigger.vue";
export default {
name: "LessonNotes",
components: { ManageStudentsTrigger },
mixins: [documentationPartMixin],
};
</script>
......
......@@ -10,6 +10,13 @@ export default {
type: Object,
required: true,
},
/**
* The query used by the coursebook. Used to update the store when data changes.
*/
affectedQuery: {
type: Object,
required: true,
},
/**
* Whether the documentation is currently in the compact mode (meaning coursebook row)
*/
......@@ -38,6 +45,7 @@ export default {
documentation: this.documentation,
compact: this.compact,
dialogActivator: this.dialogActivator,
affectedQuery: this.affectedQuery,
};
},
},
......
......@@ -77,5 +77,8 @@
"week": {
"menu_title": "Aktuelle Woche"
}
},
"actions": {
"back_to_overview": "Zurück zur Übersicht"
}
}
......@@ -77,5 +77,8 @@
"no_data": "No lessons for the selected groups and courses in this period",
"no_results": "No search results for {search}"
}
},
"actions": {
"back_to_overview": "Back to overview"
}
}
# Generated by Django 4.2.10 on 2024-04-30 11:14
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('kolego', '0003_refactor_absence'),
('alsijil', '0020_documentation_extramark_colour_bg_and_more'),
]
operations = [
migrations.RemoveField(
model_name='participationstatus',
name='absent',
),
migrations.AlterField(
model_name='participationstatus',
name='absence_reason',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, to='kolego.absencereason', verbose_name='Absence Reason'),
),
]
......@@ -2,6 +2,7 @@ from datetime import date, datetime
from typing import Optional, Union
from urllib.parse import urlparse
from django.core.exceptions import PermissionDenied
from django.db import models
from django.db.models import QuerySet
from django.db.models.constraints import CheckConstraint
......@@ -9,7 +10,9 @@ from django.db.models.query_utils import Q
from django.http import HttpRequest
from django.urls import reverse
from django.utils.formats import date_format
from django.utils.timezone import localdate, localtime
from django.utils.translation import gettext_lazy as _
from django.contrib.auth.models import User
from calendarweek import CalendarWeek
from colorfield.fields import ColorField
......@@ -515,44 +518,17 @@ class Documentation(CalendarEvent):
# which is not possible via constraint, because amends is not local to Documentation
@classmethod
def get_for_coursebook(
cls,
own: bool,
date_start: datetime,
date_end: datetime,
request: HttpRequest,
obj_type: Optional[str] = None,
obj_id: Optional[str] = None,
incomplete: Optional[bool] = False,
) -> list:
"""Get all the documentations for an object and a time frame.
obj_type may be one of TEACHER, GROUP, ROOM, COURSE
def get_documentations_for_events(
cls,
events: list,
incomplete: Optional[bool] = False,
) -> tuple:
"""Get all the documentations for the events.
Create dummy documentations if none exist.
Returns a tuple with a list of existing documentations and a list dummy documentations.
"""
# 1. Find all LessonEvents for all Lessons of this Course in this date range
event_params = {
"own": own,
}
if obj_type is not None and obj_id is not None:
event_params.update(
{
"type": obj_type,
"id": obj_id,
}
)
events = LessonEvent.get_single_events(
date_start,
date_end,
request,
event_params,
with_reference_object=True,
)
# 2. For each lessonEvent → check if there is a documentation
# if so, add the documentation to a list, if not, create a new one
docs = []
dummies = []
for event in events:
if incomplete and event["STATUS"] == "CANCELLED":
continue
......@@ -582,7 +558,7 @@ class Documentation(CalendarEvent):
else:
course, subject = event_reference_obj.course, event_reference_obj.subject
docs.append(
dummies.append(
cls(
pk=f"DUMMY;{event_reference_obj.id};{event['DTSTART'].dt.isoformat()};{event['DTEND'].dt.isoformat()}",
amends=event_reference_obj,
......@@ -593,7 +569,144 @@ class Documentation(CalendarEvent):
)
)
return docs
return (docs, dummies)
@classmethod
def get_documentations_for_person(
cls,
person: int,
start: datetime,
end: datetime,
incomplete: Optional[bool] = False,
) -> tuple:
"""Get all the documentations for the person from start to end datetime.
Create dummy documentations if none exist.
Returns a tuple with a list of existing documentations and a list dummy documentations.
"""
event_params = {
"type": "PARTICIPANT",
"obj_id": person,
}
events = LessonEvent.get_single_events(
start,
end,
None,
event_params,
with_reference_object=True,
)
return Documentation.get_documentations_for_events(events, incomplete)
@classmethod
def parse_dummy(
cls,
_id: str,
) -> tuple:
"""Parse dummy id string into lesson_event, datetime_start, datetime_end.
"""
dummy, lesson_event_id, datetime_start_iso, datetime_end_iso = _id.split(";")
lesson_event = LessonEvent.objects.get(id=lesson_event_id)
datetime_start = datetime.fromisoformat(datetime_start_iso).astimezone(
lesson_event.timezone
)
datetime_end = datetime.fromisoformat(datetime_end_iso).astimezone(
lesson_event.timezone
)
return (lesson_event, datetime_start, datetime_end)
@classmethod
def create_from_lesson_event(
cls,
user: User,
lesson_event: LessonEvent,
datetime_start: datetime,
datetime_end: datetime,
) -> "Documentation":
""" Create a documentation from a lesson_event with start and end datetime.
User is needed for permission checking.
"""
if not user.has_perm(
"alsijil.add_documentation_for_lesson_event_rule", lesson_event
) or not (
get_site_preferences()["alsijil__allow_edit_future_documentations"] == "all"
or (
get_site_preferences()["alsijil__allow_edit_future_documentations"]
== "current_day"
and datetime_start.date() <= localdate()
)
or (
get_site_preferences()["alsijil__allow_edit_future_documentations"]
== "current_time"
and datetime_start <= localtime()
)
):
raise PermissionDenied()
if lesson_event.amends:
if lesson_event.course:
course = lesson_event.course
else:
course = lesson_event.amends.course
if lesson_event.subject:
subject = lesson_event.subject
else:
subject = lesson_event.amends.subject
if lesson_event.teachers:
teachers = lesson_event.teachers
else:
teachers = lesson_event.amends.teachers
else:
course, subject, teachers = (
lesson_event.course,
lesson_event.subject,
lesson_event.teachers,
)
obj = cls.objects.create(
datetime_start=datetime_start,
datetime_end=datetime_end,
amends=lesson_event,
course=course,
subject=subject,
topic="",
homework="",
group_note="",
)
obj.teachers.set(teachers.all())
obj.save()
# Create Participation Statuses
# Cannot use djangos bulk_create method, as then the save method of the
# superclass wouldn't be called
for member in lesson_event.all_members:
# TODO: Check for preexisting absences in kolego
# TODO: maybe only create if the lesson start is in the past
status = ParticipationStatus.objects.create(
person=member,
related_documentation=obj,
datetime_start=datetime_start,
datetime_end=datetime_end,
timezone=lesson_event.timezone,
)
status.groups_of_person.set(member.member_of.all())
status.save()
return obj
@classmethod
def get_or_create_by_id(cls, _id: str|int, user):
if _id.startswith("DUMMY"):
return cls.create_from_lesson_event(
user,
*cls.parse_dummy(_id),
)
return cls.objects.get(id=_id)
class ParticipationStatus(CalendarEvent):
......@@ -620,9 +733,12 @@ class ParticipationStatus(CalendarEvent):
)
# Absence part
absent = models.BooleanField(verbose_name=_("Absent"))
absence_reason = models.ForeignKey(
AbsenceReason, verbose_name=_("Absence Reason"), on_delete=models.PROTECT
AbsenceReason,
verbose_name=_("Absence Reason"),
on_delete=models.PROTECT,
blank=True,
null=True,
)
base_absence = models.ForeignKey(
......
......@@ -11,12 +11,15 @@ from aleksis.core.models import Group, Person
from aleksis.core.schema.base import FilterOrderList
from aleksis.core.schema.group import GroupType
from aleksis.core.util.core_helpers import has_person
from aleksis.apps.chronos.models import LessonEvent
from ..models import Documentation
from .documentation import (
DocumentationBatchCreateOrUpdateMutation,
DocumentationType,
LessonsForPersonType,
)
from .participation_status import ParticipationStatusBatchPatchMutation
class Query(graphene.ObjectType):
......@@ -37,6 +40,13 @@ class Query(graphene.ObjectType):
groups_by_person = FilterOrderList(GroupType, person=graphene.ID())
courses_of_person = FilterOrderList(CourseType, person=graphene.ID())
lessons_for_persons = graphene.List(
LessonsForPersonType,
persons=graphene.List(graphene.ID, required=True),
start=graphene.Date(required=True),
end=graphene.Date(required=True),
)
def resolve_documentations_by_course_id(root, info, course_id, **kwargs):
documentations = Documentation.objects.filter(
Q(course__pk=course_id) | Q(amends__course__pk=course_id)
......@@ -54,9 +64,6 @@ class Query(graphene.ObjectType):
incomplete=False,
**kwargs,
):
datetime_start = datetime.combine(date_start, datetime.min.time())
datetime_end = datetime.combine(date_end, datetime.max.time())
if (
(
obj_type == "COURSE"
......@@ -79,10 +86,30 @@ class Query(graphene.ObjectType):
):
raise PermissionDenied()
return Documentation.get_for_coursebook(
own, datetime_start, datetime_end, info.context, obj_type, obj_id, incomplete
# Find all LessonEvents for all Lessons of this Course in this date range
event_params = {
"own": own,
}
if obj_type is not None and obj_id is not None:
event_params.update(
{
"type": obj_type,
"id": obj_id,
}
)
events = LessonEvent.get_single_events(
datetime.combine(date_start, datetime.min.time()),
datetime.combine(date_end, datetime.max.time()),
info.context,
event_params,
with_reference_object=True,
)
# Lookup or create documentations and return them all.
docs, dummies = Documentation.get_documentations_for_events(events, incomplete)
return docs + dummies
@staticmethod
def resolve_groups_by_person(root, info, person=None):
if person:
......@@ -116,6 +143,33 @@ class Query(graphene.ObjectType):
| Q(groups__parent_groups__owners=person)
)
@staticmethod
def resolve_lessons_for_persons(
root,
info,
persons,
start,
end,
**kwargs,
):
"""Resolve all lesson events for each person in timeframe start to end.
"""
lessons_for_person = []
for person in persons:
docs, dummies = Documentation.get_documentations_for_person(
person,
datetime.combine(start, datetime.min.time()),
datetime.combine(end, datetime.max.time()),
)
lessons_for_person.append(
id=person,
lessons=docs + dummies
)
return lessons_for_person
class Mutation(graphene.ObjectType):
create_or_update_documentations = DocumentationBatchCreateOrUpdateMutation.Field()
update_participation_statuses = ParticipationStatusBatchPatchMutation.Field()
from datetime import datetime
from django.core.exceptions import PermissionDenied
from django.utils.timezone import localdate, localtime
import graphene
from graphene_django.types import DjangoObjectType
......@@ -9,7 +6,6 @@ from guardian.shortcuts import get_objects_for_user
from reversion import create_revision, set_comment, set_user
from aleksis.apps.alsijil.util.predicates import can_edit_documentation, is_in_allowed_time_range
from aleksis.apps.chronos.models import LessonEvent
from aleksis.apps.chronos.schema import LessonEventType
from aleksis.apps.cursus.models import Subject
from aleksis.apps.cursus.schema import CourseType, SubjectType
......@@ -18,9 +14,9 @@ from aleksis.core.schema.base import (
DjangoFilterMixin,
PermissionsTypeMixin,
)
from aleksis.core.util.core_helpers import get_site_preferences
from ..models import Documentation
from .participation_status import ParticipationStatusType
class DocumentationType(PermissionsTypeMixin, DjangoFilterMixin, DjangoObjectType):
......@@ -39,6 +35,7 @@ class DocumentationType(PermissionsTypeMixin, DjangoFilterMixin, DjangoObjectTyp
"date_start",
"date_end",
"teachers",
"participations",
)
filter_fields = {
"id": ["exact", "lte", "gte"],
......@@ -48,6 +45,7 @@ class DocumentationType(PermissionsTypeMixin, DjangoFilterMixin, DjangoObjectTyp
course = graphene.Field(CourseType, required=False)
amends = graphene.Field(lambda: LessonEventType, required=False)
subject = graphene.Field(SubjectType, required=False)
participations = graphene.List(ParticipationStatusType, required=False)
future_notice = graphene.Boolean(required=False)
......@@ -71,6 +69,15 @@ class DocumentationType(PermissionsTypeMixin, DjangoFilterMixin, DjangoObjectTyp
info.context.user, root
)
@staticmethod
def resolve_participations(root: Documentation, info, **kwargs):
# TODO: probably check for permission
# A dummy documentation will not have any participations
if str(root.pk).startswith("DUMMY") or not hasattr(root, "participations"):
return []
return root.participations.all()
@classmethod
def get_queryset(cls, queryset, info):
return get_objects_for_user(info.context.user, "alsijil.view_documentation", queryset)
......@@ -87,6 +94,11 @@ class DocumentationInputType(graphene.InputObjectType):
group_note = graphene.String(required=False)
class LessonsForPersonType(graphene.ObjectType):
id = graphene.ID() # noqa
lessons = graphene.List(DocumentationType)
class DocumentationBatchCreateOrUpdateMutation(graphene.Mutation):
class Arguments:
input = graphene.List(DocumentationInputType)
......@@ -99,91 +111,25 @@ class DocumentationBatchCreateOrUpdateMutation(graphene.Mutation):
# 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, lesson_event_id, datetime_start_iso, datetime_end_iso = _id.split(";")
lesson_event = LessonEvent.objects.get(id=lesson_event_id)
datetime_start = datetime.fromisoformat(datetime_start_iso).astimezone(
lesson_event.timezone
)
datetime_end = datetime.fromisoformat(datetime_end_iso).astimezone(
lesson_event.timezone
)
if info.context.user.has_perm(
"alsijil.add_documentation_for_lesson_event_rule", lesson_event
) and (
get_site_preferences()["alsijil__allow_edit_future_documentations"] == "all"
or (
get_site_preferences()["alsijil__allow_edit_future_documentations"]
== "current_day"
and datetime_start.date() <= localdate()
)
or (
get_site_preferences()["alsijil__allow_edit_future_documentations"]
== "current_time"
and datetime_start <= localtime()
)
):
if lesson_event.amends:
if lesson_event.course:
course = lesson_event.course
else:
course = lesson_event.amends.course
if lesson_event.subject:
subject = lesson_event.subject
else:
subject = lesson_event.amends.subject
if lesson_event.teachers:
teachers = lesson_event.teachers
else:
teachers = lesson_event.amends.teachers
else:
course, subject, teachers = (
lesson_event.course,
lesson_event.subject,
lesson_event.teachers,
)
obj = Documentation.objects.create(
datetime_start=datetime_start,
datetime_end=datetime_end,
amends=lesson_event,
course=course,
subject=subject,
topic=doc.topic or "",
homework=doc.homework or "",
group_note=doc.group_note or "",
)
if doc.teachers is not None:
obj.teachers.add(*doc.teachers)
else:
obj.teachers.set(teachers.all())
obj.save()
return obj
obj = Documentation.get_or_create_by_id(_id, info.context.user)
if not info.context.user.has_perm("alsijil.edit_documentation_rule", obj):
raise PermissionDenied()
else:
obj = Documentation.objects.get(id=_id)
if not info.context.user.has_perm("alsijil.edit_documentation_rule", obj):
raise PermissionDenied()
if doc.topic is not None:
obj.topic = doc.topic
if doc.homework is not None:
obj.homework = doc.homework
if doc.group_note is not None:
obj.group_note = doc.group_note
if doc.subject is not None:
obj.subject = Subject.objects.get(pk=doc.subject)
if doc.teachers is not None:
obj.teachers.set(Person.objects.filter(pk__in=doc.teachers))
obj.save()
return obj
if doc.topic is not None:
obj.topic = doc.topic
if doc.homework is not None:
obj.homework = doc.homework
if doc.group_note is not None:
obj.group_note = doc.group_note
if doc.subject is not None:
obj.subject = Subject.objects.get(pk=doc.subject)
if doc.teachers is not None:
obj.teachers.set(Person.objects.filter(pk__in=doc.teachers))
obj.save()
return obj
@classmethod
def mutate(cls, root, info, input): # noqa
......
from graphene_django import DjangoObjectType
from aleksis.apps.alsijil.models import ParticipationStatus
from aleksis.core.schema.base import (
BaseBatchPatchMutation,
DjangoFilterMixin,
OptimisticResponseTypeMixin,
PermissionsTypeMixin,
)
class ParticipationStatusType(
OptimisticResponseTypeMixin,
PermissionsTypeMixin,
DjangoFilterMixin,
DjangoObjectType,
):
class Meta:
model = ParticipationStatus
fields = (
"id",
"person",
"absence_reason",
"related_documentation",
"base_absence",
)
class ParticipationStatusBatchPatchMutation(BaseBatchPatchMutation):
class Meta:
model = ParticipationStatus
fields = ("id", "absence_reason") # Only the reason can be updated after creation
permissions = ("alsijil.change_participation_status",) # FIXME
return_field_name = "participationStatuses"