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"> </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"> </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 }}×</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"> </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 }}×</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"> </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 }} + {% endfor %} + </td> + <td class="lesson-comment"> + {{ substitution.comment }} + </td> + </tr> + {% endfor %} + </tbody> + </table> + {% endif %} + + {% endwith %} + {% if week.week|divisibleby:4 %} + <div class="page-break"> </div> + {% endif %} + {% endfor %} +{% endblock %} -- GitLab