From 4822847b541f99d4177b4e5e8cc379073811eebb Mon Sep 17 00:00:00 2001
From: Jonathan Weth <git@jonathanweth.de>
Date: Sat, 25 Jan 2025 16:25:48 +0100
Subject: [PATCH] Add coursebook printout for single persons

---
 CHANGELOG.rst                                 |  5 ++
 .../coursebook/CoursebookPrintDialog.vue      |  2 +-
 .../statistics/StatisticsForPersonPage.vue    | 12 +++-
 aleksis/apps/alsijil/frontend/index.js        | 10 ++-
 aleksis/apps/alsijil/tasks.py                 | 67 ++++++++++++++++++-
 .../alsijil/print/register_for_group.html     |  2 +-
 .../alsijil/print/register_for_person.html    | 15 +++++
 aleksis/apps/alsijil/urls.py                  |  5 +-
 aleksis/apps/alsijil/views.py                 | 47 +++++++++++--
 9 files changed, 152 insertions(+), 13 deletions(-)
 create mode 100644 aleksis/apps/alsijil/templates/alsijil/print/register_for_person.html

diff --git a/CHANGELOG.rst b/CHANGELOG.rst
index b6443616e..b1000af70 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
+~~~~~
+
+* Printout with person overview including all statistics.
+
 `4.0.0.dev9`_ - 2024-12-07
 --------------------------
 
