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 116 additions and 541 deletions
<strong>
<span data-position="bottom" class="tooltipped" data-tooltip="{{ subject.name }}">{{ subject.short_name }}</span>
</strong>
{{ subject.short_name|default:subject.name }}
{% if subject.colour_fg %}
color: {{ subject.colour_fg }};
{% endif %}
{% if subject.colour_bg and subject.colour_bg != subject.colour_fg %}
background-color: {{ subject.colour_bg }};
{% endif %}
{% load i18n %}
{% if sub.cancelled %}
{% if el.cancelled %}
<span class="badge new green">{% trans "Cancelled" %}</span>
{% elif item.el.cancelled_for_teachers %}
<span class="badge new green">{% trans "Cancelled for teachers" %}</span>
{% endif %}
{% if item.type == "substitution" %}
{% if item.el.cancelled or item.el.cancelled_for_teachers %}
green-text
{% else %}
black-text
{% endif %}
{% elif item.type == "supervision_substitution" %}
blue-text
{% elif item.type == "event" %}
purple-text
{% if el.cancelled %}
green-text
{% else %}
black-text
{% endif %}
{% if type == "substitution" %}
{% include "chronos/partials/groups.html" with groups=el.lesson_period.lesson.groups.all %}
{% elif type == "extra_lesson" or type == "event" %}
{% if el.cancelled and el.amends.groups.all %}
{% include "chronos/partials/groups.html" with groups=el.amends.groups.all %}
{% elif el.groups.all and el.amends.groups.all %}
<s>{% include "chronos/partials/groups.html" with groups=el.amends.groups.all %}</s>
&nbsp;&nbsp;
<strong>{% include "chronos/partials/groups.html" with groups=el.groups.all %}</strong>
{% elif el.groups.all and not el.amends.groups.all %}
{% include "chronos/partials/groups.html" with groups=el.groups.all %}
{% elif el.amends.groups.all %}
{% include "chronos/partials/groups.html" with groups=el.amends.groups.all %}
{% endif %}
{% load i18n %}
<strong>
{% if type == "substitution" and item.start_period == item.end_period %}
{{ el.lesson_period.period.period }}.
{% elif type == "substitution" %}
{{ item.start_period }}.–{{ item.end_period }}.
{% elif type == "extra_lesson" %}
{{ el.period.period }}.
{% elif type == "event" %}
{% if el.period_from_on_day == el.period_to_on_day %}
{{ el.period_from_on_day }}.
{% if el.REFERENCE_OBJECT.slot_number_start and el.REFERENCE_OBJECT.slot_number_end %}
{% if el.REFERENCE_OBJECT.slot_number_start == el.REFERENCE_OBJECT.slot_number_end %}
{{ el.REFERENCE_OBJECT.slot_number_start }}.
{% else %}
{{ el.period_from_on_day }}.–{{ el.period_to_on_day }}.
{{ el.REFERENCE_OBJECT.slot_number_start }}.–{{ el.REFERENCE_OBJECT.slot_number_end }}.
{% endif %}
{% elif type == "supervision_substitution" %}
{% with break=el.supervision.break_item %}
{{ break.after_period_number }}./{{ break.before_period_number }}.
{% endwith %}
{% elif el.DTSTART.dt.time %}
{{ el.DTSTART.dt.time }}–{{ el.DTEND.dt.time }}
{% else %}
{% trans "all day" %}
{% endif %}
</strong>
{% if type == "substitution" %}
{% if el.cancelled or el.cancelled_for_teachers %}
{# Cancelled lesson: no room #}
{% elif el.room and el.lesson_period.room %}
{# New and old room available #}
<span class="tooltipped" data-position="bottom"
data-tooltip="{{ el.lesson_period.room.name }} → {{ el.room.name }}"
title="{{ el.lesson_period.room.name }} → {{ el.room.name }}">
<a href="{% url "timetable" "room" el.lesson_period.room.pk %}">
<s>{{ el.lesson_period.room.short_name }}</s>
</a>
<a href="{% url "timetable" "room" el.room.pk %}">
<strong>{{ el.room.short_name }}</strong>
</a>
</span>
{% elif el.room and not el.lesson_period.room %}
{# Only new room available #}
{% include "chronos/partials/room.html" with room=el.room %}
{% elif not el.room and not el.lesson_period.room %}
{# Nothing to view #}
{% else %}
{# Only old room available #}
{% include "chronos/partials/room.html" with room=el.lesson_period.room %}
{% endif %}
{% elif type == "supervision_substitution" %}
{% with supervision=el.supervision %}
<span data-position="bottom" class="tooltipped"
data-tooltip="{{ supervision.area.name }}" title="{{ supervision.area.name }}">
{{ supervision.area.short_name }}
</span>
{% endwith %}
{% elif type == "extra_lesson" %}
{% include "chronos/partials/room.html" with room=el.room %}
{% elif type == "event" %}
{% for room in el.rooms.all %}
{% include "chronos/partials/room.html" with room=room %}{% if not forloop.last %},{% endif %}
{% endfor %}
{% endif %}
{% if el.cancelled and el.amends.rooms.all %}
{% include "chronos/partials/rooms.html" with rooms=el.amends.rooms.all %}
{% elif el.rooms.all and el.amends.rooms.all %}
<s>{% include "chronos/partials/rooms.html" with rooms=el.amends.rooms.all %}</s>
&nbsp;&nbsp;
<strong>{% include "chronos/partials/rooms.html" with rooms=el.rooms.all %}</strong>
{% elif el.rooms.all and not el.amends.rooms.all %}
{% include "chronos/partials/rooms.html" with rooms=el.rooms.all %}
{% elif el.amends.rooms.all %}
{% include "chronos/partials/rooms.html" with rooms=el.amends.rooms.all %}
{% endif %}
{% load i18n %}
{% if type == "substitution" %}
{% if not el.lesson_period.lesson.subject and not el.subject %}
{% elif el.cancelled or el.cancelled_for_teachers %}
<span data-position="bottom" class="tooltipped" data-tooltip="{{ el.lesson_period.lesson.subject.name }}">
<s>{{ el.lesson_period.lesson.subject.short_name }}</s>
</span>
{% elif el.subject and el.lesson_period.lesson.subject %}
<span data-position="bottom" class="tooltipped" data-tooltip="{{ el.lesson_period.lesson.subject.name }}">
<s>{{ el.lesson_period.lesson.subject.short_name }}</s>
</span>
<span data-position="bottom" class="tooltipped" data-tooltip="{{ el.subject.name }}">
<strong>{{ el.subject.short_name }}</strong>
</span>
{% elif el.subject and not el.lesson_period.lesson.subject %}
{% include "chronos/partials/subject.html" with subject=el.subject %}
{% else %}
{% include "chronos/partials/subject.html" with subject=el.lesson_period.lesson.subject %}
{% endif %}
{% elif type == "supervision_substitution" %}
{% if el.name == "supervision" %}
{% trans "Supervision" %}
{% elif type == "extra_lesson" %}
{% elif not el.amends.subject and not el.subject %}
{% if el.amends.title %}
<s>{{ el.amends.title }}</s>
{% endif %}
{% if el.title %}
<s>{{ el.title }}</s>
{% endif %}
{% elif el.cancelled %}
<s>{% include "chronos/partials/subject.html" with subject=el.amends.subject %}</s>
{% elif el.subject and el.amends.subject %}
<s>{% include "chronos/partials/subject.html" with subject=el.amends.subject %}</s>
&nbsp;&nbsp;
<strong>{% include "chronos/partials/subject.html" with subject=el.subject %}</strong>
{% elif el.subject and not el.amends.subject %}
{% include "chronos/partials/subject.html" with subject=el.subject %}
{% elif type == "event" %}
{% trans "Event" %}
{% else %}
{% include "chronos/partials/subject.html" with subject=el.amends.subject %}
{% endif %}
{% if type == "substitution" %}
{% if el.cancelled and el.lesson_period.lesson.teachers.all %}
{% include "chronos/partials/teachers.html" with teachers=el.lesson_period.lesson.teachers.all %}
{% elif el.teachers.all and el.lesson_period.lesson.teachers.all %}
<s>
{% include "chronos/partials/teachers.html" with teachers=el.lesson_period.lesson.teachers.all %}
</s>
<strong>
{% include "chronos/partials/teachers.html" with teachers=el.teachers.all %}
</strong>
{% elif el.teachers.all and not el.lesson_period.lesson.teachers.all %}
{% include "chronos/partials/teachers.html" with teachers=el.teachers.all %}
{% elif el.lesson_period.lesson.teachers.all %}
{% include "chronos/partials/teachers.html" with teachers=el.lesson_period.lesson.teachers.all %}
{% endif %}
{% elif type == "supervision_substitution" %}
{% if el.cancelled and el.amends.teachers.all %}
{% include "chronos/partials/teachers.html" with teachers=el.amends.teachers.all %}
{% elif el.teachers.all and el.amends.teachers.all %}
<s>
{% include "chronos/partials/teachers.html" with teachers=el.supervision.teachers %}
{% include "chronos/partials/teachers.html" with teachers=el.amends.teachers.all %}
</s>
<strong>
{% include "chronos/partials/teachers.html" with teachers=el.teachers %}
{% include "chronos/partials/teachers.html" with teachers=el.teachers.all %}
</strong>
{% elif type == "extra_lesson" or type == "event" %}
{% elif el.teachers.all and not el.amends.teachers.all %}
{% include "chronos/partials/teachers.html" with teachers=el.teachers.all %}
{% elif el.amends.teachers.all %}
{% include "chronos/partials/teachers.html" with teachers=el.amends.teachers.all %}
{% endif %}
{% load i18n %}
<div class="card lesson-card supervision-card {% if active_day and week_day == active_day %} z-depth-5 active {% endif %}">
<div class="card-content">
{% if supervision %}
<div style="
{% if supervision.area.colour_fg %}
color: {{ supervision.area.colour_fg }};
{% endif %}
{% if supervision.area.colour_bg and supervision.area.colour_bg != supervision.area.colour_fg %}
background-color: {{ supervision.area.colour_bg }};
{% endif %}
" class="{% if supervision.get_substitution and smart %}lesson-with-sub{% endif %}">
<p>
<strong>{% trans "Supervision" %}</strong>
<span data-position="bottom" class="tooltipped"
data-tooltip="{{ supervision.area.name }}" title="{{ supervision.area.name }}">
{{ supervision.area.short_name }}
</span>
{% if supervision.get_substitution and smart %}
{% include "chronos/partials/subs/teachers.html" with type="supervision_substitution" el=supervision.get_substitution %}
{% elif type == "supervision_area" %}
{% include "chronos/partials/teachers.html" with teachers=supervision.teachers %}
{% endif %}
</p>
</div>
{% endif %}
</div>
</div>
{% for teacher in teachers %}
<span data-position="bottom" class="tooltipped"
data-tooltip="{{ teacher }}">
<a href="{% url "timetable" "teacher" teacher.pk %}">
{{ teacher.short_name }}{% if not forloop.last %},{% endif %}
</a>
</span>
{{ teacher.short_name|default:teacher.full_name }}{% if not forloop.last %},{% endif %}
{% endfor %}
{% load i18n %}
<span class="badge new orange center-align holiday-badge">{% trans "Today" %}</span>
{% load i18n %}
<div class="col s12 m6 right">
<div class="col s2 no-print">
<a class="waves-effect waves-teal btn-flat btn-flat-medium right" href="{{ url_prev }}">
<i class="material-icons iconify center" data-icon="mdi:chevron-left"></i>
</a>
</div>
<div class="input-field col s8 no-margin hide-on-med-and-up">
<select id="calendar-week-1">
{% for week in weeks %}
<option value="{{ week.week }}" {% if week == wanted_week %}
selected {% endif %}>{% trans "CW" %} {{ week.week }}
({{ week.0|date:"SHORT_DATE_FORMAT" }}–{{ week.6|date:"SHORT_DATE_FORMAT" }})
</option>
{% endfor %}
</select>
</div>
<div class="input-field col s8 no-margin hide-on-med-and-down">
<select id="calendar-week-2">
{% for week in weeks %}
<option value="{{ week.week }}" {% if week == wanted_week %}
selected {% endif %}>{% trans "CW" %} {{ week.week }}
({{ week.0|date:"j.n." }}–{{ week.6|date:"SHORT_DATE_FORMAT" }})
</option>
{% endfor %}
</select>
</div>
<div class="input-field col s8 no-margin hide-on-small-and-down hide-on-large-only">
<select id="calendar-week-3">
{% for week in weeks %}
<option value="{{ week.week }}" {% if week == wanted_week %}
selected {% endif %}>{% trans "CW" %} {{ week.week }} ({{ week.0|date:"j.n." }}–{{ week.6|date:"j.n." }})
</option>
{% endfor %}
</select>
</div>
<div class="col s2 no-print">
<a class="waves-effect waves-teal btn-flat btn-flat-medium left" href="{{ url_next }}">
<i class="material-icons iconify center" data-icon="mdi:chevron-right"></i>
</a>
</div>
</div>
{# Week days #}
<div class="row">
<div class="col {% if active_day %}s1{% else %}s2{% endif %}">
</div>
{# Show short weekdays on tablets #}
{% for weekday in weekdays_short %}
<div class="col s2 hide-on-large-only">
<div class="card timetable-title-card {% if active_day and weekday.date == active_day %} z-depth-5 {% endif %}">
<div class="card-content">
<span class="card-title">
{{ weekday.name }}
</span>
{% if smart %}
{{ weekday.date }}
{% if weekday.holiday %}
<br/>{% include "chronos/partials/holiday.html" with holiday=weekday.holiday %}
{% endif %}
{% if weekday.date == today %}
<br/> {% include "chronos/partials/today.html" %}
{% endif %}
{% endif %}
</div>
</div>
</div>
{% endfor %}
{# Show long weekdays elsewere #}
{% for weekday in weekdays %}
<div class="col {% if weekday.date == active_day %} s3 {% else %} s2 {% endif %} hide-on-med-only">
<div class="card timetable-title-card {% if active_day and weekday.date == active_day %} z-depth-5 {% endif %}">
<div class="card-content">
<span class="card-title">
{{ weekday.name }}
</span>
{% if smart %}
{{ weekday.date }}
{% if weekday.holiday %}
<br/>{% include "chronos/partials/holiday.html" with holiday=weekday.holiday %}
{% endif %}
{% if weekday.date == today %}
<br/> {% include "chronos/partials/today.html" %}
{% endif %}
{% endif %}
</div>
</div>
</div>
{% endfor %}
</div>
{# Lessons #}
{% for row in timetable %}
<div class="row">
<div class="col {% if active_day %}s1{% else %}s2{% endif %}">
{% if row.type == "period" %}
{% include "chronos/partials/period_time.html" with period=row.period periods=periods %}
{% endif %}
</div>
{% for col in row.cols %}
{# A lesson #}
<div class="col {% if forloop.counter0 == active_day.weekday %} s3 {% else %} s2 {% endif %}">
{% if row.type == "period" %}
{% include "chronos/partials/elements.html" with elements=col week_day=forloop.counter0 active_day=active_day.weekday %}
{% else %}
{% include "chronos/partials/supervision.html" with supervision=col %}
{% endif %}
</div>
{% endfor %}
</div>
{% endfor %}
\ No newline at end of file
......@@ -18,13 +18,13 @@
{% include "core/partials/announcements.html" with announcements=c.announcements show_recipients=1 %}
{% include "chronos/partials/headerbox.html" with affected_teachers=c.affected_teachers affected_groups=c.affected_groups absent_teachers=c.absent_teachers absent_groups=c.absent_groups print=1 %}
{% include "chronos/partials/headerbox.html" with absent_teachers=c.absent_teachers absent_groups=c.absent_groups affected_teachers=c.affected_teachers affected_groups=c.affected_groups %}
<table class="substitutions">
<thead>
<tr>
<th><i class="material-icons iconify center" data-icon="mdi:account-multiple-outline"></i></th>
<th><i class="material-icons iconify center" data-icon="mdi:clock-outline"></i></th>
<th>{% blocktrans %}Groups{% endblocktrans %}</th>
<th>{% blocktrans %}Time{% endblocktrans %}</th>
<th>{% blocktrans %}Teachers{% endblocktrans %}</th>
<th>{% blocktrans %}Subject{% endblocktrans %}</th>
<th>{% blocktrans %}Room{% endblocktrans %}</th>
......@@ -47,30 +47,30 @@
<tbody>
{% for item in c.substitutions %}
{% ifchanged item.el.lesson_period.lesson.groups_to_show_names %}
{% ifchanged item.el.REFERENCE_OBJECT.group_names %}
</tbody>
<tbody class="{% cycle "striped" "not-striped" %}">
{% endifchanged %}
<tr class="{% include "chronos/partials/subs/colour.html" with item=item %}">
<tr class="{% include "chronos/partials/subs/colour.html" with el=item.el.REFERENCE_OBJECT %}">
<td>
{% include "chronos/partials/subs/groups.html" with type=item.type el=item.el %}
{% include "chronos/partials/subs/groups.html" with el=item.el.REFERENCE_OBJECT %}
</td>
<td>
{% include "chronos/partials/subs/period.html" with type=item.type el=item.el item=item %}
{% include "chronos/partials/subs/period.html" with el=item.el %}
</td>
<td>
{% include "chronos/partials/subs/teachers.html" with type=item.type el=item.el %}
{% include "chronos/partials/subs/teachers.html" with el=item.el.REFERENCE_OBJECT %}
</td>
<td>
{% include "chronos/partials/subs/subject.html" with type=item.type el=item.el %}
{% include "chronos/partials/subs/subject.html" with el=item.el.REFERENCE_OBJECT %}
</td>
<td>
{% include "chronos/partials/subs/room.html" with type=item.type el=item.el %}
{% include "chronos/partials/subs/rooms.html" with el=item.el.REFERENCE_OBJECT %}
</td>
<td>
{% include "chronos/partials/subs/badge.html" with sub=item.el %}
{% include "chronos/partials/subs/comment.html" with el=item.el %}
{% include "chronos/partials/subs/badge.html" with el=item.el.REFERENCE_OBJECT %}
{% include "chronos/partials/subs/comment.html" with el=item.el.REFERENCE_OBJECT %}
</td>
</tr>
{% endfor %}
......
{% extends 'core/base_print.html' %}
{% load data_helpers static i18n %}
{% block extra_head %}
<link rel="stylesheet" href="{% static 'css/chronos/timetable.css' %}">
<link rel="stylesheet" href="{% static 'css/chronos/timetable_print.css' %}">
{% endblock %}
{% block page_title %}
{% trans "Timetable" %} <i>{{ el.short_name }}</i>
{% endblock %}
{% block content %}
<div class="timetable-plan">
{# Week days #}
<div class="row">
<div class="col s2">
</div>
{% for weekday in weekdays_short %}
<div class="col s2">
<div class="card timetable-title-card">
<div class="card-content">
<span class="card-title">
{{ weekday.name }}
</span>
</div>
</div>
</div>
{% endfor %}
</div>
{% for row in timetable %}
<div class="row">
<div class="col s2">
{% if row.type == "period" %}
{% include "chronos/partials/period_time.html" with period=row.period periods=periods %}
{% endif %}
</div>
{% for col in row.cols %}
{# A lesson #}
<div class="col s2">
{% if row.type == "period" %}
{% include "chronos/partials/elements.html" with elements=col %}
{% else %}
{% include "chronos/partials/supervision.html" with supervision=col %}
{% endif %}
</div>
{% endfor %}
</div>
{% endfor %}
</div>
{% endblock %}
......@@ -8,4 +8,9 @@ urlpatterns = [
views.substitutions_print,
name="substitutions_print",
),
path(
"substitutions/print/<str:day>/",
views.substitutions_print,
name="substitutions_print",
),
]
from collections import OrderedDict
from datetime import date
from datetime import date, datetime, time
from typing import Union
from django.apps import apps
......@@ -7,9 +7,11 @@ from django.apps import apps
from calendarweek import CalendarWeek
from aleksis.apps.chronos.managers import TimetableType
from aleksis.apps.chronos.models import SupervisionEvent
from aleksis.core.models import Group, Person, Room
LessonPeriod = apps.get_model("chronos", "LessonPeriod")
LessonEvent = apps.get_model("chronos", "LessonEvent")
TimePeriod = apps.get_model("chronos", "TimePeriod")
Break = apps.get_model("chronos", "Break")
Supervision = apps.get_model("chronos", "Supervision")
......@@ -380,96 +382,57 @@ def build_timetable(
return rows
def build_substitutions_list(wanted_day: date) -> list[dict]:
def build_substitutions_list(wanted_day: date) -> tuple[list[dict], set[Person], set[Group]]:
rows = []
subs = LessonSubstitution.objects.on_day(wanted_day).order_by(
"lesson_period__lesson__groups", "lesson_period__period"
affected_teachers = set()
affected_groups = set()
lesson_events = LessonEvent.get_single_events(
datetime.combine(wanted_day, time.min),
datetime.combine(wanted_day, time.max),
params={"amending": True},
with_reference_object=True,
)
start_period = None
for i, sub in enumerate(subs):
if not sub.cancelled_for_teachers:
sort_a = sub.lesson_period.lesson.groups_to_show_names
else:
sort_a = f"Z.{sub.lesson_period.lesson.teacher_names}"
# Get next substitution
next_sub = subs[i + 1] if i + 1 < len(subs) else None
# Check if next substitution is equal with this substitution
if (
next_sub
and sub.comment == next_sub.comment
and sub.cancelled == next_sub.cancelled
and sub.subject == next_sub.subject
and sub.room == next_sub.room
and sub.lesson_period.lesson == next_sub.lesson_period.lesson
and set(sub.teachers.all()) == set(next_sub.teachers.all())
):
if not start_period:
start_period = sub.lesson_period.period.period
continue
for lesson_event in lesson_events:
affected_teachers.update(lesson_event["REFERENCE_OBJECT"].teachers.all())
affected_teachers.update(lesson_event["REFERENCE_OBJECT"].amends.teachers.all())
affected_groups.update(lesson_event["REFERENCE_OBJECT"].groups.all())
affected_groups.update(lesson_event["REFERENCE_OBJECT"].amends.groups.all())
row = {
"type": "substitution",
"sort_a": sort_a,
"sort_b": str(sub.lesson_period.period.period),
"el": sub,
"start_period": start_period if start_period else sub.lesson_period.period.period,
"end_period": sub.lesson_period.period.period,
"sort_a": lesson_event["REFERENCE_OBJECT"].group_names,
"sort_b": str(lesson_event["DTSTART"]),
"el": lesson_event,
}
if start_period:
start_period = None
rows.append(row)
# Get supervision substitutions
super_subs = SupervisionSubstitution.objects.filter(date=wanted_day)
supervision_events = SupervisionEvent.get_single_events(
datetime.combine(wanted_day, time.min),
datetime.combine(wanted_day, time.max),
params={"amending": True},
with_reference_object=True,
)
print(supervision_events)
for supervision_event in supervision_events:
affected_teachers.update(supervision_event["REFERENCE_OBJECT"].teachers.all())
affected_teachers.update(supervision_event["REFERENCE_OBJECT"].amends.teachers.all())
for super_sub in super_subs:
row = {
"type": "supervision_substitution",
"sort_a": f"Z.{super_sub.teacher}",
"sort_b": str(super_sub.supervision.break_item.after_period_number),
"el": super_sub,
"sort_a": "Z",
"sort_b": str(supervision_event["DTSTART"]),
"el": supervision_event,
}
rows.append(row)
# Get extra lessons
extra_lessons = ExtraLesson.objects.on_day(wanted_day)
for extra_lesson in extra_lessons:
row = {
"type": "extra_lesson",
"sort_a": str(extra_lesson.group_names),
"sort_b": str(extra_lesson.period.period),
"el": extra_lesson,
}
rows.append(row)
# Get events
events = Event.objects.on_day(wanted_day).annotate_day(wanted_day)
rows.sort(key=lambda row: row["sort_a"] + row["sort_b"])
for event in events:
sort_a = event.group_names if event.groups.all() else f"Z.{event.teacher_names}"
row = {
"type": "event",
"sort_a": sort_a,
"sort_b": str(event.period_from_on_day),
"el": event,
}
rows.append(row)
# Sort all items
def sorter(row: dict):
return row["sort_a"] + row["sort_b"]
rows.sort(key=sorter)
return rows
return rows, affected_teachers, affected_groups
def build_weekdays(
......
from typing import Any, Optional
from django.contrib.contenttypes.models import ContentType
from django.db import transaction
from django.db.models import Model
from django.db.models.signals import m2m_changed, post_save, pre_delete
from django.dispatch import Signal, receiver
from celery import shared_task
......@@ -11,126 +6,9 @@ from reversion.models import Revision
def _get_substitution_models():
from aleksis.apps.chronos.models import (
Event,
ExtraLesson,
LessonSubstitution,
SupervisionSubstitution,
)
return [LessonSubstitution, Event, ExtraLesson, SupervisionSubstitution]
class TimetableChange:
"""A change to timetable models."""
def __init__(
self,
instance: Model,
changed_fields: Optional[dict[str, Any]] = None,
created: bool = False,
deleted: bool = False,
):
self.instance = instance
self.changed_fields = changed_fields or {}
self.created = created
self.deleted = deleted
class TimetableDataChangeTracker:
"""Helper class for tracking changes in timetable models by using signals."""
@classmethod
def get_models(cls) -> list[type[Model]]:
"""Return all models that should be tracked."""
from aleksis.apps.chronos.models import (
Event,
ExtraLesson,
LessonSubstitution,
SupervisionSubstitution,
)
return [LessonSubstitution, Event, ExtraLesson, SupervisionSubstitution]
def __init__(self):
self.changes = {}
self.m2m_fields = {}
if transaction.get_autocommit():
raise RuntimeError("Cannot use change tracker outside of transaction")
for model in self.get_models():
post_save.connect(self._handle_save, sender=model, weak=False)
pre_delete.connect(self._handle_delete, sender=model, weak=False)
# Register signals for all relevant m2m fields
m2m_fields = {
getattr(model, f.name).through: f
for f in model._meta.get_fields()
if f.many_to_many
}
self.m2m_fields.update(m2m_fields)
for through_model, _field in m2m_fields.items():
m2m_changed.connect(self._handle_m2m_changed, sender=through_model, weak=False)
transaction.on_commit(self.close)
def get_instance_key(self, instance: Model) -> str:
"""Get unique string key for an instance."""
return f"{instance._meta.model_name}_{instance.id}"
def _add_change(self, change: TimetableChange):
"""Add a change to the list of changes and update, if necessary."""
key = self.get_instance_key(change.instance)
if key not in self.changes or change.deleted or change.created:
self.changes[key] = change
else:
self.changes[key].changed_fields.update(change.changed_fields)
def _handle_save(self, sender: type[Model], instance: Model, created: bool, **kwargs):
"""Handle the save signal."""
change = TimetableChange(instance, created=created)
if not created:
change.changed_fields = instance.tracker.changed()
self._add_change(change)
def _handle_delete(self, sender: type[Model], instance: Model, **kwargs):
"""Handle the delete signal."""
change = TimetableChange(instance, deleted=True)
self._add_change(change)
def _handle_m2m_changed(
self,
sender: type[Model],
instance: Model,
action: str,
model: type[Model],
pk_set: set,
**kwargs,
):
"""Handle the m2m_changed signal."""
if action in ["pre_add", "pre_remove", "pre_clear"]:
field_name = self.m2m_fields[sender].name
current_value = list(getattr(instance, field_name).all())
if self.get_instance_key(instance) in self.changes:
change = self.changes[self.get_instance_key(instance)]
if field_name in change.changed_fields:
current_value = None
if current_value is not None:
change = TimetableChange(instance, changed_fields={field_name: current_value})
self._add_change(change)
def close(self):
"""Disconnect signals and send change signal."""
for model in self.get_models():
post_save.disconnect(self._handle_save, sender=model)
pre_delete.disconnect(self._handle_delete, sender=model)
for through_model, _field in self.m2m_fields.items():
m2m_changed.disconnect(self._handle_m2m_changed, sender=through_model)
from aleksis.apps.chronos.models import LessonEvent, SupervisionEvent
timetable_data_changed.send(sender=self, changes=self.changes)
return [LessonEvent, SupervisionEvent]
chronos_revision_created = Signal()
......
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