Skip to content
Snippets Groups Projects
Commit 3211fca8 authored by Jonathan Weth's avatar Jonathan Weth :keyboard:
Browse files

Merge branch '215-substitutions-pdf-for-new-data-model' into 'master'

Resolve "Substitutions PDF for new data model"

Closes #215

See merge request !315
parents 3c2a4aa0 2aa7a0e9
No related branches found
No related tags found
1 merge request!315Resolve "Substitutions PDF for new data model"
Pipeline #192228 failed
Showing
with 129 additions and 311 deletions
......@@ -32,5 +32,21 @@ export default {
fullWidth: true,
},
},
{
path: "substitutions/print/",
component: () => import("aleksis.core/components/LegacyBaseTemplate.vue"),
name: "chronos.substitutions",
props: {
byTheGreatnessOfTheAlmightyAleksolotlISwearIAmWorthyOfUsingTheLegacyBaseTemplate: true,
},
},
{
path: "substitutions/print/:date/",
component: () => import("aleksis.core/components/LegacyBaseTemplate.vue"),
name: "chronos.substitutionsByDate",
props: {
byTheGreatnessOfTheAlmightyAleksolotlISwearIAmWorthyOfUsingTheLegacyBaseTemplate: true,
},
},
],
};
......@@ -946,3 +946,11 @@ class LessonEventQuerySet(RecurrencePolymorphicQuerySet):
def not_amending(self) -> "LessonEventQuerySet":
"""Get all lesson events that are not amending other events."""
return self.filter(amends__isnull=True)
def amending(self) -> "LessonEventQuerySet":
"""Get all lesson events that are amending other events."""
return self.filter(amends__isnull=False)
class SupervisionEventQuerySet(LessonEventQuerySet):
pass
......@@ -42,6 +42,7 @@ from aleksis.apps.chronos.managers import (
LessonPeriodQuerySet,
LessonSubstitutionManager,
LessonSubstitutionQuerySet,
SupervisionEventQuerySet,
SupervisionManager,
SupervisionQuerySet,
SupervisionSubstitutionManager,
......@@ -1209,20 +1210,26 @@ class AutomaticPlan(LiveDocument):
@property
def current_start_day(self) -> date:
"""Get first day which should be shown in the PDF."""
return TimePeriod.get_next_relevant_day(timezone.now().date(), datetime.now().time())
from aleksis.apps.chronos.util.chronos_helpers import get_next_relevant_day
return get_next_relevant_day(timezone.now())
@property
def current_end_day(self) -> date:
"""Get last day which should be shown in the PDF."""
return self.current_start_day + timedelta(days=self.number_of_days - 1)
from aleksis.apps.chronos.util.chronos_helpers import get_next_relevant_day
day = self.current_start_day
for _i in range(self.number_of_days - 1):
day = get_next_relevant_day(day)
return day
def get_context_data(self) -> dict[str, Any]:
"""Get context data for generating the substitutions PDF."""
from aleksis.apps.chronos.util.chronos_helpers import get_substitutions_context_data # noqa
context = get_substitutions_context_data(
request=None,
is_print=True,
wanted_day=date.today(),
number_of_days=self.number_of_days,
show_header_box=self.show_header_box,
)
......@@ -1248,11 +1255,16 @@ class AutomaticPlan(LiveDocument):
continue
# Check if the changed object is relevant for the time period of the PDF file
if isinstance(version.object, Event):
if not version.object.amends:
return
if version.object.datetime_start:
date_start = version.object.datetime_start.date()
date_end = version.object.datetime_end.date()
else:
date_start = version.object.date_start
date_end = version.object.date_end
else:
date_start = date_end = version.object.date
if date_start <= self.current_end_day and date_end >= self.current_start_day:
update = True
break
......@@ -1537,9 +1549,15 @@ class LessonEvent(CalendarEvent):
@classmethod
def get_objects(
cls, request: HttpRequest | None = None, params: dict[str, any] | None = None, **kwargs
cls,
request: HttpRequest | None = None,
params: dict[str, any] | None = None,
no_effect: bool = False,
**kwargs,
) -> Iterable:
"""Return all objects that should be included in the calendar."""
if no_effect:
return super().get_objects(request, params, **kwargs)
objs = (
super()
.get_objects(request, params, **kwargs)
......@@ -1556,6 +1574,7 @@ class LessonEvent(CalendarEvent):
type_ = params.get("type", None)
not_amended = params.get("not_amended", False)
not_amending = params.get("not_amending", False)
amending = params.get("amending", False)
own = params.get("own", False)
if not_amended:
......@@ -1564,6 +1583,9 @@ class LessonEvent(CalendarEvent):
if not_amending:
objs = objs.not_amending()
if amending:
objs = objs.amending()
if request and "own" in params:
if own:
objs = objs.for_person(request.user.person)
......@@ -1599,7 +1621,7 @@ class SupervisionEvent(LessonEvent):
name = "supervision"
verbose_name = _("Supervisions")
objects = RecurrencePolymorphicManager.from_queryset(LessonEventQuerySet)()
objects = RecurrencePolymorphicManager.from_queryset(SupervisionEventQuerySet)()
@classmethod
def value_title(cls, reference_object: LessonEvent, request: HttpRequest | None = None) -> str:
......@@ -1624,10 +1646,22 @@ class SupervisionEvent(LessonEvent):
cls, request: HttpRequest | None = None, params: dict[str, any] | None = None, **kwargs
) -> Iterable:
"""Return all objects that should be included in the calendar."""
objs = super().get_objects(request, params, **kwargs).instance_of(cls)
objs = super().get_objects(request, params, no_effect=True, **kwargs)
if params:
obj_id = int(params.get("id", 0))
type_ = params.get("type", None)
not_amended = params.get("not_amended", False)
not_amending = params.get("not_amending", False)
amending = params.get("amending", False)
if not_amended:
objs = objs.not_amended()
if not_amending:
objs = objs.not_amending()
if amending:
objs = objs.amending()
if type_ and obj_id:
if type_ == "TEACHER":
......
from datetime import time
from django.core.exceptions import ValidationError
from django.utils.translation import gettext_lazy as _
from calendarweek.django import i18n_day_name_choices_lazy
from colorfield.widgets import ColorWidget
from dynamic_preferences.preferences import Section
from dynamic_preferences.types import (
BooleanPreference,
IntegerPreference,
ModelMultipleChoicePreference,
MultipleChoicePreference,
StringPreference,
TimePreference,
)
......@@ -52,6 +55,34 @@ class ShortenGroupsLimit(IntegerPreference):
)
@site_preferences_registry.register
class SubstitutionsRelevantDays(MultipleChoicePreference):
"""Relevant days which have substitution plans."""
section = chronos
name = "substitutions_relevant_days"
default = [0, 1, 2, 3, 4]
verbose_name = _("Relevant days for substitution plans")
required = True
choices = i18n_day_name_choices_lazy()
def validate(self, value):
for v in value:
if int(v) not in self.get_choice_values():
raise ValidationError(f"{v} is not a valid choice")
@site_preferences_registry.register
class SubstitutionsDayChangeTime(TimePreference):
"""Time when substitution plans should switch to the next day."""
section = chronos
name = "substitutions_day_change_time"
default = time(18, 0)
verbose_name = _("Time when substitution plans switch to the next day")
required = True
@site_preferences_registry.register
class SubstitutionsPrintNumberOfDays(IntegerPreference):
section = chronos
......
from rules import add_perm
from aleksis.core.util.predicates import (
has_any_object,
has_global_perm,
has_object_perm,
has_person,
)
from .models import LessonSubstitution
from .util.predicates import has_any_timetable_object, has_room_timetable_perm, has_timetable_perm
# View timetable overview
......@@ -42,10 +40,7 @@ delete_substitution_predicate = has_person & (
add_perm("chronos.delete_substitution_rule", delete_substitution_predicate)
# View substitutions
view_substitutions_predicate = has_person & (
has_global_perm("chronos.view_lessonsubstitution")
| has_any_object("chronos.view_lessonsubstitution", LessonSubstitution)
)
view_substitutions_predicate = has_person & (has_global_perm("chronos.view_lessonsubstitution"))
add_perm("chronos.view_substitutions_rule", view_substitutions_predicate)
# View all supervisions per day
......
......@@ -2,13 +2,13 @@ from datetime import timezone
import graphene
from graphene_django import DjangoObjectType
from graphene_django_cud.mutations import (
DjangoBatchCreateMutation,
DjangoBatchDeleteMutation,
DjangoBatchPatchMutation,
)
from aleksis.core.models import Group, Person, Room
from aleksis.core.schema.base import (
BaseBatchCreateMutation,
BaseBatchDeleteMutation,
BaseBatchPatchMutation,
)
from ..models import LessonEvent
from ..util.chronos_helpers import get_groups, get_rooms, get_teachers
......@@ -80,7 +80,7 @@ class DatetimeTimezoneMixin:
return value
class AmendLessonBatchCreateMutation(DatetimeTimezoneMixin, DjangoBatchCreateMutation):
class AmendLessonBatchCreateMutation(DatetimeTimezoneMixin, BaseBatchCreateMutation):
class Meta:
model = LessonEvent
permissions = ("chronos.edit_substitution_rule",)
......@@ -98,12 +98,13 @@ class AmendLessonBatchCreateMutation(DatetimeTimezoneMixin, DjangoBatchCreateMut
@classmethod
def before_save(cls, root, info, input, created_objects): # noqa: A002
super().before_save(root, info, input, created_objects)
for obj in created_objects:
obj.timezone = obj.amends.timezone
return created_objects
class AmendLessonBatchPatchMutation(DatetimeTimezoneMixin, DjangoBatchPatchMutation):
class AmendLessonBatchPatchMutation(DatetimeTimezoneMixin, BaseBatchPatchMutation):
class Meta:
model = LessonEvent
permissions = ("chronos.edit_substitution_rule",)
......@@ -111,12 +112,13 @@ class AmendLessonBatchPatchMutation(DatetimeTimezoneMixin, DjangoBatchPatchMutat
@classmethod
def before_save(cls, root, info, input, updated_objects): # noqa: A002
super().before_save(root, info, input, updated_objects)
for obj in updated_objects:
obj.timezone = obj.amends.timezone
return updated_objects
class AmendLessonBatchDeleteMutation(DjangoBatchDeleteMutation):
class AmendLessonBatchDeleteMutation(BaseBatchDeleteMutation):
class Meta:
model = LessonEvent
permissions = ("chronos.delete_substitution_rule",)
......
{% load static %}
{% if not display_date_only %}
<script type="text/javascript" src="{% static "js/helper.js" %}"></script>
{{ datepicker|json_script:"datepicker_data" }}
<script type="text/javascript" src="{% static "js/chronos/date_select.js" %}"></script>
{% endif %}
<div class="col s2 no-padding">
<a class="waves-effect waves-secondary btn-flat btn-flat-medium left" href="{{ url_prev }}">
<i class="material-icons iconify center" data-icon="mdi:chevron-left"></i>
</a>
</div>
{% if display_date_only %}
<div class="col s8">
<span class="card-title center-block" id="date">
{{ day|date:"l" }}, {{ day }}
</span>
</div>
{% else %}
<div class="col s8 no-padding">
<input type="text" class="datepicker center-align" id="date">
</div>
{% endif %}
<div class="col s2 no-padding">
<a class="waves-effect waves-secondary btn-flat btn-flat-medium right" href="{{ url_next }}">
<i class="material-icons iconify center" data-icon="mdi:chevron-right"></i>
</a>
</div>
<div class="card lesson-card {% if active_day and week_day == active_day %} z-depth-5 active {% endif %}">
<div class="card-content">
{% for element in elements %}
{% if element.label_ == "lesson_period" %}
{% include "chronos/partials/lesson.html" with lesson_period=element %}
{% elif element.label_ == "extra_lesson" and smart %}
{% include "chronos/partials/extra_lesson.html" with extra_lesson=element %}
{% elif element.label_ == "event" and smart %}
{% include "chronos/partials/event.html" with event=element %}
{% endif %}
{% endfor %}
</div>
</div>
<div class="lesson-with-event">
<p>
{# Teacher or room > Display groups #}
{% if type.value == "teacher" or type.value == "room" %}
{% include "chronos/partials/groups.html" with groups=event.groups.all %}
{% endif %}
{# Class or room > Display teachers #}
{% if type.value == "room" or type.value == "group" %}
{% include "chronos/partials/teachers.html" with teachers=event.teachers.all %}
{% endif %}
{# Teacher or class > Display rooms #}
{% if type.value == "teacher" or type.value == "group" %}
{% for room in event.rooms.all %}
{% include "chronos/partials/room.html" with room=room %}{% if not forloop.last %},{% endif %}
{% endfor %}
{% endif %}
{% if type.value == "teacher" and not event.groups.all and not event.rooms.all and event.title %}
<em>{{ event.title }}</em>
{% elif type.value == "group" and not event.teachers.all and not event.groups.all and event.title %}
<em>{{ event.title }}</em>
{% elif type.value == "room" and not event.teachers.all and not event.groups.all and event.title %}
<em>{{ event.title }}</em>
{% elif event.title %}
<br/>
<small>
<em>{{ event.title }}</em>
</small>
{% endif %}
</p>
</div>
<div class="lesson-with-sub"
style="{% include "chronos/partials/subject_colour.html" with subject=extra_lesson.subject %}">
<p>
{# Teacher or room > Display groups #}
{% if type.value == "teacher" or type.value == "room" %}
{% include "chronos/partials/groups.html" with groups=extra_lesson.groups.all %}
{% endif %}
{# Class or room > Display teachers #}
{% if type.value == "room" or type.value == "group" %}
{% include "chronos/partials/teachers.html" with teachers=extra_lesson.teachers.all %}
{% endif %}
{% include "chronos/partials/subject.html" with subject=extra_lesson.subject %}
{# Teacher or class > Display rooms #}
{% if type.value == "teacher" or type.value == "group" %}
{% include "chronos/partials/room.html" with room=extra_lesson.room %}
{% endif %}
{% if extra_lesson.comment %}
<br/>
<small>
<em>{{ extra_lesson.comment }}</em>
</small>
{% endif %}
</p>
</div>
<a href="{% url "timetable" "group" item.pk %}">
{{ item.short_name }}{% if not forloop.last %},{% endif %}
</a>
{{ item.short_name }}{% if not forloop.last %},{% endif %}
{% if groups.count == 1 and groups.0.parent_groups.all and request.site.preferences.chronos__use_parent_groups %}
{% include "chronos/partials/groups_part.html" with groups=groups.0.parent_groups.all no_collapsible=no_collapsible %}
{% include "chronos/partials/groups_part.html" with groups=groups.0.parent_groups.all %}
{% else %}
{% include "chronos/partials/groups_part.html" with groups=groups no_collapsible=no_collapsible %}
{% endif %}
{% if groups.count > request.site.preferences.chronos__shorten_groups_limit and request.user.person.preferences.chronos__shorten_groups and not no_collapsible %}
{% include "components/text_collapsible.html" with template="chronos/partials/group.html" qs=groups %}
{% else %}
{% for group in groups %}
{% include "chronos/partials/group.html" with item=group %}
{% endfor %}
{% endif %}
{% for group in groups %}
{% include "chronos/partials/group.html" with item=group %}
{% endfor %}
{% load i18n %}
{% if affected_teachers or affected_groups or absent_teachers or absent_groups %}
<div class="{% if not print %}card{% endif %}">
<div class="{% if not print %}card-content{% endif %}">
<div>
<div>
{% if absent_teachers %}
<div class="row no-margin">
<div class="col {% if not print %}s12 m3{% else %}s3{% endif %}">
<div class="col">
<strong class="truncate">
{% trans "Absent teachers" %}
</strong>
</div>
<div class="col {% if not print %}s12 m9{% else %}s9{% endif %} black-text-a">
<div class="col">
{% include "chronos/partials/teachers.html" with teachers=absent_teachers %}
</div>
</div>
{% endif %}
{% if absent_groups %}
<div class="row no-margin">
<div class="col {% if not print %}s12 m3{% else %}s3{% endif %}">
<div class="col">
<strong class="truncate">
{% trans "Absent groups" %}
</strong>
</div>
<div class="col {% if not print %}s12 m9{% else %}s9{% endif %} black-text-a">
<div class="col">
{% include "chronos/partials/groups.html" with groups=absent_groups no_collapsible=True %}
</div>
</div>
{% endif %}
{% if affected_teachers %}
<div class="row no-margin">
<div class="col {% if not print %}s12 m3{% else %}s3{% endif %}">
<div class="col">
<strong class="truncate">
{% trans "Affected teachers" %}
</strong>
</div>
<div class="col {% if not print %}s12 m9{% else %}s9{% endif %} black-text-a">
<div class="col">
{% include "chronos/partials/teachers.html" with teachers=affected_teachers %}
</div>
</div>
{% endif %}
{% if affected_groups %}
<div class="row no-margin">
<div class="col {% if not print %}s12 m3{% else %}s3{% endif %}">
<div class="col">
<strong class="truncate">
{% trans "Affected groups" %}
</strong>
</div>
<div class="col {% if not print %}s12 m9{% else %}s9{% endif %} black-text-a">
{% include "chronos/partials/groups.html" with groups=affected_groups no_collapsible=True %}
<div class="col">
{% include "chronos/partials/groups.html" with groups=affected_groups %}
</div>
</div>
{% endif %}
......
<span class="badge new blue center-align holiday-badge">{{ holiday.title }}</span>
{% load i18n %}
<div style="
{% with sub=lesson_period.get_substitution %}
{# Display background color only if lesson is not cancelled and it is not the old room #}
{% if not smart %}
{% include "chronos/partials/subject_colour.html" with subject=lesson_period.lesson.subject %}
{% elif not sub.cancelled and not lesson_period.get_substitution.cancelled_for_teachers and not lesson_period.replaced_by_event %}
{% if not type.value == "room" or lesson_period.room == lesson_period.get_room or lesson_period.get_room == el %}
{% if sub and sub.subject %}
{% include "chronos/partials/subject_colour.html" with subject=sub.subject %}
{% else %}
{% include "chronos/partials/subject_colour.html" with subject=lesson_period.lesson.subject %}
{% endif %}
{% endif %}
{% endif %}
{% endwith %}
"
{# Add CSS class for sub when it's a sub #}
class="{% if smart %}{% if lesson_period.get_substitution or lesson_period.replaced_by_event %}lesson-with-sub{% endif %}{% endif %}"
>
<p>
{% if lesson_period.replaced_by_event and smart %}
{% include "chronos/partials/groups.html" with groups=lesson_period.lesson.groups.all %}
{% include "chronos/partials/subject.html" with subject=lesson_period.lesson.subject %}
<br/>
<span class="badge new green">{% trans "Cancelled due to an event" %}</span>
{% elif lesson_period.get_substitution and smart %}
{% with sub=lesson_period.get_substitution %}
{# SUBSTITUTION #}
{% if type.value == "room" and lesson_period.room != lesson_period.get_room and lesson_period.get_room != el %}
{# When it's the old room, let it empty #}
{% elif sub.cancelled or sub.cancelled_for_teachers %}
{# When a badge (cancellation, etc.) exists, then display it with the teacher#}
{# Class or room > Display teacher #}
{% if type.value == "group" or type.value == "room" and lesson_period.lesson.teachers.all %}
{% include "chronos/partials/teachers.html" with teachers=lesson_period.lesson.teachers.all %}<br/>
{% endif %}
{# Badge #}
{% include "chronos/partials/subs/badge.html" with sub=sub %}
{% else %}
{# Display sub #}
{# Teacher or room > display classes #}
{% if type.value == "teacher" or type.value == "room" %}
{% include "chronos/partials/groups.html" with groups=lesson_period.lesson.groups.all %}
{% endif %}
{# Display teacher with tooltip #}
{% include "chronos/partials/subs/teachers.html" with type="substitution" el=sub %}
{# Display subject #}
{% include "chronos/partials/subs/subject.html" with type="substitution" el=sub %}
{# Teacher or class > display room #}
{% if type.value == "teacher" or type.value == "group" %}
{% include "chronos/partials/subs/room.html" with type="substitution" el=sub %}
{% endif %}
{% endif %}
{# Display the comment (e. g. work orders) #}
{% if sub.comment %}
<br>
<small>
<em>{{ sub.comment }}</em>
</small>
{% endif %}
{% endwith %}
{% else %}
{# Normal plan #}
{# Teacher or room > Display classes #}
{% if type.value == "teacher" or type.value == "room" %}
{# {{ element_container.element.classes }}#}
{% if lesson_period.lesson.groups %}
{% include "chronos/partials/groups.html" with groups=lesson_period.lesson.groups.all %}
{% endif %}
{% endif %}
{# Class or room > Display teacher #}
{% if type.value == "room" or type.value == "group" %}
{% include "chronos/partials/teachers.html" with teachers=lesson_period.lesson.teachers.all %}
{% endif %}
{# Display subject #}
{% include "chronos/partials/subject.html" with subject=lesson_period.lesson.subject %}
{# Teacher or class > Display room #}
{% if type.value == "teacher" or type.value == "group" %}
{% if lesson_period.room %}
{% include "chronos/partials/room.html" with room=lesson_period.room %}
{% endif %}
{% endif %}
{% endif %}
</p>
</div>
{% if holiday %}
<div class="row">
<div class="col s12">
<div class="card col s12 holiday-card">
<div class="card-content">
<p>
{% include "chronos/partials/holiday.html" with holiday=holiday %}<br/>
</p>
</div>
</div>
</div>
</div>
{% else %}
{% for row in timetable %}
<div class="row">
<div class="col s4">
{% if row.type == "period" %}
{% include "chronos/partials/period_time.html" with period=row.period periods=periods %}
{% endif %}
</div>
<div class="col s8">
{% if row.type == "period" %}
{% include "chronos/partials/elements.html" with elements=row.col %}
{% else %}
{% include "chronos/partials/supervision.html" with supervision=row.col %}
{% endif %}
</div>
</div>
{% endfor %}
{% endif %}
{% load data_helpers %}
<div class="card timetable-title-card">
<div class="card-content">
{# Lesson number #}
<span class="card-title left">
{{ period }}.
</span>
{# Time dimension of lesson #}
<div class="right timetable-time grey-text text-darken-2">
{% with period_obj=periods|get_dict:period %}
<span>{{ period_obj.0|time }}</span>
<br/>
<span>{{ period_obj.1|time }}</span>
{% endwith %}
</div>
</div>
</div>
{% if room %}
<span class="tooltipped" data-position="bottom" data-tooltip="{{ room.name }}">
<a href="{% url "timetable" "room" room.pk %}">
{{ room.short_name }}
</a>
</span>
{% endif %}
{% for room in rooms %}
{{ room.short_name|default:room.name }}{% if not forloop.last %},{% endif %}
{% endfor %}
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