diff --git a/aleksis/apps/alsijil/frontend/components/coursebook/CoursebookPrintDialog.vue b/aleksis/apps/alsijil/frontend/components/coursebook/CoursebookPrintDialog.vue
index 8a9058d6d..52444d0e5 100644
--- a/aleksis/apps/alsijil/frontend/components/coursebook/CoursebookPrintDialog.vue
+++ b/aleksis/apps/alsijil/frontend/components/coursebook/CoursebookPrintDialog.vue
@@ -157,7 +157,7 @@ export default {
     },
     print() {
       this.$router.push({
-        name: "alsijil.coursebook_print",
+        name: "alsijil.coursebookPrintGroups",
         params: {
           groupIds: this.selectedGroups,
         },
diff --git a/aleksis/apps/alsijil/frontend/components/coursebook/statistics/StatisticsForPersonPage.vue b/aleksis/apps/alsijil/frontend/components/coursebook/statistics/StatisticsForPersonPage.vue
index c9c8bcd3c..84e63a495 100644
--- a/aleksis/apps/alsijil/frontend/components/coursebook/statistics/StatisticsForPersonPage.vue
+++ b/aleksis/apps/alsijil/frontend/components/coursebook/statistics/StatisticsForPersonPage.vue
@@ -257,8 +257,16 @@
         v-model="$root.activeSchoolTerm"
         color="secondary"
       />
-      <!-- TODO: add functionality -->
-      <v-btn v-if="toolbar" icon color="primary" disabled>
+      <v-btn
+        v-if="toolbar"
+        icon
+        color="primary"
+        :to="{
+          name: 'alsijil.coursebookPrintPerson',
+          params: { id: personId },
+        }"
+        target="_blank"
+      >
         <v-icon>$print</v-icon>
       </v-btn>
       <FabButton v-else icon-text="$print" i18n-key="actions.print" disabled />
diff --git a/aleksis/apps/alsijil/frontend/index.js b/aleksis/apps/alsijil/frontend/index.js
index 18e0f68ea..13967281f 100644
--- a/aleksis/apps/alsijil/frontend/index.js
+++ b/aleksis/apps/alsijil/frontend/index.js
@@ -95,7 +95,15 @@ export default {
     {
       path: "print/groups/:groupIds+/",
       component: () => import("aleksis.core/components/LegacyBaseTemplate.vue"),
-      name: "alsijil.coursebook_print",
+      name: "alsijil.coursebookPrintGroups",
+      props: {
+        byTheGreatnessOfTheAlmightyAleksolotlISwearIAmWorthyOfUsingTheLegacyBaseTemplate: true,
+      },
+    },
+    {
+      path: "print/person/:id(\\d+)?/",
+      component: () => import("aleksis.core/components/LegacyBaseTemplate.vue"),
+      name: "alsijil.coursebookPrintPerson",
       props: {
         byTheGreatnessOfTheAlmightyAleksolotlISwearIAmWorthyOfUsingTheLegacyBaseTemplate: true,
       },
diff --git a/aleksis/apps/alsijil/tasks.py b/aleksis/apps/alsijil/tasks.py
index 355396dcb..47fd9074c 100644
--- a/aleksis/apps/alsijil/tasks.py
+++ b/aleksis/apps/alsijil/tasks.py
@@ -9,7 +9,7 @@ from celery.states import SUCCESS
 
 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.models import Group, PDFFile, Person, SchoolTerm
 from aleksis.core.util.celery_progress import ProgressRecorder, recorded_task
 from aleksis.core.util.pdf import generate_pdf_from_template
 
@@ -18,7 +18,7 @@ from .util.statistics import StatisticsBuilder
 
 
 @recorded_task
-def generate_full_register_printout(
+def generate_groups_register_printout(
     groups: list[int],
     file_object: int,
     recorder: ProgressRecorder,
@@ -138,3 +138,66 @@ def generate_full_register_printout(
             raise Exception(_("PDF generation failed"))
 
     recorder.set_progress(5 + len(groups), _number_of_steps)
+
+
+@recorded_task
+def generate_person_register_printout(
+    person: int,
+    school_term: int,
+    file_object: int,
+    recorder: ProgressRecorder,
+):
+    """Generate a register printout as PDF for a person."""
+
+    context = {}
+
+    _number_of_steps = 4
+
+    recorder.set_progress(1, _number_of_steps, _("Loading data ..."))
+
+    person = Person.objects.get(pk=person)
+    school_term = SchoolTerm.objects.get(pk=school_term)
+
+    doc_query_set = Documentation.objects.select_related("subject").prefetch_related("teachers")
+
+    statistics = (
+        (
+            StatisticsBuilder(Person.objects.filter(id=person.id))
+            .use_from_school_term(school_term)
+            .annotate_statistics()
+        )
+        .prefetch_relevant_participations(documentation_with_details=doc_query_set)
+        .prefetch_relevant_personal_notes(documentation_with_details=doc_query_set)
+        .build()
+        .first()
+    )
+
+    context["person"] = statistics
+
+    context["school_term"] = school_term
+
+    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()
+
+    recorder.set_progress(2, _number_of_steps, _("Generating template ..."))
+
+    file_object, result = generate_pdf_from_template(
+        "alsijil/print/register_for_person.html",
+        context,
+        file_object=PDFFile.objects.get(pk=file_object),
+    )
+
+    recorder.set_progress(3, _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(4, _number_of_steps)
diff --git a/aleksis/apps/alsijil/templates/alsijil/print/register_for_group.html b/aleksis/apps/alsijil/templates/alsijil/print/register_for_group.html
index 225763327..8e395b723 100644
--- a/aleksis/apps/alsijil/templates/alsijil/print/register_for_group.html
+++ b/aleksis/apps/alsijil/templates/alsijil/print/register_for_group.html
@@ -42,7 +42,7 @@
 
     {% if include_person_overviews %}
       {% for person in group.members_with_stats %}
-        {% include "alsijil/partials/person_overview.html" with person=person group=group %}
+        {% include "alsijil/partials/person_overview.html" with person=person %}
         <div class="page-break">&nbsp;</div>
       {% endfor %}
     {% endif %}
diff --git a/aleksis/apps/alsijil/templates/alsijil/print/register_for_person.html b/aleksis/apps/alsijil/templates/alsijil/print/register_for_person.html
new file mode 100644
index 000000000..f22d62060
--- /dev/null
+++ b/aleksis/apps/alsijil/templates/alsijil/print/register_for_person.html
@@ -0,0 +1,15 @@
+{% extends "core/base_print.html" %}
+
+{% load static i18n %}
+
+{% block page_title %}
+  {% trans "Class Register" %} · {{ school_term.name }}
+{% endblock %}
+
+{% block extra_head %}
+  <link rel="stylesheet" href="{% static 'css/alsijil/full_register.css' %}"/>
+{% endblock %}
+
+{% block content %}
+      {% include "alsijil/partials/person_overview.html" with person=person %}
+{% endblock %}
diff --git a/aleksis/apps/alsijil/urls.py b/aleksis/apps/alsijil/urls.py
index 8017db1c9..e0dfc61b0 100644
--- a/aleksis/apps/alsijil/urls.py
+++ b/aleksis/apps/alsijil/urls.py
@@ -3,7 +3,10 @@ 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(
+        "print/groups/<path:ids>/", views.groups_register_printout, name="full_register_for_group"
+    ),
+    path("print/person/<int:pk>/", views.person_register_printout, name="full_register_for_person"),
     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 71b4cbdd6..3e6839203 100644
--- a/aleksis/apps/alsijil/views.py
+++ b/aleksis/apps/alsijil/views.py
@@ -22,10 +22,10 @@ from aleksis.core.mixins import (
     AdvancedEditView,
     SuccessNextMixin,
 )
-from aleksis.core.models import Group, PDFFile
+from aleksis.core.models import Group, PDFFile, Person
 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
+from aleksis.core.util.core_helpers import get_active_school_term, has_person
 
 from .forms import (
     AssignGroupRoleForm,
@@ -36,10 +36,10 @@ from .models import GroupRole, GroupRoleAssignment
 from .tables import (
     GroupRoleTable,
 )
-from .tasks import generate_full_register_printout
+from .tasks import generate_groups_register_printout, generate_person_register_printout
 
 
-def full_register_for_group(request: HttpRequest, ids: str) -> HttpResponse:
+def groups_register_printout(request: HttpRequest, ids: str) -> HttpResponse:
     """Show a configurable register printout as PDF for a group."""
 
     def parse_get_param(name):
@@ -65,7 +65,7 @@ def full_register_for_group(request: HttpRequest, ids: str) -> HttpResponse:
 
     redirect_url = f"/pdfs/{file_object.pk}"
 
-    result = generate_full_register_printout.delay(
+    result = generate_groups_register_printout.delay(
         groups=ids,
         file_object=file_object.pk,
         include_cover=parse_get_param("cover"),
@@ -95,6 +95,43 @@ def full_register_for_group(request: HttpRequest, ids: str) -> HttpResponse:
     )
 
 
+def person_register_printout(request: HttpRequest, pk: int) -> HttpResponse:
+    """Show a statistics printout as PDF for a person."""
+
+    person = get_object_or_404(Person, pk=pk)
+    school_term = get_active_school_term(request)
+    if not request.user.has_perm("alsijil.view_person_statistics_rule", person) or not school_term:
+        raise PermissionDenied()
+
+    file_object = PDFFile.objects.create()
+    file_object.person = request.user.person
+    file_object.save()
+
+    redirect_url = f"/pdfs/{file_object.pk}"
+
+    result = generate_person_register_printout.delay(
+        person=person.id,
+        school_term=school_term.id,
+        file_object=file_object.pk,
+    )
+
+    back_url = request.GET.get("back", "")
+
+    return render_progress_page(
+        request,
+        result,
+        title=_(f"Generate register printout for {person.full_name}"),
+        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,
+        back_url=back_url,
+        button_title=_("Download PDF"),
+        button_url=redirect_url,
+        button_icon="picture_as_pdf",
+    )
+
+
 @method_decorator(pwa_cache, "dispatch")
 class GroupRoleListView(PermissionRequiredMixin, SingleTableView):
     """Table of all group roles."""
-- 
GitLab