From caf313703c7f29a9cf89756d9dfbe3093ad163b1 Mon Sep 17 00:00:00 2001
From: magicfelix <felix@felix-zauberer.de>
Date: Thu, 18 May 2023 21:30:43 +0200
Subject: [PATCH] Implement coursebook printout

---
 aleksis/apps/alsijil/tasks.py                 | 125 ++---
 .../alsijil/print/full_coursebook.html        | 429 ++++++++++++++++++
 2 files changed, 465 insertions(+), 89 deletions(-)
 create mode 100644 aleksis/apps/alsijil/templates/alsijil/print/full_coursebook.html

diff --git a/aleksis/apps/alsijil/tasks.py b/aleksis/apps/alsijil/tasks.py
index fddf14959..f18ae76c3 100644
--- a/aleksis/apps/alsijil/tasks.py
+++ b/aleksis/apps/alsijil/tasks.py
@@ -8,7 +8,13 @@ from calendarweek import CalendarWeek
 from celery.result import allow_join_result
 from celery.states import SUCCESS
 
-from aleksis.apps.chronos.models import Event, ExtraLesson, Lesson, LessonPeriod, ValidityRange
+from aleksis.apps.chronos.models import (
+    Lesson,
+    LessonPeriod,
+    LessonSubstitution,
+    Subject,
+    ValidityRange,
+)
 from aleksis.core.celery import app
 from aleksis.core.models import Group, PDFFile
 from aleksis.core.util.celery_progress import ProgressRecorder, recorded_task
