Skip to content
Snippets Groups Projects
Commit 4cd4ea45 authored by Julian's avatar Julian
Browse files

Merge branch '265-dialog-long-term-absences' into...

Merge branch '265-dialog-long-term-absences' into 261-add-absence-management-to-course-book-student-dialog

# Conflicts:
#	aleksis/apps/alsijil/schema/__init__.py
parents 2239d1c8 7568ba39
No related branches found
No related tags found
1 merge request!362Resolve "Add personal note management dialog in course book"
<template>
<mobile-fullscreen-dialog v-model="popup">
<template #activator="activator">
<!-- button +? -->
<!-- -> popup = true -->
</template>
<template #title>
<!-- Abwesenheit/Entschuldigung erfassen -->
<!-- Abwesenheit/Entschuldigung Zusammenfassung -->
</template>
<template #content>
<absence-form v-if="form" />
<absence-summary v-else />
</template>
<template #actions>
<!-- secondary -->
<!-- TODO: Return to form on cancel? form=true -->
<cancel-button
@click="popup = false"
disabled="loading"
/>
<!-- primary -->
<save-button
v-if="form"
i18n-key="actions.continue"
@click="form = false"
:loading="loading"
/>
<save-button
v-if="form"
i18n-key="actions.confirm"
@click="confirm"
:loading="loading"
/>
</template>
</mobile-fullscreen-dialog>
</template>
<script>
import MobileFullscreenDialog from "aleksis.core/components/generic/dialogs/MobileFullscreenDialog.vue";
import AbsenceForm from "./AbsenceForm.vue";
import AbsenceSummary from "./AbsenceSummary.vue";
import CancelButton from "aleksis.core/components/generic/buttons/CancelButton.vue";
import SaveButton from "aleksis.core/components/generic/buttons/SaveButton.vue";
export default {
name: "AbsenceDialog",
components: {
MobileFullscreenDialog,
AbsenceForm,
AbsenceSummary,
CancelButton,
SaveButton,
},
data() {
return {
popup: false,
form: true,
loading: false,
};
},
methods: {
confirm() {
// TODO: Send mutation (shown in absence-summary)
popup = false,
},
},
};
</script>
<template>
<v-container>
<v-row>
<!-- persons -->
<!-- v-autocomplete -->
</v-row>
<v-row>
<!-- Start -->
<v-col
cols="12"
:sm="6"
>
<date-field
:value="value"
@input="$emit('input', $event)"
:label="$t('date_select.label')"
:disabled="loading"
/>
</v-col>
<!-- End -->
<v-col
cols="12"
:sm="6"
>
<date-field
:value="value"
@input="$emit('input', $event)"
:label="$t('date_select.label')"
:disabled="loading"
/>
</v-col>
</v-row>
<v-row>
<!-- comment -->
</v-row>
<v-row>
<!-- TODO: Component from Julian -->
</v-row>
</v-container>
</template>
<script>
import DateField from "aleksis.core/components/generic/forms/DateField.vue";
export default {
name: "AbsenceForm",
components: {
DateField,
},
};
</script>
<template>
<!-- TODO: Hide header -->
<c-r-u-d-iterator
:gql-query=""
:gql-additional-query-args="FROM FORM"
:enable-create="false"
:enable-edit="false"
:elevated="false"
>
<template #default="{ items }">
<!-- expandable card per person -->
</template>
</c-r-u-d-iterator>
</template>
<script>
import CRUDIterator from "aleksis.core/components/generic/CRUDIterator.vue";
export default {
name: "AbsenceSummary",
components: {
CRUDIterator,
},
};
</script>
# Uses core persons query
query persons {
persons: persons {
id
firstName
lastName
}
}
query lessonsForPersons(
$persons: [ID!]!
$start: Date!
$end: Date!
) {
items: lessonsForPersons(
persons: $persons
start: $start
end: $end
) {
id
lessons {
id
datetimeStart
datetimeEnd
course {
id
name
}
subject {
id
name
shortName
colourFg
colourBg
}
}
}
}
# Use absencesInputType?
mutation createAbsences(
$persons: [ID!]!
$start: Date!
$end: Date!
$comment: String
$reason: ID!
) {
createAbsences(
person: $persons
start: $start
end: $end
comment: $comment
reason: $reason
) {
items: absences {
ok
}
}
}
......@@ -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,116 @@ 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()
return obj
class ParticipationStatus(CalendarEvent):
......
......@@ -11,12 +11,17 @@ 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,
)
from .absences import (
LessonsForPersonType,
AbsencesBatchCreateMutation,
)
from .participation_status import ParticipationStatusBatchPatchMutation
......@@ -38,6 +43,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)
......@@ -55,9 +67,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"
......@@ -80,10 +89,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:
......@@ -117,7 +146,34 @@ 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()
create_absences = AbsencesBatchCreateMutation.Field()
update_participation_statuses = ParticipationStatusBatchPatchMutation.Field()
import graphene
from datetime import datetime
from aleksis.apps.kolego.models import Absence
from .documentation import DocumentationType
from ..models import Documentation, ParticipationStatus
class LessonsForPersonType(graphene.ObjectType):
id = graphene.ID() # noqa
lessons = graphene.List(DocumentationType)
class AbsencesBatchCreateMutation(graphene.Mutation):
class Arguments:
persons = graphene.List(graphene.ID)
start = graphene.Date()
end = graphene.Date()
comment = graphene.String()
reason = graphene.ID()
ok = graphene.Boolean()
@classmethod
def mutate(cls, root, info, persons, start, end, comment, reason): # noqa
# TODO: Check permissions for ParticipationStatus & KolegoAbsence
# See MR 356
# DocumentationBatchCreateOrUpdateMutation.create_or_update
# at least already checks permissions.
for person in persons:
# Get all documentations for this person between start & end
docs, dummies = Documentation.get_documentations_for_person(
person,
datetime.combine(start, datetime.min.time()),
datetime.combine(end, datetime.max.time()),
)
# Create doc for dummies that are already in the past
future = False
for dummy in dummies:
lesson_event, dummy_start, dummy_end = Documentation.parse_dummy(dummy.id)
if dummy_start < datetime.now():
# In the past -> Create a Documentation
docs.append(
Documentation.create_from_lesson_event(
info.context.user,
lesson_event,
dummy_start,
dummy_end,
)
)
else:
future = True
# Create a ParticipationStatus for each documentation
for doc in docs:
# Set person & absence_reason directly from id
ParticipationStatus.objects.create(
person_id=person,
related_documentation=doc,
absence_reason_id=reason,
)
# If there are still dummy documentations in the future
# create a Kolego Absence
if future:
# TODO: Are date_start & date_end from CalendarEvent enough
# or more needed?
# Set reason & person directly from id
Absence.objects.create(
date_start=datetime.now().date(),
date_end=end,
reason_id=reason,
person_id=person,
comment=comment,
)
# Return ok=True if everything went well.
return AbsencesBatchCreateMutation(ok=True)
......@@ -9,7 +9,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,7 +17,6 @@ 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
......@@ -112,90 +110,30 @@ 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
obj = Documentation.create_from_lesson_event(
info.context.user,
*Documentation.parse_dummy(_id),
)
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
raise PermissionDenied()
else:
obj = Documentation.objects.get(id=_id)
if not info.context.user.has_perm("alsijil.edit_documentation_rule", obj):
raise PermissionDenied()
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.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))
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
obj.save()
return obj
@classmethod
def mutate(cls, root, info, input): # noqa
......
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