diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 9896d170efea81df55d670f15c8df22d6ee08256..21313f1a392296f9139fdf5bb52b305ebe290def 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -16,6 +16,11 @@ If you're upgrading from 3.x, there is now a migration path to use. Therefore, please install ``AlekSIS-App-Lesrooster`` which now includes parts of the legacy Chronos and the migration path. +Added +~~~~~ + +* Configurable PDF export of the coursebook for one or more groups. + `4.0.0.dev8`_ - 2024-11-15 -------------------------- diff --git a/aleksis/apps/alsijil/frontend/components/coursebook/Coursebook.vue b/aleksis/apps/alsijil/frontend/components/coursebook/Coursebook.vue index bf9d99bd2bf8d49715c823c26dc1393d1df2d1cd..87d9f429075cad7a46a80dd55a82e3d01ea642c2 100644 --- a/aleksis/apps/alsijil/frontend/components/coursebook/Coursebook.vue +++ b/aleksis/apps/alsijil/frontend/components/coursebook/Coursebook.vue @@ -15,7 +15,7 @@ use-deep-search > <template #additionalActions="{ attrs, on }"> - <coursebook-filters :page-type="pageType" v-model="filters" /> + <coursebook-controls :page-type="pageType" v-model="filters" /> <v-expand-transition> <v-card outlined @@ -56,7 +56,7 @@ :subjects="subjects" :documentation="item" :affected-query="lastQuery" - :value="(selectedParticipations[item.id] ??= [])" + :value="selectedParticipations[item.id] ??= []" @input="selectParticipation(item.id, $event)" /> </template> @@ -69,9 +69,7 @@ <DocumentationLoader /> </template> </infinite-scrolling-date-sorted-c-r-u-d-iterator> - <absence-creation-dialog - :absence-reasons="absenceReasons" - /> + <absence-creation-dialog :absence-reasons="absenceReasons" /> </div> </template> @@ -79,7 +77,7 @@ import InfiniteScrollingDateSortedCRUDIterator from "aleksis.core/components/generic/InfiniteScrollingDateSortedCRUDIterator.vue"; import { documentationsForCoursebook } from "./coursebook.graphql"; import AbsenceReasonButtons from "aleksis.apps.kolego/components/AbsenceReasonButtons.vue"; -import CoursebookFilters from "./CoursebookFilters.vue"; +import CoursebookControls from "./CoursebookControls.vue"; import CoursebookLoader from "./CoursebookLoader.vue"; import DocumentationModal from "./documentation/DocumentationModal.vue"; import DocumentationAbsencesModal from "./absences/DocumentationAbsencesModal.vue"; @@ -95,7 +93,7 @@ export default { components: { DocumentationLoader, AbsenceReasonButtons, - CoursebookFilters, + CoursebookControls, CoursebookLoader, DocumentationModal, DocumentationAbsencesModal, diff --git a/aleksis/apps/alsijil/frontend/components/coursebook/CoursebookFilters.vue b/aleksis/apps/alsijil/frontend/components/coursebook/CoursebookControls.vue similarity index 84% rename from aleksis/apps/alsijil/frontend/components/coursebook/CoursebookFilters.vue rename to aleksis/apps/alsijil/frontend/components/coursebook/CoursebookControls.vue index 6b9dcc92fb433ff320f0c76790656c7b76c0472f..96bf15d75ac030201f46c2d2e96ed20b066aa39d 100644 --- a/aleksis/apps/alsijil/frontend/components/coursebook/CoursebookFilters.vue +++ b/aleksis/apps/alsijil/frontend/components/coursebook/CoursebookControls.vue @@ -1,3 +1,7 @@ +<script setup> +import CoursebookPrintDialog from "./CoursebookPrintDialog.vue"; +</script> + <template> <div class="d-flex flex-column flex-sm-row flex-nowrap flex-grow-1 justify-end gap align-stretch" @@ -56,14 +60,22 @@ hide-details /> </div> - <v-btn - outlined - color="primary" - :loading="selectLoading" - @click="togglePageType()" - > - {{ pageTypeButtonText }} - </v-btn> + <div class="d-flex flex-column gap"> + <v-btn + outlined + color="primary" + :loading="selectLoading" + @click="togglePageType()" + > + {{ pageTypeButtonText }} + </v-btn> + <coursebook-print-dialog + v-if="pageType === 'documentations'" + :loading="selectLoading" + :available-groups="groups" + :value="currentGroups" + /> + </div> </div> </template> @@ -125,6 +137,13 @@ export default { o.id === this.value.objId, ); }, + currentGroups() { + return this.groups.filter( + (o) => + TYPENAMES_TO_TYPES[o.__typename] === this.value.objType && + o.id === this.value.objId, + ); + }, pageTypeButtonText() { if (this.value.pageType === "documentations") { return this.$t("alsijil.coursebook.filter.page_type.absences"); diff --git a/aleksis/apps/alsijil/frontend/components/coursebook/CoursebookPrintDialog.vue b/aleksis/apps/alsijil/frontend/components/coursebook/CoursebookPrintDialog.vue new file mode 100644 index 0000000000000000000000000000000000000000..976dccbebb60ffc9dff257bb496542410cc9aeb2 --- /dev/null +++ b/aleksis/apps/alsijil/frontend/components/coursebook/CoursebookPrintDialog.vue @@ -0,0 +1,163 @@ +<script setup> +import MobileFullscreenDialog from "aleksis.core/components/generic/dialogs/MobileFullscreenDialog.vue"; +import SecondaryActionButton from "aleksis.core/components/generic/buttons/SecondaryActionButton.vue"; +import PrimaryActionButton from "aleksis.core/components/generic/buttons/PrimaryActionButton.vue"; +import CancelButton from "aleksis.core/components/generic/buttons/CancelButton.vue"; +</script> + +<template> + <mobile-fullscreen-dialog v-model="dialog"> + <template #activator> + <secondary-action-button + i18n-key="alsijil.coursebook.print.button" + icon-text="$print" + :loading="loading" + @click="dialog = true" + :disabled="dialog" + /> + </template> + <template #title> + {{ $t("alsijil.coursebook.print.title") }} + </template> + <template #content> + {{ $t("alsijil.coursebook.print.groups") }} + <v-autocomplete + :items="availableGroups" + item-text="name" + item-value="id" + :value="value" + @input="setGroupSelection" + @click:clear="setGroupSelection" + multiple + chips + deletable-chips + /> + <div class="d-flex flex-column"> + {{ $t("alsijil.coursebook.print.include") }} + <v-checkbox + v-model="includeCover" + :label="$t('alsijil.coursebook.print.include_cover')" + /> + <v-checkbox + v-model="includeAbbreviations" + :label="$t('alsijil.coursebook.print.include_abbreviations')" + /> + <v-checkbox + v-model="includeMembersTable" + :label="$t('alsijil.coursebook.print.include_members_table')" + /> + <v-checkbox + v-model="includeTeachersAndSubjectsTable" + :label=" + $t('alsijil.coursebook.print.include_teachers_and_subjects_table') + " + /> + <v-checkbox + v-model="includePersonOverviews" + :label="$t('alsijil.coursebook.print.include_person_overviews')" + /> + <v-checkbox + v-model="includeCoursebook" + :label="$t('alsijil.coursebook.print.include_coursebook')" + /> + </div> + </template> + <template #actions> + <!-- TODO: Should cancel reset state? --> + <cancel-button @click="dialog = false" /> + <primary-action-button + i18n-key="alsijil.coursebook.print.button" + icon-text="$print" + :disabled="!valid" + @click="print" + /> + </template> + </mobile-fullscreen-dialog> +</template> + +<script> +/** + * This component provides a dialog for configuring the coursebook-printout + */ +export default { + name: "CoursebookPrintDialog", + props: { + /** + * Groups available for selection + */ + availableGroups: { + type: Array, + required: true, + }, + /** + * Initially selected groups + */ + value: { + type: Array, + required: false, + default: () => [], + }, + /** + * Loading state + */ + loading: { + type: Boolean, + required: false, + default: false, + }, + }, + emits: ["input"], + data() { + return { + dialog: false, + currentGroupSelection: [], + includeCover: true, + includeAbbreviations: true, + includeMembersTable: true, + includeTeachersAndSubjectsTable: true, + includePersonOverviews: true, + includeCoursebook: true, + }; + }, + computed: { + selectedGroups() { + if (this.currentGroupSelection.length == 0) { + return this.value.map((group) => group.id); + } else { + return this.currentGroupSelection; + } + }, + valid() { + return ( + this.selectedGroups.length > 0 && + (this.includeMembersTable || + this.includeTeachersAndSubjectsTable || + this.includePersonOverviews || + this.includeCoursebook) + ); + }, + }, + methods: { + setGroupSelection(groups) { + this.$emit("input", groups); + this.currentGroupSelection = groups; + }, + print() { + this.$router.push({ + name: "alsijil.coursebook_print", + params: { + groupIds: this.selectedGroups, + }, + query: { + cover: this.includeCover, + abbreviations: this.includeAbbreviations, + members_table: this.includeMembersTable, + teachers_and_subjects_table: this.includeTeachersAndSubjectsTable, + person_overviews: this.includePersonOverviews, + coursebook: this.includeCoursebook, + }, + }); + }, + }, +}; +</script> diff --git a/aleksis/apps/alsijil/frontend/index.js b/aleksis/apps/alsijil/frontend/index.js index 98c02be8426c7d1e57ccde23eb7a5fe121b3152b..ac475483e76e9538c36ffb55d85f53c6531ec802 100644 --- a/aleksis/apps/alsijil/frontend/index.js +++ b/aleksis/apps/alsijil/frontend/index.js @@ -94,6 +94,14 @@ export default { }, ], }, + { + path: "print/groups/:groupIds+/", + component: () => import("aleksis.core/components/LegacyBaseTemplate.vue"), + name: "alsijil.coursebook_print", + props: { + byTheGreatnessOfTheAlmightyAleksolotlISwearIAmWorthyOfUsingTheLegacyBaseTemplate: true, + }, + }, { path: "extra_marks/", component: () => import("./components/extra_marks/ExtraMarks.vue"), diff --git a/aleksis/apps/alsijil/frontend/messages/de.json b/aleksis/apps/alsijil/frontend/messages/de.json index e8fff940a675cdb516b04b23bb962b84266d5549..872b2e07510f802eebe988b4adda0141f404391f 100644 --- a/aleksis/apps/alsijil/frontend/messages/de.json +++ b/aleksis/apps/alsijil/frontend/messages/de.json @@ -98,8 +98,19 @@ "person_page": { "title": "Kursbuch · Statistiken · {fullName}", "summary": "Zusammenfassung" - }, - "title_plural": "Statistiken" + } + }, + "print": { + "button": "Drucken", + "title": "Kursbuchausdruck", + "groups": "Gruppen", + "include": "Abschnitte", + "include_cover": "Deckblatt", + "include_abbreviations": "Abkürzungen", + "include_members_table": "Tabelle aller Gruppenmitglieder mit Statistiken", + "include_teachers_and_subjects_table": "Tabelle mit Lehrkräften und Fächern", + "include_person_overviews": "Detailseiten für alle Gruppenmitglieder", + "include_coursebook": "Kursbuch" } }, "excuse_types": { diff --git a/aleksis/apps/alsijil/frontend/messages/en.json b/aleksis/apps/alsijil/frontend/messages/en.json index 474a039e6d94485f73ae8baade8a3035c1600f6a..2294349f2f1a42241e4017865bcd30fda40ee919 100644 --- a/aleksis/apps/alsijil/frontend/messages/en.json +++ b/aleksis/apps/alsijil/frontend/messages/en.json @@ -129,6 +129,18 @@ "title": "Error: no person | Successfully marked {name} as {reason} | Successfully marked {n} people as {reason}", "description": "Do you want to mark them as {reason} for the rest of their day?", "action_button": "Extend absence" + }, + "print": { + "button": "Print", + "title": "Print Coursebook", + "groups": "Groups", + "include": "Parts to include", + "include_cover": "Cover", + "include_abbreviations": "Abbreviations", + "include_members_table": "Members Table", + "include_teachers_and_subjects_table": "Teachers and Subjects Table", + "include_person_overviews": "Person Overviews", + "include_coursebook": "Coursebook" } }, "personal_notes": { diff --git a/aleksis/apps/alsijil/model_extensions.py b/aleksis/apps/alsijil/model_extensions.py index a80d22be6058a173cb6f4b3da80283d6ca0f55bd..a3d9c14fafedc02858b025ef221d97b08bfb5b90 100644 --- a/aleksis/apps/alsijil/model_extensions.py +++ b/aleksis/apps/alsijil/model_extensions.py @@ -115,6 +115,19 @@ def annotate_person_statistics( return persons +def annotate_person_statistics_from_documentations( + persons: QuerySet[Person], docs: QuerySet[Documentation] +) -> QuerySet[Person]: + """Annotate a queryset of persons with class register statistics from documentations.""" + docs = list(docs.values_list("pk", flat=True)) + return annotate_person_statistics( + persons, + Q(participations__related_documentation__in=docs), + Q(new_personal_notes__documentation__in=docs), + ignore_filters=len(docs) == 0, + ) + + def annotate_person_statistics_for_school_term( persons: QuerySet[Person], school_term: SchoolTerm, group: Group | None = None ) -> QuerySet[Person]: @@ -133,10 +146,4 @@ def annotate_person_statistics_for_school_term( ) ) ) - docs = list(documentations.values_list("pk", flat=True)) - return annotate_person_statistics( - persons, - Q(participations__related_documentation__in=docs), - Q(new_personal_notes__documentation__in=docs), - ignore_filters=len(docs) == 0, - ) + return annotate_person_statistics_from_documentations(persons, documentations) diff --git a/aleksis/apps/alsijil/models.py b/aleksis/apps/alsijil/models.py index 8ab95cdfac80cb73a3d556137c6e60e95ca208cb..7e10a80cb0f83f065d94198658d13c9bc3acfe30 100644 --- a/aleksis/apps/alsijil/models.py +++ b/aleksis/apps/alsijil/models.py @@ -1,5 +1,5 @@ from datetime import datetime -from typing import Optional +from typing import List, Optional from django.contrib.auth.models import User from django.core.exceptions import PermissionDenied @@ -117,6 +117,9 @@ class Documentation(CalendarEvent): if self.course: return self.course.groups.all() + def get_teachers_short_names(self) -> List[str]: + return [teacher.short_name or teacher.name for teacher in self.teachers.all()] + def __str__(self) -> str: start_datetime = CalendarEvent.value_start_datetime(self) end_datetime = CalendarEvent.value_end_datetime(self) diff --git a/aleksis/apps/alsijil/static/css/alsijil/full_register.css b/aleksis/apps/alsijil/static/css/alsijil/full_register.css index 533c84326887b2050cb66edadcf92706f64b963d..63b239041e9db507ba4b1d9753d8f9a175fe63db 100644 --- a/aleksis/apps/alsijil/static/css/alsijil/full_register.css +++ b/aleksis/apps/alsijil/static/css/alsijil/full_register.css @@ -54,10 +54,6 @@ td.lesson-notes span.lesson-note-late { color: #ff9933; } -td.lesson-notes span.lesson-note-excused { - color: #009933; -} - table.person-info { border: none; } diff --git a/aleksis/apps/alsijil/tasks.py b/aleksis/apps/alsijil/tasks.py index 015f4905dfd4dcf37f4c5949c65e212d3b316dbb..2502658defa8da7ecb68e9be7fbb606da7de9632 100644 --- a/aleksis/apps/alsijil/tasks.py +++ b/aleksis/apps/alsijil/tasks.py @@ -1,189 +1,170 @@ -# from copy import deepcopy -# from datetime import date, timedelta +from datetime import date +from typing import List, Optional -# from django.db.models import Q -# from django.utils.translation import gettext as _ +from django.db.models import Prefetch, Q +from django.utils.translation import gettext as _ -# from calendarweek import CalendarWeek -# from celery.result import allow_join_result -# from celery.states import SUCCESS +from celery.result import allow_join_result +from celery.states import SUCCESS -# from aleksis.core.models import Group, PDFFile +from aleksis.apps.cursus.models import Course +from aleksis.apps.kolego.models.absence import AbsenceReason +from aleksis.core.models import Group, PDFFile from aleksis.core.util.celery_progress import ProgressRecorder, recorded_task +from aleksis.core.util.pdf import generate_pdf_from_template -# from aleksis.core.util.pdf import generate_pdf_from_template - -# from .models import ExtraMark +from .model_extensions import annotate_person_statistics_from_documentations +from .models import Documentation, ExtraMark, NewPersonalNote, ParticipationStatus @recorded_task -def generate_full_register_printout(group: int, file_object: int, recorder: ProgressRecorder): - """Generate a full register printout as PDF for a group.""" - - -# context = {} - -# _number_of_steps = 8 - -# recorder.set_progress(1, _number_of_steps, _("Load data ...")) - -# 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) -# ) -# personal_notes = ( -# PersonalNote.objects.prefetch_related( -# "lesson_period__substitutions", "lesson_period__lesson__teachers" -# ) -# .not_empty() -# .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 - -# for note in personal_notes: -# key = note.register_object.label_ -# sorted_personal_notes[key].setdefault(note.register_object_key, []) -# sorted_personal_notes[key][note.register_object_key].append(note) -# sorted_personal_notes["person"].setdefault(note.person.pk, []) -# sorted_personal_notes["person"][note.person.pk].append(note) - -# recorder.set_progress(3, _number_of_steps, _("Load lesson data ...")) - -# # 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, -# ) -# ) - -# recorder.set_progress(4, _number_of_steps, _("Sort lesson data ...")) - -# weeks = CalendarWeek.weeks_within( -# group.school_term.date_start, -# 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) -# ) - -# recorder.set_progress(5, _number_of_steps, _("Load statistics ...")) - -# persons = group.members.prefetch_related(None).select_related(None) -# persons = group.generate_person_list_with_class_register_statistics(persons) - -# prefetched_persons = [] -# for person in persons: -# person.filtered_notes = sorted_personal_notes["person"].get(person.pk, []) -# prefetched_persons.append(person) - -# context["school_term"] = group.school_term -# context["persons"] = prefetched_persons -# context["excuse_types"] = ExcuseType.objects.filter(count_as_absent=True) -# context["excuse_types_not_absent"] = ExcuseType.objects.filter(count_as_absent=False) -# 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["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 -# ) - -# recorder.set_progress(7, _number_of_steps, _("Generate PDF ...")) - -# with allow_join_result(): -# result.wait() -# file_object.refresh_from_db() -# if not result.status == SUCCESS and file_object.file: -# raise Exception(_("PDF generation failed")) - -# recorder.set_progress(8, _number_of_steps) +def generate_full_register_printout( + groups: List[int], + file_object: int, + recorder: ProgressRecorder, + include_cover: Optional[bool] = True, + include_abbreviations: Optional[bool] = True, + include_members_table: Optional[bool] = True, + include_teachers_and_subjects_table: Optional[bool] = True, + include_person_overviews: Optional[bool] = True, + include_coursebook: Optional[bool] = True, +): + """Generate a configurable register printout as PDF for a group.""" + + def prefetch_notable_participations(select_related=None, prefetch_related=None): + if not select_related: + select_related = [] + if not prefetch_related: + prefetch_related = [] + return Prefetch( + "participations", + to_attr="notable_participations", + queryset=ParticipationStatus.objects.filter( + Q(absence_reason__tags__short_name="class_register") | Q(tardiness__isnull=False) + ) + .select_related("absence_reason", *select_related) + .prefetch_related(*prefetch_related), + ) + + def prefetch_personal_notes(name, select_related=None, prefetch_related=None): + if not select_related: + select_related = [] + if not prefetch_related: + prefetch_related = [] + return Prefetch( + name, + queryset=NewPersonalNote.objects.filter(Q(note__gt="") | Q(extra_mark__isnull=False)) + .select_related("extra_mark", *select_related) + .prefetch_related(*prefetch_related), + ) + + context = {} + + context["include_cover"] = include_cover + context["include_abbreviations"] = include_abbreviations + context["include_members_table"] = include_members_table + context["include_teachers_and_subjects_table"] = include_teachers_and_subjects_table + context["include_person_overviews"] = include_person_overviews + context["include_coursebook"] = include_coursebook + + context["today"] = date.today() + + _number_of_steps = 5 + len(groups) + + recorder.set_progress(1, _number_of_steps, _("Loading data ...")) + + groups = Group.objects.filter(pk__in=groups).order_by("name") + + if include_cover: + groups = groups.select_related("school_term") + + if include_abbreviations or include_members_table: + context["absence_reasons"] = AbsenceReason.objects.filter( + tags__short_name="class_register", count_as_absent=True + ) + context["absence_reasons_not_counted"] = AbsenceReason.objects.filter( + tags__short_name="class_register", count_as_absent=False + ) + context["extra_marks"] = ExtraMark.objects.all() + + if include_members_table or include_person_overviews: + groups = groups.prefetch_related("members") + + if include_teachers_and_subjects_table: + groups = groups.prefetch_related( + Prefetch("courses", queryset=Course.objects.select_related("subject")), + "courses__teachers", + "child_groups", + Prefetch("child_groups__courses", queryset=Course.objects.select_related("subject")), + "child_groups__courses__teachers", + ) + + recorder.set_progress(2, _number_of_steps, _("Loading groups ...")) + + for i, group in enumerate(groups, start=1): + recorder.set_progress( + 2 + i, _number_of_steps, _(f"Loading group {group.short_name or group.name} ...") + ) + + if include_members_table or include_person_overviews or include_coursebook: + documentations = Documentation.objects.filter( + Q(datetime_start__date__gte=group.school_term.date_start) + & Q(datetime_end__date__lte=group.school_term.date_end) + & Q( + pk__in=Documentation.objects.filter(course__groups=group) + .values_list("pk", flat=True) + .union( + Documentation.objects.filter( + course__groups__parent_groups=group + ).values_list("pk", flat=True) + ) + ) + ) + + if include_members_table or include_person_overviews: + group.members_with_stats = annotate_person_statistics_from_documentations( + group.members.all(), documentations + ) + + if include_person_overviews: + doc_query_set = documentations.select_related("subject").prefetch_related("teachers") + group.members_with_stats = group.members_with_stats.prefetch_related( + prefetch_notable_participations( + prefetch_related=[Prefetch("related_documentation", queryset=doc_query_set)] + ), + prefetch_personal_notes( + "new_personal_notes", + prefetch_related=[Prefetch("documentation", queryset=doc_query_set)], + ), + ) + + if include_teachers_and_subjects_table: + group.as_list = [group] + + if include_coursebook: + group.documentations = documentations.order_by( + "datetime_start" + ).prefetch_related( + prefetch_notable_participations(select_related=["person"]), + prefetch_personal_notes("personal_notes", select_related=["person"]), + ) + + context["groups"] = groups + + recorder.set_progress(3 + len(groups), _number_of_steps, _("Generating template ...")) + + file_object, result = generate_pdf_from_template( + "alsijil/print/register_for_group.html", + context, + file_object=PDFFile.objects.get(pk=file_object), + ) + + recorder.set_progress(4 + len(groups), _number_of_steps, _("Generating PDF ...")) + + with allow_join_result(): + result.wait() + file_object.refresh_from_db() + if not result.status == SUCCESS and file_object.file: + raise Exception(_("PDF generation failed")) + + recorder.set_progress(5 + len(groups), _number_of_steps) diff --git a/aleksis/apps/alsijil/templates/alsijil/partials/absences.html b/aleksis/apps/alsijil/templates/alsijil/partials/absences.html deleted file mode 100644 index d1faaadd474111453bc07e608ddb63aee11efedc..0000000000000000000000000000000000000000 --- a/aleksis/apps/alsijil/templates/alsijil/partials/absences.html +++ /dev/null @@ -1,9 +0,0 @@ -{% load i18n rules %} -{% for note in notes %} - {% has_perm "alsijil.view_personalnote_rule" user note as can_view_personalnote %} - {% if can_view_personalnote %} - <span class="{% if note.excused %}green-text{% else %}red-text{% endif %}">{{ note.person }} - {% if note.excused %}{% if note.excuse_type %}({{ note.excuse_type.short_name }}){% else %}{% trans "(e)" %}{% endif %}{% else %}{% trans "(u)" %}{% endif %}{% if not forloop.last %},{% endif %} - </span> - {% endif %} -{% endfor %} diff --git a/aleksis/apps/alsijil/templates/alsijil/partials/legend.html b/aleksis/apps/alsijil/templates/alsijil/partials/legend.html deleted file mode 100644 index bf0c82d792b57943b36502f0b94dff3db193a70d..0000000000000000000000000000000000000000 --- a/aleksis/apps/alsijil/templates/alsijil/partials/legend.html +++ /dev/null @@ -1,71 +0,0 @@ -{% load i18n %} -<div class="card"> - <div class="card-content"> - <div class="card-title">{% trans "Legend" %}</div> - <div class="row"> - <div class="col s12 m12 l4"> - <h6>{% trans "General" %}</h6> - <ul class="collection"> - <li class="collection-item chip-height"> - <strong>{% trans "(a)" %}</strong> {% trans "Absences" %} - <span class="chip secondary-color white-text right">0</span> - </li> - <li class="collection-item chip-height"> - <strong>{% trans "(u)" %}</strong> {% trans "Unexcused absences" %} - <span class="chip red white-text right">0</span> - </li> - <li class="collection-item chip-height"> - <strong>{% trans "Sum (e)" %}</strong> {% trans "Sum of excused absences" %} - <span class="chip green white-text right">0</span> - </li> - <li class="collection-item chip-height"> - <strong>{% trans "(e)" %}</strong> {% trans "Regular excused absences" %} - <span class="chip grey white-text right">0</span> - </li> - </ul> - </div> - - {% if excuse_types %} - <div class="col s12 m12 l4"> - <h6>{% trans "Excuse types" %}</h6> - - <ul class="collection"> - {% for excuse_type in excuse_types %} - <li class="collection-item chip-height"> - <strong>({{ excuse_type.short_name }})</strong> {{ excuse_type.name }} - <span class="chip grey white-text right">0</span> - </li> - {% endfor %} - </ul> - {% if excuse_types_not_absent %} - <h6>{% trans "Excuse types (not counted as absent)" %}</h6> - - <ul class="collection"> - {% for excuse_type in excuse_types_not_absent %} - <li class="collection-item chip-height"> - <strong>({{ excuse_type.short_name }})</strong> {{ excuse_type.name }} - <span class="chip grey white-text right">0</span> - </li> - {% endfor %} - </ul> - {% endif %} - </div> - {% endif %} - - {% if extra_marks %} - <div class="col s12 m12 l4"> - <h6>{% trans "Extra marks" %}</h6> - - <ul class="collection"> - {% for extra_mark in extra_marks %} - <li class="collection-item chip-height"> - <strong>{{ extra_mark.short_name }}</strong> {{ extra_mark.name }} - <span class="chip grey white-text right">0</span> - </li> - {% endfor %} - </ul> - </div> - {% endif %} - </div> - </div> -</div> diff --git a/aleksis/apps/alsijil/templates/alsijil/partials/lesson_status.html b/aleksis/apps/alsijil/templates/alsijil/partials/lesson_status.html deleted file mode 100644 index acd8d283d6ecc9fe7ed84234600444e1424a0e49..0000000000000000000000000000000000000000 --- a/aleksis/apps/alsijil/templates/alsijil/partials/lesson_status.html +++ /dev/null @@ -1,36 +0,0 @@ -{% load i18n week_helpers %} - -{% now_datetime as now_dt %} - -{% if has_documentation or register_object.has_documentation %} - {% include "alsijil/partials/lesson_status_icon.html" with text=_("Data complete") icon="mdi:check-circle-outline" color="green" %} -{% elif not register_object.period %} - {% if week %} - {% period_to_time_start week register_object.raw_period_from_on_day as time_start %} - {% period_to_time_end week register_object.raw_period_to_on_day as time_end %} - {% else %} - {% period_to_time_start register_object.date_start register_object.period_from as time_start %} - {% period_to_time_end register_object.date_end register_object.period_to as time_end %} - {% endif %} - - {% if now_dt > time_end %} - {% include "alsijil/partials/lesson_status_icon.html" with text=_("Missing data") icon="mdi:alert-outline" color="red" %} - {% elif now_dt > time_start and now_dt < time_end %} - {% include "alsijil/partials/lesson_status_icon.html" with text=_("Pending") icon="mdi:dots-horizontal" color="orange" %} - {% else %} - {% include "alsijil/partials/lesson_status_icon.html" with text=_("Event") icon="mdi:calendar" color="purple" %} - {% endif %} -{% else %} - {% period_to_time_start week register_object.period as time_start %} - {% period_to_time_end week register_object.period as time_end %} - - {% if substitution.cancelled or register_object.get_substitution.cancelled %} - {% include "alsijil/partials/lesson_status_icon.html" with text=_("Lesson cancelled") icon="mdi:close" color="red" %} - {% elif now_dt > time_end %} - {% include "alsijil/partials/lesson_status_icon.html" with text=_("Missing data") icon="mdi:alert-outline" color="red" %} - {% elif now_dt > time_start and now_dt < time_end %} - {% include "alsijil/partials/lesson_status_icon.html" with text=_("Pending") icon="mdi:dots-horizontal" color="orange" %} - {% elif substitution or register_object.get_substitution %} - {% include "alsijil/partials/lesson_status_icon.html" with text=_("Substitution") icon="mdi:update" color="orange" %} - {% endif %} -{% endif %} diff --git a/aleksis/apps/alsijil/templates/alsijil/partials/lesson_status_icon.html b/aleksis/apps/alsijil/templates/alsijil/partials/lesson_status_icon.html deleted file mode 100644 index 2c016c685bb89e8edca9e75408fd11109f655e0b..0000000000000000000000000000000000000000 --- a/aleksis/apps/alsijil/templates/alsijil/partials/lesson_status_icon.html +++ /dev/null @@ -1,12 +0,0 @@ -{% if chip %} - <span class="{% if chip %}chip{% endif %} {{ color }} white-text {{ css_class }}"> - <i class="material-icons iconify left" data-icon="{{ icon }}"></i> - {{ text }} - </span> -{% else %} - <i class="material-icons iconify {{ color }}{% firstof color_suffix "-text" %} tooltipped {{ css_class }}" - data-icon="{{ icon }}" - data-position="bottom" - data-tooltip="{{ text }}" title="{{ text }}"> - </i> -{% endif %} diff --git a/aleksis/apps/alsijil/templates/alsijil/partials/objects_table.html b/aleksis/apps/alsijil/templates/alsijil/partials/objects_table.html deleted file mode 100644 index e2053816e4a682b5de56596f0cf998e2b4e0a371..0000000000000000000000000000000000000000 --- a/aleksis/apps/alsijil/templates/alsijil/partials/objects_table.html +++ /dev/null @@ -1,43 +0,0 @@ -{% load i18n material_form django_tables2 %} -<div class="card"> - <div class="card-content"> - <div class="card-title">{% trans "Lesson filter" %}</div> - <form action="" method="get"> - {% form form=filter_form %}{% endform %} - <button type="submit" class="btn waves-effect waves-light"> - <i class="material-icons iconify left" data-icon="mdi:refresh"></i> - {% trans "Update filters" %} - </button> - </form> - </div> -</div> - -{% if table %} - <div class="card"> - <div class="card-content"> - <form action="" method="post"> - {% csrf_token %} - <div class="row"> - <div class="col s12 {% if action_form %}m4 l4 xl6{% endif %}"> - <div class="card-title">{% trans "Lesson table" %}</div> - </div> - {% if action_form %} - <div class="col s12 m8 l8 xl6"> - <div class="col s12 m8"> - {% form form=action_form %}{% endform %} - </div> - <div class="col s12 m4"> - <button type="submit" class="btn waves-effect waves-primary"> - {% trans "Execute" %} - <i class="material-icons iconify right" data-icon="mdi:send-outline"></i> - </button> - </div> - </div> - {% endif %} - </div> - {% render_table table %} - - </form> - </div> - </div> -{% endif %} diff --git a/aleksis/apps/alsijil/templates/alsijil/partials/person_overview.html b/aleksis/apps/alsijil/templates/alsijil/partials/person_overview.html new file mode 100644 index 0000000000000000000000000000000000000000..6c91c34730b10116e190b4892bb26415a4ceed20 --- /dev/null +++ b/aleksis/apps/alsijil/templates/alsijil/partials/person_overview.html @@ -0,0 +1,139 @@ +{% load static i18n data_helpers %} + +<h4>{% blocktrans with full_name=person.full_name %}Personal Overview: {{ full_name }}{% endblocktrans %}</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.full_name }}"/> + {% else %} + <img src="{% static 'img/fallback.png' %}" alt="{{ person.full_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.absence_count }}</td> + </tr> + {% for absence_reason in absence_reasons %} + <tr style="color: {{ absence_reason.colour }};"> + <th>{{ absence_reason.name }}</th> + <td>{{ person|get_dict:absence_reason.count_label }}</td> + </tr> + {% endfor %} + {% for absence_reason in absence_reasons_not_counted %} + <tr style="color: {{ absence_reason.colour }};"> + <th colspan="3">{{ absence_reason.name }}</th> + <td>{{ person|get_dict:absence_reason.count_label }}</td> + </tr> + {% endfor %} + <tr> + <th colspan="3">{% trans 'Tardiness' %}</th> + <td>{{ person.tardiness_sum|default_if_none:0 }}'/{{ 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 'Absences and Tardinesses' %}</h5> +<table class="small-print"> + <thead> + <tr> + <th>{% trans 'Date' %}</th> + <th>{% trans 'Subject' %}</th> + <th>{% trans 'Teachers' %}</th> + <th>{% trans 'Absent' %}</th> + <th>{% trans 'Tardiness' %}</th> + </tr> + </thead> + + <tbody> + {% for participation in person.notable_participations %} + <tr> + <td>{{ participation.related_documentation.datetime_start }}</td> + <td> + {{ participation.related_documentation.subject.short_name }} + </td> + <td>{{ participation.related_documentation.get_teachers_short_names|join:', ' }}</td> + <td style="color: {{ absence_reason.colour }};"> + {{ participation.absence_reason.short_name }} + </td> + <td>{{ participation.tardiness|default_if_none:"" }}</td> + </tr> + {% endfor %} + </tbody> +</table> + +<h5>{% trans 'Personal Notes' %}</h5> +<table class="small-print"> + <thead> + <tr> + <th>{% trans 'Date' %}</th> + <th>{% trans 'Subject' %}</th> + <th>{% trans 'Teacher' %}</th> + <th colspan="2">{% trans 'Remarks' %}</th> + </tr> + </thead> + + <tbody> + {% for note in person.new_personal_notes.all %} + <tr> + <td>{{ note.documentation.datetime_start }}</td> + <td> + {{ note.documentation.subject.short_name }} + </td> + <td>{{ note.documentation.get_teachers_short_names|join:', ' }}</td> + {% if note.extra_mark %} + <td>{{ note.extra_mark.short_name }}</td> + {% endif %} + <td>{{ note.note }}</td> + </tr> + {% endfor %} + </tbody> +</table> diff --git a/aleksis/apps/alsijil/templates/alsijil/partials/persons_with_stats.html b/aleksis/apps/alsijil/templates/alsijil/partials/persons_with_stats.html deleted file mode 100644 index 69ce9c6c9a011744eb2b3bed80228fe3147ff01b..0000000000000000000000000000000000000000 --- a/aleksis/apps/alsijil/templates/alsijil/partials/persons_with_stats.html +++ /dev/null @@ -1,147 +0,0 @@ -{% load data_helpers time_helpers i18n rules %} - -{% if not persons %} - <figure class="alert primary"> - <i class="material-icons iconify left" data-icon="mdi:alert-outline"></i> - {% blocktrans %}No students available.{% endblocktrans %} - </figure> -{% else %} - <table class="highlight responsive-table"> - <thead> - <tr class="hide-on-med-and-down"> - <th rowspan="2">{% trans "Name" %}</th> - <th rowspan="2">{% trans "Primary group" %}</th> - <th colspan="{{ excuse_types.count|add:4 }}">{% trans "Absences" %}</th> - {% if excuse_types_not_absent %} - <th colspan="{{ excuse_types_not_absent.count }}">{% trans "Uncounted Absences" %}</th> - {% endif %} - {% if extra_marks %} - <th colspan="{{ extra_marks.count }}">{% trans "Extra marks" %}</th> - {% endif %} - <th rowspan="2">{% trans "Tardiness" %}</th> - <th rowspan="2"></th> - </tr> - <tr class="hide-on-large-only"> - <th class="truncate">{% trans "Name" %}</th> - <th class="truncate">{% trans "Primary group" %}</th> - <th class="truncate chip-height">{% trans "Absences" %}</th> - <th class="chip-height">{% trans "Sum (e)" %}</th> - <th class="chip-height">{% trans "(e)" %}</th> - {% for excuse_type in excuse_types %} - <th class="chip-height"> - ({{ excuse_type.short_name }}) - </th> - {% endfor %} - <th class="chip-height">{% trans "(u)" %}</th> - {% for excuse_type in excuse_types_not_absent %} - <th class="chip-height"> - ({{ excuse_type.short_name }}) - </th> - {% endfor %} - {% for extra_mark in extra_marks %} - <th class="chip-height"> - {{ extra_mark.short_name }} - </th> - {% endfor %} - <th class="truncate chip-height">{% trans "Tardiness" %}</th> - <th rowspan="2"></th> - </tr> - <tr class="hide-on-med-and-down"> - <th>{% trans "Sum" %}</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 %} - {% for extra_mark in extra_marks %} - <th> - {{ extra_mark.short_name }} - </th> - {% endfor %} - </tr> - </thead> - {% for person in persons %} - <tr> - <td> - <a href="{% url "overview_person" person.pk %}"> - {{ person }} - </a> - </td> - <td> - {% firstof person.primary_group "–" %} - </td> - <td> - <span class="chip secondary-color white-text" title="{% trans "Absences" %}"> - {{ person.absences_count }} - </span> - </td> - <td class="green-text"> - <span class="chip green white-text" title="{% trans "Excused" %}"> - {{ person.excused }} - </span> - </td> - <td> - <span class="chip grey white-text" title="{% trans "Regular excused" %}"> - {{ person.excused_without_excuse_type }} - </span> - </td> - {% for excuse_type in excuse_types %} - <td> - <span class="chip grey white-text" title="{{ excuse_type.name }}"> - {{ person|get_dict:excuse_type.count_label }} - </span> - </td> - {% endfor %} - <td class="red-text"> - <span class="chip red white-text" title="{% trans "Unexcused" %}"> - {{ person.unexcused }} - </span> - </td> - {% for excuse_type in excuse_types_not_absent %} - <td> - <span class="chip grey white-text" title="{{ excuse_type.name }}"> - {{ person|get_dict:excuse_type.count_label }} - </span> - </td> - {% endfor %} - {% for extra_mark in extra_marks %} - <td> - <span class="chip grey white-text" title="{{ extra_mark.name }}"> - {{ person|get_dict:extra_mark.count_label }} - </span> - </td> - {% endfor %} - <td> - <span class="chip orange white-text" title="{% trans "Tardiness" %}"> - {% firstof person.tardiness|to_time|time:"H\h i\m" "–" %} - </span> - <span class="chip orange white-text" title="{% trans "Count of tardiness" %}">{{ person.tardiness_count }} ×</span> - </td> - - <td> - <a class="btn primary waves-effect waves-light" href="{% url "overview_person" person.pk %}"> - <i class="material-icons iconify left" data-icon="mdi:chart-box-outline"></i> - <span class="hide-on-med-and-down"> {% trans "Show more details" %}</span> - <span class="hide-on-large-only">{% trans "Details" %}</span> - </a> - - {% has_perm "alsijil.register_absence_rule" user person as can_register_absence %} - {% if can_register_absence %} - <a class="btn primary-color waves-effect waves-light" href="{% url "register_absence" person.pk %}"> - <i class="material-icons iconify left" data-icon="mdi:message-draw"></i> - {% trans "Register absence" %} - </a> - {% endif %} - </td> - </tr> - {% endfor %} -{% endif %} -</table> diff --git a/aleksis/apps/alsijil/templates/alsijil/partials/register_abbreviations.html b/aleksis/apps/alsijil/templates/alsijil/partials/register_abbreviations.html new file mode 100644 index 0000000000000000000000000000000000000000..00b3befb820a8a2188f6d71a2dbff92302a8c359 --- /dev/null +++ b/aleksis/apps/alsijil/templates/alsijil/partials/register_abbreviations.html @@ -0,0 +1,44 @@ +{% load i18n %} + +<h4>{% trans "Abbreviations" %}</h4> + +<h5>{% trans "General" %}</h5> + +<!-- TODO: This implies AbsenceReasons can not have the shortNames a and b! --> +<ul class="collection"> + <li class="collection-item"> + <strong>(a)</strong> {% trans "Absent" %} + </li> + <li class="collection-item"> + <strong>(b)</strong> {% trans "Late" %} + </li> +</ul> + +{% if absence_reasons %} + <h5>{% trans "Absence Reasons" %}</h5> + + <ul class="collection"> + {% for absence_reason in absence_reasons %} + <li class="collection-item" style="color: {{ absence_reason.colour }};"> + <strong>({{ absence_reason.short_name }})</strong> {{ absence_reason.name }} + </li> + {% endfor %} + {% for absence_reason in absence_reasons_not_counted %} + <li class="collection-item" style="color: {{ absence_reason.colour }};"> + <strong>({{ absence_reason.short_name }})</strong> {{ absence_reason.name }} + </li> + {% endfor %} + </ul> +{% endif %} + +{% if extra_marks %} + <h5>{% trans "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 %} diff --git a/aleksis/apps/alsijil/templates/alsijil/partials/register_coursebook.html b/aleksis/apps/alsijil/templates/alsijil/partials/register_coursebook.html new file mode 100644 index 0000000000000000000000000000000000000000..ab626b7dd389ae6fa77732f5dd34328b31f3946a --- /dev/null +++ b/aleksis/apps/alsijil/templates/alsijil/partials/register_coursebook.html @@ -0,0 +1,95 @@ +{% load i18n %} + +<h4>{% trans 'Coursebook' %}</h4> + +<table class="small-print"> + <thead> + <tr> + <th>{% trans 'Time' %}</th> + <th>{% trans 'Subj.' %}</th> + <th>{% trans 'Topic' %}</th> + <th>{% trans 'Homework' %}</th> + <th>{% trans 'Notes' %}</th> + <th>{% trans 'Te.' %}</th> + </tr> + </thead> + <tbody> + {% for doc in group.documentations %} + {% ifchanged doc.datetime_start.date %} + <tr><th colspan="6">{{ doc.datetime_start.date|date:"D d M Y" }}</th></tr> + {% endifchanged %} + <tr class=" + {% if doc.amends %} + {% if doc.amends.cancelled %} + lesson-cancelled + {% endif %} + {% if doc.amends.amends %} + lesson-substituted + {% endif %} + {% endif %} + {% ifchanged doc.datetime_start.date %} + lessons-day-first + {% endifchanged %} + "> + <td class="lesson-pe"> + {% if doc.amends %} + {% if doc.amends.slot_number_start == doc.amends.slot_number_end %} + {{ doc.amends.slot_number_start }}. + {% else %} + {{ doc.amends.slot_number_start }}.–{{ doc.amends.slot_number_end }}. + {% endif %} + {% else %} + {{ doc.datetime_start|time:"H:i" }}-{{ doc.datetime_end|time:"H:i" }} + {% endif %} + </td> + <td class="lesson-subj"> + {% if doc.subject %} + {{ doc.subject.short_name|default:doc.subject.name }} + {% endif %} + </td> + <td class="lesson-topic"> + {{ doc.topic }} + </td> + <td class="lesson-homework">{{ doc.homework }}</td> + <td class="lesson-notes"> + {{ documentation.group_note }} + {% for participation in doc.notable_participations %} + {% if participation.absence_reason %} + <span class="lesson-note-absent"> + {{ participation.person.full_name }} + <span style="color: {{ participation.absence_reason.colour }};"> + ({{ participation.absence_reason.short_name }}) + </span> + </span> + {% endif %} + {% if participation.tardiness %} + <span class="lesson-note-late"> + {{ participation.person.full_name }} + ({{ participation.tardiness }}′) + </span> + {% endif %} + {% endfor %} + {% for personal_note in doc.personal_notes.all %} + {% if personal_note.extra_mark %} + <span> + {{ personal_note.person.full_name }} + ({{ personal_note.extra_mark.short_name }}) + </span> + {% endif %} + {% if personal_note.note %} + <span> + {{ personal_note.person.full_name }} + ({{ personal_note.note }}) + </span> + {% endif %} + {% endfor %} + </td> + <td class="lesson-te"> + {% if doc.topic %} + {{ doc.get_teachers_short_names|join:', ' }} + {% endif %} + </td> + </tr> + {% endfor %} + </tbody> +</table> diff --git a/aleksis/apps/alsijil/templates/alsijil/partials/register_cover.html b/aleksis/apps/alsijil/templates/alsijil/partials/register_cover.html new file mode 100644 index 0000000000000000000000000000000000000000..e588a39aafbd81539a856514656f62a27cdd6d2f --- /dev/null +++ b/aleksis/apps/alsijil/templates/alsijil/partials/register_cover.html @@ -0,0 +1,53 @@ +{% load static i18n %} + +<div class="center-align"> + <h1>{% trans 'Class Register' %}</h1> + <h5>{{ group.school_term }}</h5> + <p>({{ group.school_term.date_start }}–{{ group.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> diff --git a/aleksis/apps/alsijil/templates/alsijil/partials/register_members_table.html b/aleksis/apps/alsijil/templates/alsijil/partials/register_members_table.html new file mode 100644 index 0000000000000000000000000000000000000000..bc5d51a0bee1abde028eed9e850592fa8a778c1b --- /dev/null +++ b/aleksis/apps/alsijil/templates/alsijil/partials/register_members_table.html @@ -0,0 +1,53 @@ +{% load i18n data_helpers %} + +<h4>{% blocktrans with group=group.name %}Persons in Group {{ group }}{% endblocktrans %}</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> + {% for absence_reason in absence_reasons %} + <th style="color: {{ absence_reason.colour }};"> + ({{ absence_reason.short_name }}) + </th> + {% endfor %} + {% for absence_reason in absence_reasons_not_counted %} + <th style="color: {{ absence_reason.colour }};"> + ({{ absence_reason.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 group.members_with_stats %} + <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|default_if_none:'' }}</td> + <td>{{ person.absence_count }}</td> + {% for absence_reason in absence_reasons %} + <td>{{ person|get_dict:absence_reason.count_label }}</td> + {% endfor %} + {% for absence_reason in absence_reasons_not_counted %} + <td>{{ person|get_dict:absence_reason.count_label }}</td> + {% endfor %} + <td>{{ person.tardiness_sum|default_if_none:0 }}'/{{ person.tardiness_count }}×</td> + {% for extra_mark in extra_marks %} + <td>{{ person|get_dict:extra_mark.count_label }}</td> + {% endfor %} + </tr> + {% endfor %} + </tbody> +</table> diff --git a/aleksis/apps/alsijil/templates/alsijil/partials/register_teachers_and_subjects_table.html b/aleksis/apps/alsijil/templates/alsijil/partials/register_teachers_and_subjects_table.html new file mode 100644 index 0000000000000000000000000000000000000000..6ad34170fc352bb63b6c0726756bb922e66bec6a --- /dev/null +++ b/aleksis/apps/alsijil/templates/alsijil/partials/register_teachers_and_subjects_table.html @@ -0,0 +1,29 @@ +{% load i18n %} + +<table id="lessons"> + <thead> + <tr> + {% if groups|length > 1 %} + <th>{% trans 'Group' %}</th> + {% endif %} + <th>{% trans 'Subject' %}</th> + <th>{% trans 'Teacher' %}</th> + <th>{% trans 'Per week' %}</th> + </tr> + </thead> + + <tbody> + {% for group in groups %} + {% for course in group.courses.all %} + <tr> + {% if groups|length > 1 %} + <td>{{ group.name }}</td> + {% endif %} + <td>{{ course.subject.name }}</td> + <td>{{ course.teachers.all|join:', ' }}</td> + <td>{{ course.lesson_quota }}</td> + </tr> + {% endfor %} + {% endfor %} + </tbody> +</table> diff --git a/aleksis/apps/alsijil/templates/alsijil/partials/tardinesses.html b/aleksis/apps/alsijil/templates/alsijil/partials/tardinesses.html deleted file mode 100644 index ca20a9c4d1f420736febd149e657a67e855d45f4..0000000000000000000000000000000000000000 --- a/aleksis/apps/alsijil/templates/alsijil/partials/tardinesses.html +++ /dev/null @@ -1,7 +0,0 @@ -{% load rules %} -{% for note in notes %} - {% has_perm "alsijil.view_personalnote_rule" user note as can_view_personalnote %} - {% if can_view_personalnote %} - <span>{{ note.person }} ({{ note.tardiness }}'){% if not forloop.last %},{% endif %}</span> - {% endif %} -{% endfor %} diff --git a/aleksis/apps/alsijil/templates/alsijil/print/full_register.html b/aleksis/apps/alsijil/templates/alsijil/print/full_register.html deleted file mode 100644 index c699287a00987ad99c1fd0c2d4617827670fae2f..0000000000000000000000000000000000000000 --- a/aleksis/apps/alsijil/templates/alsijil/print/full_register.html +++ /dev/null @@ -1,526 +0,0 @@ -{% extends "core/base_print.html" %} - -{% load static i18n data_helpers week_helpers %} - -{% block page_title %} - {% trans "Class register:" %} {{ 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 'Class register' %}</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> - - {% if lessons %} - <h4>{% trans 'Teachers and lessons in group' %} {{ group.name }}</h4> - - <table id="lessons"> - <thead> - <tr> - <th>{% trans 'Subject' %}</th> - <th>{% trans 'Teacher' %}</th> - <th>{% trans 'Lesson start' %}</th> - <th>{% trans 'Lesson end' %}</th> - <th>{% trans 'Per week' %}</th> - </tr> - </thead> - - <tbody> - {% for lesson in lessons %} - <tr> - <td>{{ lesson.subject.name }}</td> - <td>{{ lesson.teachers.all|join:', ' }}</td> - <td>{{ lesson.validity.date_start }}</td> - <td>{{ lesson.validity.date_end }}</td> - <td>{{ lesson.lesson_periods.count }}</td> - </tr> - {% endfor %} - </tbody> - </table> - <div class="page-break"> </div> - - {% endif %} - - {% if child_groups %} - <h4>{% trans 'Teachers and lessons in child groups' %}</h4> - - <table id="lessons"> - <thead> - <tr> - <th>{% trans 'Group' %}</th> - <th>{% trans 'Subject' %}</th> - <th>{% trans 'Teacher' %}</th> - <th>{% trans 'Lesson start' %}</th> - <th>{% trans 'Lesson end' %}</th> - <th>{% trans 'Per week' %}</th> - </tr> - </thead> - - <tbody> - {% for child_group in child_groups %} - {% for lesson in child_group.lessons.all %} - <tr> - <td>{{ child_group.name }}</td> - <td>{{ lesson.subject.name }}</td> - <td>{{ lesson.teachers.all|join:', ' }}</td> - <td>{{ lesson.validity.date_start }}</td> - <td>{{ lesson.validity.date_end }}</td> - <td>{{ lesson.lesson_periods.count }}</td> - </tr> - {% endfor %} - {% endfor %} - </tbody> - </table> - <div class="page-break"> </div> - {% endif %} - - {% 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 %} - <h4>{% trans 'Week' %} {{ week.week }}: {{ week.0 }}–{{ week.6 }}</h4> - - <table class="small-print"> - <thead> - <tr> - <th></th> - <th>{% trans 'Pe.' %}</th> - <th>{% trans 'Subj.' %}</th> - <th>{% trans 'Lesson topic' %}</th> - <th>{% trans 'Homework' %}</th> - <th>{% trans 'Notes' %}</th> - <th>{% trans 'Te.' %}</th> - </tr> - </thead> - <tbody> - {% for day in week %} - {% with register_objects_by_day|get_dict:day as register_objects %} - {% for register_object, documentation, notes, substitution in register_objects %} - <tr class=" - {% if substitution %} - {% if substitution.cancelled %} - lesson-cancelled - {% else %} - lesson-substituted - {% endif %} - {% endif %} - {% if forloop.first %} - lessons-day-first - {% endif %} - "> - {% if forloop.first %} - <th rowspan="{{ register_objects|length }}" class="lessons-day-head">{{ day|date:"D" }}</th> - {% endif %} - <td class="lesson-pe"> - {% if register_object.label_ != "event" %} - {{ register_object.period.period }} - {% else %} - {{ register_object.period_from_on_day }}.–{{ register_object.period_to_on_day }}. - {% endif %} - </td> - <td class="lesson-subj"> - {% if register_object.label_ == "event" %} - <strong>{% trans "Event" %}</strong> - {% elif substitution %} - {% include "chronos/partials/subs/subject.html" with type="substitution" el=substitution %} - {% else %} - {% include "chronos/partials/subject.html" with subject=register_object.get_subject %} - {% endif %} - </td> - <td class="lesson-topic"> - {% if register_object.label_ == "event" %} - {{ register_object.title }}: {{ documentation.topic }} - {% elif substitution.cancelled %} - {% trans 'Lesson cancelled' %} - {% else %} - {{ documentation.topic }} - {% endif %} - </td> - <td class="lesson-homework">{{ documentation.homework }}</td> - <td class="lesson-notes"> - {{ documentation.group_note }} - {% for note in notes %} - {% if note.absent %} - <span class="lesson-note-absent"> - {{ note.person.last_name }}, {{ note.person.first_name|slice:"0:1" }}. - {% if note.excused %} - <span class="lesson-note-excused"> - {% if note.excuse_type %} - ({{ note.excuse_type.short_name }}) - {% else %} - ({% trans 'e' %}) - {% endif %} - </span> - {% endif %} - </span> - {% endif %} - {% if note.tardiness %} - <span class="lesson-note-late"> - {{ note.person.last_name }}, {{ note.person.first_name|slice:"0:1" }}. - ({{ note.tardiness }}′) - {% if note.excused %} - <span class="lesson-note-excused"> - {% if note.excuse_type %} - ({{ note.excuse_type.short_name }}) - {% else %} - ({% trans 'e' %}) - {% endif %} - </span> - {% endif %} - </span> - {% endif %} - {% for extra_mark in note.extra_marks.all %} - <span> - {{ note.person.last_name }}, {{ note.person.first_name|slice:"0:1" }}. - ({{ extra_mark.short_name }}) - </span> - {% endfor %} - {% endfor %} - </td> - <td class="lesson-te"> - {% if documentation.topic %} - {{ register_object.get_teachers.first.short_name }} - {% endif %} - </td> - </tr> - {% endfor %} - {% endwith %} - {% endfor %} - </tbody> - </table> - - <div class="page-break"> </div> - {% endfor %} -{% endblock %} diff --git a/aleksis/apps/alsijil/templates/alsijil/print/register_for_group.html b/aleksis/apps/alsijil/templates/alsijil/print/register_for_group.html new file mode 100644 index 0000000000000000000000000000000000000000..2257633273a5faccede0e6bef2abbb059a2f65b5 --- /dev/null +++ b/aleksis/apps/alsijil/templates/alsijil/print/register_for_group.html @@ -0,0 +1,54 @@ +{% extends "core/base_print.html" %} + +{% load static i18n %} + +{% block page_title %} + {% trans "Class Register" %} +{% endblock %} + +{% block extra_head %} + <link rel="stylesheet" href="{% static 'css/alsijil/full_register.css' %}"/> +{% endblock %} + +{% block content %} + {% for group in groups %} + {% if include_cover %} + {% include "alsijil/partials/register_cover.html" with group=group %} + <div class="page-break"> </div> + {% endif %} + + {% if include_abbreviations %} + {% include "alsijil/partials/register_abbreviations.html" with group=group %} + <div class="page-break"> </div> + {% endif %} + + {% if include_members_table %} + {% include "alsijil/partials/register_members_table.html" with group=group %} + <div class="page-break"> </div> + {% endif %} + + {% if include_teachers_and_subjects_table %} + {% if group.courses.all %} + <h4>{% trans 'Teachers and lessons in group' %} {{ group.name }}</h4> + {% include "alsijil/partials/register_teachers_and_subjects_table.html" with groups=group.as_list only %} + <div class="page-break"> </div> + {% endif %} + {% if group.child_groups.all %} + <h4>{% trans 'Teachers and lessons in child groups' %}</h4> + {% include "alsijil/partials/register_teachers_and_subjects_table.html" with groups=group.child_groups.all only %} + <div class="page-break"> </div> + {% endif %} + {% endif %} + + {% if include_person_overviews %} + {% for person in group.members_with_stats %} + {% include "alsijil/partials/person_overview.html" with person=person group=group %} + <div class="page-break"> </div> + {% endfor %} + {% endif %} + + {% if include_coursebook %} + {% include "alsijil/partials/register_coursebook.html" with group=group %} + {% endif %} + {% endfor %} +{% endblock %} diff --git a/aleksis/apps/alsijil/urls.py b/aleksis/apps/alsijil/urls.py index cd7367ce84973bb4eb62791e9ce5b2c7eb3a5f85..8017db1c94d063930ed462be021846841bce3d40 100644 --- a/aleksis/apps/alsijil/urls.py +++ b/aleksis/apps/alsijil/urls.py @@ -3,6 +3,7 @@ from django.urls import path from . import views urlpatterns = [ + path("print/groups/<path:ids>/", views.full_register_for_group, name="full_register_for_group"), path("group_roles/", views.GroupRoleListView.as_view(), name="group_roles"), path("group_roles/create/", views.GroupRoleCreateView.as_view(), name="create_group_role"), path( diff --git a/aleksis/apps/alsijil/views.py b/aleksis/apps/alsijil/views.py index 75f0e1b06d6dfe14380047fe8a6d1221109bcf21..c48c13eea04af8ab0c205ba851717a936cfe50a6 100644 --- a/aleksis/apps/alsijil/views.py +++ b/aleksis/apps/alsijil/views.py @@ -1,19 +1,19 @@ from typing import Any, Dict +from django.core.exceptions import BadRequest, PermissionDenied from django.db.models import Q from django.http import HttpRequest, HttpResponse from django.shortcuts import get_object_or_404, redirect from django.urls import reverse, reverse_lazy from django.utils import timezone from django.utils.decorators import method_decorator -from django.utils.http import url_has_allowed_host_and_scheme from django.utils.translation import gettext as _ from django.views.decorators.cache import never_cache from django.views.generic import DetailView from django_tables2 import SingleTableView from reversion.views import RevisionMixin -from rules.contrib.views import PermissionRequiredMixin, permission_required +from rules.contrib.views import PermissionRequiredMixin from aleksis.core.decorators import pwa_cache from aleksis.core.mixins import ( @@ -25,7 +25,7 @@ from aleksis.core.mixins import ( from aleksis.core.models import Group, PDFFile from aleksis.core.util import messages from aleksis.core.util.celery_progress import render_progress_page -from aleksis.core.util.core_helpers import has_person, objectgetter_optional +from aleksis.core.util.core_helpers import has_person from .forms import ( AssignGroupRoleForm, @@ -39,11 +39,24 @@ from .tables import ( from .tasks import generate_full_register_printout -@permission_required( - "alsijil.view_full_register_rule", fn=objectgetter_optional(Group, None, False) -) -def full_register_group(request: HttpRequest, id_: int) -> HttpResponse: - group = get_object_or_404(Group, pk=id_) +def full_register_for_group(request: HttpRequest, ids: str) -> HttpResponse: + """Show a configurable register printout as PDF for a group.""" + + def parse_get_param(name): + """Defaults to true""" + return request.GET.get(name) != "false" + + try: + ids = [int(id_) for id_ in ids.split("/")] + except ValueError as e: + raise BadRequest() from e + + groups = [] + for id_ in ids: + group = get_object_or_404(Group, pk=id_) + if not request.user.has_perm("alsijil.view_full_register_rule", group): + raise PermissionDenied() + groups.append(group) file_object = PDFFile.objects.create() if has_person(request): @@ -52,22 +65,26 @@ def full_register_group(request: HttpRequest, id_: int) -> HttpResponse: redirect_url = f"/pdfs/{file_object.pk}" - result = generate_full_register_printout.delay(group.pk, file_object.pk) + result = generate_full_register_printout.delay( + groups=ids, + file_object=file_object.pk, + include_cover=parse_get_param("cover"), + include_abbreviations=parse_get_param("abbreviations"), + include_members_table=parse_get_param("members_table"), + include_teachers_and_subjects_table=parse_get_param("teachers_and_subjects_table"), + include_person_overviews=parse_get_param("person_overviews"), + include_coursebook=parse_get_param("coursebook"), + ) back_url = request.GET.get("back", "") - back_url_is_safe = url_has_allowed_host_and_scheme( - url=back_url, - allowed_hosts={request.get_host()}, - require_https=request.is_secure(), - ) - if not back_url_is_safe: - back_url = reverse("my_groups") return render_progress_page( request, result, - title=_("Generate full register printout for {}").format(group), - progress_title=_("Generate full register printout …"), + title=_( + f"Generate register printout for {', '.join([group.short_name for group in groups])}" + ), + progress_title=_("Generate register printout …"), success_message=_("The printout has been generated successfully."), error_message=_("There was a problem while generating the printout."), redirect_on_success_url=redirect_url,