diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 282575f36d245ab7835f6c2e168528d401d76044..7f1a51b6ef491c545742e23cd631a761b25662cb 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -13,6 +13,7 @@ Changed ~~~~~~~ * Use new icon set inside of models and templates +* Run full register printout generation in background Fixed ~~~~~ diff --git a/aleksis/apps/alsijil/tasks.py b/aleksis/apps/alsijil/tasks.py new file mode 100644 index 0000000000000000000000000000000000000000..aa8de7b5a075b9a1ae37b129b1870482966577a5 --- /dev/null +++ b/aleksis/apps/alsijil/tasks.py @@ -0,0 +1,182 @@ +from copy import deepcopy +from datetime import date, timedelta + +from django.db.models import 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 aleksis.apps.chronos.models import Event, ExtraLesson, LessonPeriod +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 .models import ExcuseType, ExtraMark, LessonDocumentation, PersonalNote + + +@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) + 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) diff --git a/aleksis/apps/alsijil/templates/alsijil/print/full_register.html b/aleksis/apps/alsijil/templates/alsijil/print/full_register.html index 859f03d53da39edcf267c682de809c2fea6eb54e..c8d71ff3f6b2162814f72a83cfbc14fa8c6f0b1b 100644 --- a/aleksis/apps/alsijil/templates/alsijil/print/full_register.html +++ b/aleksis/apps/alsijil/templates/alsijil/print/full_register.html @@ -17,8 +17,8 @@ <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 request.site.preferences.theme__logo.url aleksis_banner %}" - alt="{{ request.site.preferences.general__title }} – Logo" class="max-size-600 center"> + <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> diff --git a/aleksis/apps/alsijil/views.py b/aleksis/apps/alsijil/views.py index 7b5ebec01cb9e1cff9677bb74bbfdf1ee99677e4..1aa04dd2acd2e5ce8e0a325b66eb51a9d78db348 100644 --- a/aleksis/apps/alsijil/views.py +++ b/aleksis/apps/alsijil/views.py @@ -1,6 +1,6 @@ from contextlib import nullcontext from copy import deepcopy -from datetime import date, datetime, timedelta +from datetime import datetime, timedelta from typing import Any, Dict, Optional from django.apps import apps @@ -37,10 +37,10 @@ from aleksis.core.mixins import ( AdvancedEditView, SuccessNextMixin, ) -from aleksis.core.models import Group, Person, SchoolTerm +from aleksis.core.models import Group, PDFFile, Person, SchoolTerm from aleksis.core.util import messages +from aleksis.core.util.celery_progress import render_progress_page from aleksis.core.util.core_helpers import get_site_preferences, objectgetter_optional -from aleksis.core.util.pdf import render_pdf from aleksis.core.util.predicates import check_global_permission from .filters import PersonalNoteFilter @@ -58,14 +58,7 @@ from .forms import ( RegisterObjectActionForm, SelectForm, ) -from .models import ( - ExcuseType, - ExtraMark, - GroupRole, - GroupRoleAssignment, - LessonDocumentation, - PersonalNote, -) +from .models import ExcuseType, ExtraMark, GroupRole, GroupRoleAssignment, PersonalNote from .tables import ( ExcuseTypeTable, ExtraMarkTable, @@ -74,6 +67,7 @@ from .tables import ( RegisterObjectSelectTable, RegisterObjectTable, ) +from .tasks import generate_full_register_printout from .util.alsijil_helpers import ( annotate_documentations, generate_list_of_all_register_objects, @@ -642,138 +636,36 @@ def week_view( "alsijil.view_full_register_rule", fn=objectgetter_optional(Group, None, False) ) def full_register_group(request: HttpRequest, id_: int) -> HttpResponse: - context = {} - group = get_object_or_404(Group, pk=id_) - 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) - 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 + file_object = PDFFile.objects.create() - 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) - - # 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, - ) - ) + redirect_url = reverse("redirect_to_pdf_file", args=[file_object.pk]) - 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) - register_objects_by_day.setdefault(day, []).append( - ( - event_copy, - sorted_documentations["event"].get(event.pk), - sorted_personal_notes["event"].get(event.pk, []), - None, - ) - ) + result = generate_full_register_printout.delay(group.pk, file_object.pk) - weeks = CalendarWeek.weeks_within( - group.school_term.date_start, - group.school_term.date_end, + 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(), ) - - 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) - ) - - 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", - ) + 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 …"), + success_message=_("The printout has been generated successfully."), + error_message=_("There was a problem while generating the printout."), + redirect_on_success_url=redirect_url, + back_url=back_url, + button_title=_("Download PDF"), + button_url=redirect_url, + button_icon="picture_as_pdf", ) - return render_pdf(request, "alsijil/print/full_register.html", context) @permission_required("alsijil.view_my_students_rule") diff --git a/pyproject.toml b/pyproject.toml index bb15d014a4509f016d7d5c7e940dcbc6bb249c08..a3a4aea8d9b8f3cfd286cab366900bcce9ee5b9c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -48,7 +48,7 @@ secondary = true [tool.poetry.dependencies] python = "^3.9" -aleksis-core = "^2.11" +aleksis-core = "^2.12" aleksis-app-chronos = "^2.2" aleksis-app-stoelindeling = { version = "^1.0", optional = true }