@@ -29,14 +35,10 @@ def generate_full_register_printout(group: int, file_object: int, recorder: Prog
     group = Group.objects.get(pk=group)
     file_object = PDFFile.objects.get(pk=file_object)
 
-    groups_q = (
-        Q(lesson_period__lesson__groups=group)
-        | Q(lesson_period__lesson__groups__parent_groups=group)
-        | Q(extra_lesson__groups=group)
-        | Q(extra_lesson__groups__parent_groups=group)
-        | Q(event__groups=group)
-        | Q(event__groups__parent_groups=group)
+    groups_q = Q(lesson_period__lesson__groups=group) | Q(
+        lesson_period__lesson__groups__parent_groups=group
     )
+    subjects = Subject.objects.filter(lessons__groups=group).distinct()
     personal_notes = (
         PersonalNote.objects.prefetch_related(
             "lesson_period__substitutions", "lesson_period__lesson__teachers"
@@ -45,15 +47,10 @@ def generate_full_register_printout(group: int, file_object: int, recorder: Prog
         .filter(groups_q)
         .filter(groups_of_person=group)
     )
-    documentations = LessonDocumentation.objects.not_empty().filter(groups_q)
 
     recorder.set_progress(2, _number_of_steps, _("Sort data ..."))
 
-    sorted_documentations = {"extra_lesson": {}, "event": {}, "lesson_period": {}}
-    sorted_personal_notes = {"extra_lesson": {}, "event": {}, "lesson_period": {}, "person": {}}
-    for documentation in documentations:
-        key = documentation.register_object.label_
-        sorted_documentations[key][documentation.register_object_key] = documentation
+    sorted_personal_notes = {"lesson_period": {}, "person": {}}
 
     for note in personal_notes:
         key = note.register_object.label_
@@ -66,41 +63,9 @@ def generate_full_register_printout(group: int, file_object: int, recorder: Prog
 
     # Get all lesson periods for the selected group
     lesson_periods = LessonPeriod.objects.filter_group(group).distinct()
-    events = Event.objects.filter_group(group).distinct()
-    extra_lessons = ExtraLesson.objects.filter_group(group).distinct()
     weeks = CalendarWeek.weeks_within(group.school_term.date_start, group.school_term.date_end)
 
-    register_objects_by_day = {}
-    for extra_lesson in extra_lessons:
-        day = extra_lesson.date
-        register_objects_by_day.setdefault(day, []).append(
-            (
-                extra_lesson,
-                sorted_documentations["extra_lesson"].get(extra_lesson.pk),
-                sorted_personal_notes["extra_lesson"].get(extra_lesson.pk, []),
-                None,
-            )
-        )
-
-    for event in events:
-        day_number = (event.date_end - event.date_start).days + 1
-        for i in range(day_number):
-            day = event.date_start + timedelta(days=i)
-            event_copy = deepcopy(event)
-            event_copy.annotate_day(day)
-
-            # Skip event days if it isn't inside the timetable schema
-            if not (event_copy.raw_period_from_on_day and event_copy.raw_period_to_on_day):
-                continue
-
-            register_objects_by_day.setdefault(day, []).append(
-                (
-                    event_copy,
-                    sorted_documentations["event"].get(event.pk),
-                    sorted_personal_notes["event"].get(event.pk, []),
-                    None,
-                )
-            )
+    register_objects_by_week = {}
 
     recorder.set_progress(4, _number_of_steps, _("Sort lesson data ..."))
 
@@ -109,27 +74,28 @@ def generate_full_register_printout(group: int, file_object: int, recorder: Prog
         group.school_term.date_end,
     )
 
-    for lesson_period in lesson_periods:
-        for week in weeks:
-            day = week[lesson_period.period.weekday]
-
-            if (
-                lesson_period.lesson.validity.date_start
-                <= day
-                <= lesson_period.lesson.validity.date_end
-            ):
-                filtered_documentation = sorted_documentations["lesson_period"].get(
-                    f"{lesson_period.pk}_{week.week}_{week.year}"
-                )
-                filtered_personal_notes = sorted_personal_notes["lesson_period"].get(
-                    f"{lesson_period.pk}_{week.week}_{week.year}", []
-                )
-
-                substitution = lesson_period.get_substitution(week)
-
-                register_objects_by_day.setdefault(day, []).append(
-                    (lesson_period, filtered_documentation, filtered_personal_notes, substitution)
+    for week in weeks:
+        register_objects_by_week.setdefault(week.week, {"substitutions": [], "documentations": {}})
+        for substitution in LessonSubstitution.objects.filter(
+            year=week.year, week=week.week, lesson_period__lesson__groups=group
+        ):
+            register_objects_by_week[week.week]["substitutions"].append(substitution)
+        for subject in subjects:
+            documentation = (
+                LessonDocumentation.objects.not_empty()
+                .filter(
+                    year=week.year,
+                    week=week.week,
+                    lesson_period__lesson__subject=subject,
+                    lesson_period__lesson__groups=group,
                 )
+                .first()
+            )
+            if not documentation:
+                continue
+            register_objects_by_week[week.week]["documentations"][
+                subject.short_name
+            ] = documentation
 
     recorder.set_progress(5, _number_of_steps, _("Load statistics ..."))
 
@@ -148,33 +114,14 @@ def generate_full_register_printout(group: int, file_object: int, recorder: Prog
     context["extra_marks"] = ExtraMark.objects.all()
     context["group"] = group
     context["weeks"] = weeks
-    context["register_objects_by_day"] = register_objects_by_day
-    context["register_objects"] = list(lesson_periods) + list(events) + list(extra_lessons)
+    context["register_objects_by_week"] = register_objects_by_week
+    context["register_objects"] = list(lesson_periods)
     context["today"] = date.today()
-    context["lessons"] = (
-        group.lessons.all()
-        .select_related(None)
-        .prefetch_related(None)
-        .select_related("validity", "subject")
-        .prefetch_related("teachers", "lesson_periods")
-    )
-    context["child_groups"] = (
-        group.child_groups.all()
-        .select_related(None)
-        .prefetch_related(None)
-        .prefetch_related(
-            "lessons",
-            "lessons__validity",
-            "lessons__subject",
-            "lessons__teachers",
-            "lessons__lesson_periods",
-        )
-    )
 
     recorder.set_progress(6, _number_of_steps, _("Generate template ..."))
 
     file_object, result = generate_pdf_from_template(
-        "alsijil/print/full_register.html", context, file_object=file_object
+        "alsijil/print/full_coursebook.html", context, file_object=file_object
     )
 
     recorder.set_progress(7, _number_of_steps, _("Generate PDF ..."))
diff --git a/aleksis/apps/alsijil/templates/alsijil/print/full_coursebook.html b/aleksis/apps/alsijil/templates/alsijil/print/full_coursebook.html
new file mode 100644
index 000000000..e1d4e9307
--- /dev/null
+++ b/aleksis/apps/alsijil/templates/alsijil/print/full_coursebook.html
@@ -0,0 +1,429 @@
+{% extends "core/base_print.html" %}
+
+{% load static i18n data_helpers week_helpers %}
+
+{% block page_title %}
+  {% trans "Coursebook" %} {{ group.name }}
+{% endblock %}
+
+{% block extra_head %}
+  <link rel="stylesheet" href="{% static 'css/alsijil/full_register.css' %}"/>
+{% endblock %}
+
+{% block content %}
+
+  <div class="center-align">
+    <h1>{% trans 'Coursebook' %}</h1>
+    <h5>{{ school_term }}</h5>
+    <p>({{ school_term.date_start }}–{{ school_term.date_end }})</p>
+    {% static "img/aleksis-banner.svg" as aleksis_banner %}
+    <img src="{% firstof SITE_PREFERENCES.theme__logo.url aleksis_banner %}"
+         alt="{{ SITE_PREFERENCES.general__title }} – Logo" class="max-size-600 center">
+    <h4 id="group-desc">
+      {{ group.name }}
+    </h4>
+    <p id="group-owners" class="flow-text">
+      {% trans 'Owners' %}:
+      {{ group.owners.all|join:', ' }}
+    </p>
+    <p id="printed-info">
+      {% trans 'Printed on' %} {{ today }}
+    </p>
+  </div>
+  <div>
+    <hr/>
+  </div>
+  <div>
+    <p>
+      {% blocktrans %}
+        This printout is intended for archival purposes. The main copy of
+        the class register is stored in the AlekSIS School Information
+        System.
+      {% endblocktrans %}
+    </p>
+    <p>
+      {% blocktrans %}
+        Copies of the class register, both digital and as printout, must
+        only be kept inside the school and/or on devices authorised by the
+        school.
+      {% endblocktrans %}
+    </p>
+    <p>
+      {% blocktrans %}
+        The owner of the group and the headteacher confirm the above, as
+        well as the correctness of this printout.
+      {% endblocktrans %}
+    </p>
+    <div id="signatures">
+      <div class="signature">
+        {% trans 'Owners' %}
+      </div>
+      <div class="signature">
+        {% trans 'Headteacher' %}
+      </div>
+    </div>
+  </div>
+
+  <div class="page-break">&nbsp;</div>
+
+  <h4>{% trans "Abbreviations" %}</h4>
+
+  <h5>{% trans "General" %}</h5>
+
+  <ul class="collection">
+    <li class="collection-item">
+      <strong>(a)</strong> {% trans "Absent" %}
+    </li>
+    <li class="collection-item">
+      <strong>(b)</strong> {% trans "Late" %}
+    </li>
+    <li class="collection-item">
+      <strong>(u)</strong> {% trans "Unexcused" %}
+    </li>
+    <li class="collection-item">
+      <strong>(e)</strong> {% trans "Excused" %}
+    </li>
+  </ul>
+
+  {% if excuse_types %}
+    <h5>{% trans "Custom excuse types" %}</h5>
+
+    <ul class="collection">
+      {% for excuse_type in excuse_types %}
+        <li class="collection-item">
+          <strong>({{ excuse_type.short_name }})</strong> {{ excuse_type.name }}
+        </li>
+      {% endfor %}
+    </ul>
+  {% endif %}
+
+  {% if excuse_types_not_absent %}
+    <h5>{% trans "Custom excuse types (not counted as absent)" %}</h5>
+
+    <ul class="collection">
+      {% for excuse_type in excuse_types_not_absent %}
+        <li class="collection-item">
+          <strong>({{ excuse_type.short_name }})</strong> {{ excuse_type.name }}
+        </li>
+      {% endfor %}
+    </ul>
+  {% endif %}
+
+  {% if extra_marks %}
+    <h5>{% trans "Available extra marks" %}</h5>
+
+    <ul class="collection">
+      {% for extra_mark in extra_marks %}
+        <li class="collection-item">
+          <strong>{{ extra_mark.short_name }}</strong> {{ extra_mark.name }}
+        </li>
+      {% endfor %}
+    </ul>
+  {% endif %}
+
+  <div class="page-break">&nbsp;</div>
+
+
+  <h4>{% trans 'Persons in group' %} {{ group.name }}</h4>
+
+  <table id="persons">
+    <thead>
+    <tr>
+      <th>{% trans 'No.' %}</th>
+      <th>{% trans 'Last name' %}</th>
+      <th>{% trans 'First name' %}</th>
+      <th>{% trans 'Sex' %}</th>
+      <th>{% trans 'Date of birth' %}</th>
+      <th>{% trans '(a)' %}</th>
+      <th>{% trans "Sum (e)" %}</th>
+      <th>{% trans "(e)" %}</th>
+      {% for excuse_type in excuse_types %}
+        <th>({{ excuse_type.short_name }})</th>
+      {% endfor %}
+      <th>{% trans '(u)' %}</th>
+      {% for excuse_type in excuse_types_not_absent %}
+        <th>({{ excuse_type.short_name }})</th>
+      {% endfor %}
+      <th>{% trans '(b)' %}</th>
+      {% for extra_mark in extra_marks %}
+        <th>{{ extra_mark.short_name }}</th>
+      {% endfor %}
+    </tr>
+    </thead>
+
+    <tbody>
+    {% for person in persons %}
+      <tr>
+        <td>{{ forloop.counter }}</td>
+        <td>{{ person.last_name }}</td>
+        <td>{{ person.first_name }}</td>
+        <td>{{ person.get_sex_display }}</td>
+        <td>{{ person.date_of_birth }}</td>
+        <td>{{ person.absences_count }}</td>
+        <td>{{ person.excused }}</td>
+        <td>{{ person.excused_without_excuse_type }}</td>
+        {% for excuse_type in excuse_types %}
+          <td>{{ person|get_dict:excuse_type.count_label }}</td>
+        {% endfor %}
+        <td>{{ person.unexcused }}</td>
+        {% for excuse_type in excuse_types_not_absent %}
+          <td>{{ person|get_dict:excuse_type.count_label }}</td>
+        {% endfor %}
+        <td>{{ person.tardiness }}'/{{ person.tardiness_count }}&times;</td>
+        {% for extra_mark in extra_marks %}
+          <td>{{ person|get_dict:extra_mark.count_label }}</td>
+        {% endfor %}
+      </tr>
+    {% endfor %}
+    </tbody>
+  </table>
+
+  <div class="page-break">&nbsp;</div>
+
+  {% for person in persons %}
+    <h4>{% trans 'Personal overview' %}: {{ person.last_name }}, {{ person.first_name }}</h4>
+
+    <h5>{% blocktrans %}Contact details{% endblocktrans %}</h5>
+    <table class="person-info">
+      <tr>
+        <td rowspan="6" class="person-img">
+          {% if person.photo %}
+            <img src="{{ person.photo.url }}" alt="{{ person.first_name }} {{ person.last_name }}"/>
+          {% else %}
+            <img src="{% static 'img/fallback.png' %}" alt="{{ person.first_name }} {{ person.last_name }}"/>
+          {% endif %}
+        </td>
+        <td><i class="material-icons iconify" data-icon="mdi:account-outline"></i></td>
+        <td colspan="2">{{ person.first_name }} {{ person.additional_name }} {{ person.last_name }}</td>
+      </tr>
+      <tr>
+        <td><i class="material-icons iconify" data-icon="mdi:human-non-binary"></i></td>
+        <td colspan="2">{{ person.get_sex_display }}</td>
+      </tr>
+      <tr>
+        <td><i class="material-icons iconify" data-icon="mdi:map-marker-outline"></i></td>
+        <td>{{ person.street }} {{ person.housenumber }}</td>
+        <td>{{ person.postal_code }} {{ person.place }}</td>
+      </tr>
+      <tr>
+        <td><i class="material-icons iconify" data-icon="mdi:phone-outline"></i></td>
+        <td>{{ person.phone_number }}</td>
+        <td>{{ person.mobile_number }}</td>
+      </tr>
+      <tr>
+        <td><i class="material-icons iconify" data-icon="mdi:email-outline"></i></td>
+        <td colspan="2">{{ person.email }}</td>
+      </tr>
+      <tr>
+        <td><i class="material-icons iconify" data-icon="mdi:cake"></i></td>
+        <td colspan="2">{{ person.date_of_birth|date }}</td>
+      </tr>
+    </table>
+
+    <div class="row">
+      <div class="col s6">
+        <h5>{% trans 'Absences and tardiness' %}</h5>
+        <table>
+          <tr>
+            <th colspan="3">{% trans 'Absences' %}</th>
+            <td>{{ person.absences_count }}</td>
+          </tr>
+          <tr>
+            <td rowspan="{{ excuse_types.count|add:3 }}" style="width: 16mm;"
+                class="rotate small-print">{% trans "thereof" %}</td>
+            <th colspan="2">{% trans 'Excused' %}</th>
+            <td>{{ person.excused }}</td>
+          </tr>
+          <tr>
+            <td rowspan="{{ excuse_types.count|add:1 }}" style="width: 16mm;"
+                class="rotate small-print">{% trans "thereof" %}</td>
+            <th>{% trans "Without excuse type" %}</th>
+            <td>{{ person.excused_without_excuse_type }}</td>
+          </tr>
+          {% for excuse_type in excuse_types %}
+            <tr>
+              <th>{{ excuse_type.name }}</th>
+              <td>{{ person|get_dict:excuse_type.count_label }}</td>
+            </tr>
+          {% endfor %}
+          <tr>
+            <th colspan="2">{% trans 'Unexcused' %}</th>
+            <td>{{ person.unexcused }}</td>
+          </tr>
+          {% for excuse_type in excuse_types_not_absent %}
+            <tr>
+              <th colspan="3">{{ excuse_type.name }}</th>
+              <td>{{ person|get_dict:excuse_type.count_label }}</td>
+            </tr>
+          {% endfor %}
+          <tr>
+            <th colspan="3">{% trans 'Tardiness' %}</th>
+            <td>{{ person.tardiness }}'/{{ person.tardiness_count }}&times;</td>
+          </tr>
+        </table>
+      </div>
+
+      <div class="col s6">
+        {% if extra_marks %}
+        <h5>{% trans 'Extra marks' %}</h5>
+        <table>
+          {% for extra_mark in extra_marks %}
+            <tr>
+              <th>{{ extra_mark.name }}</th>
+              <td>{{ person|get_dict:extra_mark.count_label }}</td>
+            </tr>
+          {% endfor %}
+        </table>
+      {% endif %}
+      </div>
+    </div>
+
+    <h5>{% trans 'Relevant personal notes' %}</h5>
+    <table class="small-print">
+      <thead>
+      <tr>
+        <th>{% trans 'Date' %}</th>
+        <th>{% trans 'Pe.' %}</th>
+        <th>{% trans 'Subj.' %}</th>
+        <th>{% trans 'Te.' %}</th>
+        <th>{% trans 'Absent' %}</th>
+        <th>{% trans 'Tard.' %}</th>
+        <th colspan="2">{% trans 'Remarks' %}</th>
+      </tr>
+      </thead>
+
+      <tbody>
+      {% for note in person.filtered_notes %}
+        {% if note.absent or note.tardiness or note.remarks or note.extra_marks.all %}
+          <tr>
+            {% if note.date %}
+              <td>{{ note.date }}</td>
+              <td>{{ note.register_object.period.period }}</td>
+            {% else %}
+              <td colspan="2">
+                {{ note.register_object.date_start }} {{ note.register_object.period_from.period }}.–{{ note.register_object.date_end }}
+                {{ note.register_object.period_to.period }}.
+              </td>
+            {% endif %}
+            <td>
+              {% if note.register_object.label_ != "event" %}
+                {{ note.register_object.get_subject.short_name }}
+              {% else %}
+                {% trans "Event" %}
+              {% endif %}
+            </td>
+            <td>{{ note.register_object.teacher_short_names }}</td>
+            <td>
+              {% if note.absent %}
+                {% trans 'Yes' %}
+                {% if note.excused %}
+                  {% if note.excuse_type %}
+                    ({{ note.excuse_type.short_name }})
+                  {% else %}
+                    ({% trans 'e' %})
+                  {% endif %}
+                {% endif %}
+              {% endif %}
+            </td>
+            <td>
+              {% if note.tardiness %}
+                {{ note.tardiness }}'
+              {% endif %}
+            </td>
+            <td>
+              {% for extra_mark in note.extra_marks.all %}
+                {{ extra_mark.short_name }}{% if not forloop.last %},{% endif %}
+              {% endfor %}
+            </td>
+            <td>{{ note.remarks }}</td>
+          </tr>
+        {% endif %}
+      {% endfor %}
+      </tbody>
+    </table>
+
+    <div class="page-break">&nbsp;</div>
+
+  {% endfor %}
+
+  {% for week in weeks %}
+    {% with register_objects_by_week|get_dict:week.week as register_objects %}
+    <h4>{% trans 'Week' %} {{ week.week }}: {{ week.0 }}–{{ week.6 }}</h4>
+
+    <table class="small-print">
+      <thead>
+      <tr>
+        <th>{% trans 'Subject' %}</th>
+        <th>{% trans 'Lesson topic' %}</th>
+        <th>{% trans 'Homework' %}</th>
+        <th>{% trans 'Notes' %}</th>
+      </tr>
+      </thead>
+      <tbody>
+        {% with register_objects|get_dict:"documentations" as documentations %}
+	  {% for subject in documentations.keys %}
+          {% with documentations|get_dict:subject as documentation %}
+              <td class="lesson-subj">
+                {% include "chronos/partials/subject.html" with subject=documentation.lesson_period.lesson.subject %}
+              </td>
+              <td class="lesson-topic">
+                {{ documentation.topic }}
+              </td>
+              <td class="lesson-homework">{{ documentation.homework }}</td>
+              <td class="lesson-notes">
+                {{ documentation.group_note }}
+              </td>
+            </tr>
+        {% endwith %}
+        {% endfor %}
+      {% endwith %}
+      </tbody>
+    </table>
+
+    {% if register_objects|get_dict:"substitutions" %}
+    <h5>{% trans "Substitutions" %}</h5>
+
+    <table class="small-print">
+      <thead>
+      <tr>
+        <th>{% trans 'Period' %}</th>
+        <th>{% trans 'Subject' %}</th>
+        <th>{% trans 'Teachers' %}</th>
+        <th>{% trans 'Comment' %}</th>
+      </tr>
+      </thead>
+      <tbody>
+      {% for substitution in register_objects|get_dict:"substitutions" %}
+        <tr class="
+                {% if substitution.cancelled %}
+                  lesson-cancelled
+                {% else %}
+                  lesson-substituted
+                {% endif %}
+              ">
+          <td class="lesson-pe">
+            {{ substitution.date }}.
+          </td>
+          <td class="lesson-subj">
+            {% include "chronos/partials/subs/subject.html" with type="substitution" el=substitution %}
+          </td>
+          <td class="lesson-te">
+            {% for teacher in substitution.teachers.all %}
+              {{ teacher.short_name }}&nbsp;
+            {% endfor %}
+          </td>
+          <td class="lesson-comment">
+            {{ substitution.comment }}
+          </td>
+        </tr>
+      {% endfor %}
+      </tbody>
+    </table>
+    {% endif %}
+
+    {% endwith %}
+    {% if week.week|divisibleby:4 %}
+    <div class="page-break">&nbsp;</div>
+    {% endif %}
+  {% endfor %}
+{% endblock %}
-- 
GitLab