Skip to content
Snippets Groups Projects
Verified Commit 502836d4 authored by Jonathan Weth's avatar Jonathan Weth :keyboard:
Browse files

Add print view for regular timetables

diff --git a/aleksis/apps/lesrooster/managers.py b/aleksis/apps/lesrooster/managers.py
index 10703b0..634dfb2 100644
--- a/aleksis/apps/lesrooster/managers.py
+++ b/aleksis/apps/lesrooster/managers.py
@@ -1,6 +1,18 @@
-from django.db.models import QuerySet
+from datetime import time
+from typing import Iterable, Optional, Union

-from aleksis.core.managers import AlekSISBaseManagerWithoutMigrations, DateRangeQuerySetMixin
+from django.db.models import Max, Min, Q, QuerySet
+from django.db.models.functions import Coalesce
+
+from polymorphic.query import PolymorphicQuerySet
+
+from aleksis.apps.chronos.managers import TimetableType
+from aleksis.core.managers import (
+    AlekSISBaseManagerWithoutMigrations,
+    DateRangeQuerySetMixin,
+    PolymorphicBaseManager,
+)
+from aleksis.core.models import Group, Person, Room

 class ValidityRangeQuerySet(QuerySet, DateRangeQuerySetMixin):
@@ -9,3 +21,124 @@ class ValidityRangeQuerySet(QuerySet, DateRangeQuerySetMixin):

 class ValidityRangeManager(AlekSISBaseManagerWithoutMigrations):
     """Manager for validity ranges."""
+
+
+class SlotQuerySet(PolymorphicQuerySet):
+    def get_period_min(self) -> int:
+        """Get minimum period."""
+        return self.aggregate(period__min=Coalesce(Min("period"), 1)).get("period__min")
+
+    def get_period_max(self) -> int:
+        """Get maximum period."""
+        return self.aggregate(period__max=Coalesce(Max("period"), 7)).get("period__max")
+
+    def get_time_min(self) -> time | None:
+        """Get minimum time."""
+        return self.aggregate(Min("time_start")).get("time_start__min")
+
+    def get_time_max(self) -> time | None:
+        """Get maximum time."""
+        return self.aggregate(Max("time_end")).get("time_end__max")
+
+    def get_weekday_min(self) -> int:
+        """Get minimum weekday."""
+        return self.aggregate(weekday__min=Coalesce(Min("weekday"), 0)).get("weekday__min")
+
+    def get_weekday_max(self) -> int:
+        """Get maximum weekday."""
+        return self.aggregate(weekday__max=Coalesce(Max("weekday"), 6)).get("weekday__max")
+
+
+class SlotManager(PolymorphicBaseManager):
+    pass
+
+
+class LessonQuerySet(QuerySet):
+    def filter_participant(self, person: Union[Person, int]) -> "LessonQuerySet":
+        """Filter for all lessons a participant (student) attends."""
+        return self.filter(course__groups__members=person)
+
+    def filter_group(self, group: Union[Group, int]) -> "LessonQuerySet":
+        """Filter for all lessons a group (class) regularly attends."""
+        if isinstance(group, int):
+            group = Group.objects.get(pk=group)
+
+        return self.filter(
+            Q(course__groups=group) | Q(course__groups__parent_groups=group)
+        ).distinct()
+
+    def filter_groups(self, groups: Iterable[Group]) -> "LessonQuerySet":
+        """Filter for all lessons one of the groups regularly attends."""
+        return self.filter(
+            Q(course__groups__in=groups) | Q(course__groups__parent_groups__in=groups)
+        )
+
+    def filter_teacher(self, teacher: Union[Person, int]) -> "LessonQuerySet":
+        """Filter for all lessons given by a certain teacher."""
+        return self.filter(teachers=teacher)
+
+    def filter_room(self, room: Union[Room, int]) -> "LessonQuerySet":
+        """Filter for all lessons taking part in a certain room."""
+        return self.filter(rooms=room)
+
+    def filter_from_type(
+        self,
+        type_: TimetableType,
+        obj: Union[Person, Group, Room, int],
+    ) -> Optional["LessonQuerySet"]:
+        """Filter lessons for a group, teacher or room by provided type."""
+        if type_ == TimetableType.GROUP:
+            return self.filter_group(obj)
+        elif type_ == TimetableType.TEACHER:
+            return self.filter_teacher(obj)
+        elif type_ == TimetableType.ROOM:
+            return self.filter_room(obj)
+        else:
+            return None
+
+    def filter_from_person(self, person: Person) -> Optional["LessonQuerySet"]:
+        """Filter lessons for a person."""
+        type_ = person.timetable_type
+
+        if type_ == TimetableType.TEACHER:
+            return self.filter_teacher(person)
+        elif type_ == TimetableType.GROUP:
+            return self.filter_participant(person)
+        else:
+            return None
+
+
+class LessonManager(AlekSISBaseManagerWithoutMigrations):
+    pass
+
+
+class SupervisionQuerySet(QuerySet):
+    def filter_teacher(self, teacher: Union[Person, int]) -> "SupervisionQuerySet":
+        """Filter for all supervisions done by a certain teacher."""
+        return self.filter(teachers=teacher)
+
+    def filter_room(self, room: Union[Room, int]) -> "SupervisionQuerySet":
+        """Filter for all supervisions taking part in a certain room."""
+        return self.filter(rooms=room)
+
+    def filter_from_type(
+        self,
+        type_: TimetableType,
+        obj: Union[Person, Group, Room, int],
+    ) -> Optional["SupervisionQuerySet"]:
+        """Filter supervisions for a eacher or room by provided type."""
+        if type_ == TimetableType.TEACHER:
+            return self.filter_teacher(obj)
+        elif type_ == TimetableType.ROOM:
+            return self.filter_room(obj)
+        else:
+            return None
+
+    def filter_from_person(self, person: Person) -> Optional["SupervisionQuerySet"]:
+        """Filter supervisions for a person."""
+
+        return self.filter_teacher(person)
+
+
+class SupervisionManager(AlekSISBaseManagerWithoutMigrations):
+    pass
diff --git a/aleksis/apps/lesrooster/models.py b/aleksis/apps/lesrooster/models.py
index 17c2dc6..73a1e1a 100644
--- a/aleksis/apps/lesrooster/models.py
+++ b/aleksis/apps/lesrooster/models.py
@@ -1,5 +1,5 @@
 import logging
-from datetime import date, datetime, timedelta
+from datetime import date, datetime, timedelta, time
 from typing import Optional, Union

 from django.core.exceptions import ValidationError
@@ -25,7 +25,16 @@ from aleksis.core.mixins import ExtensibleModel, ExtensiblePolymorphicModel, Glo
 from aleksis.core.models import Group, Holiday, Person, Room, SchoolTerm
 from aleksis.core.util.celery_progress import ProgressRecorder, render_progress_page

-from .managers import ValidityRangeManager, ValidityRangeQuerySet
+from .managers import (
+    LessonManager,
+    LessonQuerySet,
+    SlotManager,
+    SlotQuerySet,
+    SupervisionManager,
+    SupervisionQuerySet,
+    ValidityRangeManager,
+    ValidityRangeQuerySet,
+)

 class ValidityRangeStatus(models.TextChoices):
@@ -222,6 +231,37 @@ class TimeGrid(ExtensibleModel):
         null=True,
     )

+    @property
+    def times_dict(self) -> dict[int, tuple[datetime, datetime]]:
+        slots = {}
+        for slot in self.slots.all():
+            slots[slot.period] = (slot.time_start, slot.time_end)
+        return slots
+
+    @property
+    def period_min(self) -> int:
+        return self.slots.get_period_min()
+
+    @property
+    def period_max(self) -> int:
+        return self.slots.get_period_max()
+
+    @property
+    def time_min(self) -> time | None:
+        return self.slots.get_time_min()
+
+    @property
+    def time_max(self) -> time | None:
+        return self.slots.get_time_max()
+
+    @property
+    def weekday_min(self) -> int:
+        return self.slots.get_weekday_min()
+
+    @property
+    def weekday_max(self) -> int:
+        return self.slots.get_weekday_max()
+
     def __str__(self):
         if self.group:
             return f"{self.validity_range}: {self.group}"
@@ -243,6 +283,8 @@ class TimeGrid(ExtensibleModel):
 class Slot(ExtensiblePolymorphicModel):
     """A slot is a time period in which a lesson can take place."""

+    objects = SlotManager.from_queryset(SlotQuerySet)()
+
     WEEKDAY_CHOICES = i18n_day_name_choices_lazy()
     WEEKDAY_CHOICES_SHORT = i18n_day_abbr_choices_lazy()

@@ -346,6 +388,8 @@ class Slot(ExtensiblePolymorphicModel):
 class Lesson(TeacherPropertiesMixin, RoomPropertiesMixin, ExtensibleModel):
     """A lesson represents a single teaching event."""

+    objects = LessonManager.from_queryset(LessonQuerySet)()
+
     lesson_event = models.OneToOneField(
         LessonEvent,
         on_delete=models.SET_NULL,
@@ -501,6 +545,8 @@ class BreakSlot(Slot):
 class Supervision(TeacherPropertiesMixin, RoomPropertiesMixin, ExtensibleModel):
     """A supervision is a time period in which a teacher supervises a room."""

+    objects = SupervisionManager.from_queryset(SupervisionQuerySet)()
+
     supervision_event = models.OneToOneField(
         SupervisionEvent,
         on_delete=models.SET_NULL,
diff --git a/aleksis/apps/lesrooster/static/css/lesrooster/timetable_print.css b/aleksis/apps/lesrooster/static/css/lesrooster/timetable_print.css
new file mode 100644
index 0000000..9bed357
--- /dev/null
+++ b/aleksis/apps/lesrooster/static/css/lesrooster/timetable_print.css
@@ -0,0 +1,63 @@
+.timetable-plan .row,
+.timetable-plan .col {
+  display: flex;
+  padding: 0;
+}
+
+.timetable-plan .row {
+  margin-bottom: 0;
+}
+
+.lesson-card,
+.timetable-title-card {
+  display: flex;
+  flex-grow: 1;
+  min-height: 40px;
+  box-shadow: none;
+  border: 1px solid black;
+  margin: -1px -1px 0 0;
+  border-radius: 0;
+  font-size: 11px;
+}
+
+.timetable-title-card .card-title {
+  margin-bottom: 0 !important;
+}
+
+.supervision-card {
+  min-height: 10px;
+  border-left: none;
+  border-right: none;
+}
+
+.lesson-card .card-content {
+  padding: 0;
+  text-align: center;
+  width: 100%;
+  height: 100%;
+  display: flex;
+  flex-direction: column;
+}
+
+.lesson-card .card-content > div {
+  padding: 0;
+  flex: auto;
+  width: 100%;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+}
+
+.timetable-title-card .card-content {
+  padding: 7px;
+  text-align: center;
+  width: 100%;
+}
+
+.lesson-card a {
+  color: inherit;
+}
+
+.card .card-title {
+  font-size: 18px;
+}
diff --git a/aleksis/apps/lesrooster/templates/lesrooster/partials/elements.html b/aleksis/apps/lesrooster/templates/lesrooster/partials/elements.html
new file mode 100644
index 0000000..00646c2
--- /dev/null
+++ b/aleksis/apps/lesrooster/templates/lesrooster/partials/elements.html
@@ -0,0 +1,7 @@
+<div class="card lesson-card">
+  <div class="card-content">
+    {% for element in elements %}
+        {% include "lesrooster/partials/lesson.html" with lesson=element %}
+    {% endfor %}
+  </div>
+</div>
diff --git a/aleksis/apps/lesrooster/templates/lesrooster/partials/group.html b/aleksis/apps/lesrooster/templates/lesrooster/partials/group.html
new file mode 100644
index 0000000..aba12c5
--- /dev/null
+++ b/aleksis/apps/lesrooster/templates/lesrooster/partials/group.html
@@ -0,0 +1 @@
+  {{ item.short_name }}{% if not forloop.last %},{% endif %}
diff --git a/aleksis/apps/lesrooster/templates/lesrooster/partials/groups.html b/aleksis/apps/lesrooster/templates/lesrooster/partials/groups.html
new file mode 100644
index 0000000..6ffcaec
--- /dev/null
+++ b/aleksis/apps/lesrooster/templates/lesrooster/partials/groups.html
@@ -0,0 +1,5 @@
+{% if groups.count == 1 and groups.0.parent_groups.all and request.site.preferences.lesrooster__use_parent_groups %}
+  {% include "lesrooster/partials/groups_part.html" with groups=groups.0.parent_groups.all no_collapsible=no_collapsible %}
+{% else %}
+  {% include "lesrooster/partials/groups_part.html" with groups=groups no_collapsible=no_collapsible %}
+{% endif %}
diff --git a/aleksis/apps/lesrooster/templates/lesrooster/partials/groups_part.html b/aleksis/apps/lesrooster/templates/lesrooster/partials/groups_part.html
new file mode 100644
index 0000000..97a9f04
--- /dev/null
+++ b/aleksis/apps/lesrooster/templates/lesrooster/partials/groups_part.html
@@ -0,0 +1,7 @@
+{% if groups.count > request.site.preferences.lesrooster__shorten_groups_limit and request.user.person.preferences.lesrooster__shorten_groups and not no_collapsible %}
+  {% include "components/text_collapsible.html" with template="lesrooster/partials/group.html" qs=groups %}
+{% else %}
+  {% for group in groups %}
+    {% include "lesrooster/partials/group.html" with item=group %}
+  {% endfor %}
+{% endif %}
diff --git a/aleksis/apps/lesrooster/templates/lesrooster/partials/lesson.html b/aleksis/apps/lesrooster/templates/lesrooster/partials/lesson.html
new file mode 100644
index 0000000..969f71f
--- /dev/null
+++ b/aleksis/apps/lesrooster/templates/lesrooster/partials/lesson.html
@@ -0,0 +1,25 @@
+{% load i18n %}
+
+<div style="{% include "lesrooster/partials/subject_colour.html" with subject=lesson.subject %}">
+  <p>
+      {# Teacher or room > Display classes #}
+      {% if type.value == "teacher" or type.value == "room" %}
+        {% if lesson.course.groups %}
+          {% include "lesrooster/partials/groups.html" with groups=lesson.course.groups.all %}
+        {% endif %}
+      {% endif %}
+
+      {# Class or room > Display teacher #}
+      {% if type.value == "room" or type.value == "group" %}
+        {% include "lesrooster/partials/teachers.html" with teachers=lesson.teachers.all %}
+      {% endif %}
+
+      {# Display subject #}
+      {% include "lesrooster/partials/subject.html" with subject=lesson.subject %}
+
+      {# Teacher or class > Display room #}
+      {% if type.value == "teacher" or type.value == "group" %}
+        {% include "lesrooster/partials/rooms.html" with rooms=lesson.rooms.all %}
+      {% endif %}
+  </p>
+</div>
diff --git a/aleksis/apps/lesrooster/templates/lesrooster/partials/rooms.html b/aleksis/apps/lesrooster/templates/lesrooster/partials/rooms.html
new file mode 100644
index 0000000..081540e
--- /dev/null
+++ b/aleksis/apps/lesrooster/templates/lesrooster/partials/rooms.html
@@ -0,0 +1,3 @@
+{% for room in rooms %}
+  {{ room.short_name }}{% if not forloop.last %},{% endif %}
+{% endfor %}
diff --git a/aleksis/apps/lesrooster/templates/lesrooster/partials/slot_time.html b/aleksis/apps/lesrooster/templates/lesrooster/partials/slot_time.html
new file mode 100644
index 0000000..b190c2e
--- /dev/null
+++ b/aleksis/apps/lesrooster/templates/lesrooster/partials/slot_time.html
@@ -0,0 +1,18 @@
+{% load data_helpers %}
+
+<div class="card timetable-title-card">
+  <div class="card-content">
+
+    {# Lesson number #}
+    <div class="card-title left">
+      {{ slot.period }}.
+    </div>
+
+    {# Time dimension of lesson #}
+    <div class="right timetable-time grey-text text-darken-2">
+        <span>{{ slot.time_start|time }}</span>
+        <br/>
+        <span>{{ slot.time_end|time }}</span>
+    </div>
+  </div>
+</div>
diff --git a/aleksis/apps/lesrooster/templates/lesrooster/partials/subject.html b/aleksis/apps/lesrooster/templates/lesrooster/partials/subject.html
new file mode 100644
index 0000000..1b565a2
--- /dev/null
+++ b/aleksis/apps/lesrooster/templates/lesrooster/partials/subject.html
@@ -0,0 +1,3 @@
+<strong>
+  {{ subject.short_name }}
+</strong>
diff --git a/aleksis/apps/lesrooster/templates/lesrooster/partials/subject_colour.html b/aleksis/apps/lesrooster/templates/lesrooster/partials/subject_colour.html
new file mode 100644
index 0000000..4cead55
--- /dev/null
+++ b/aleksis/apps/lesrooster/templates/lesrooster/partials/subject_colour.html
@@ -0,0 +1,6 @@
+{% if subject.colour_fg %}
+  color: {{ subject.colour_fg }};
+{% endif %}
+{% if subject.colour_bg and subject.colour_bg != subject.colour_fg %}
+  background-color: {{ subject.colour_bg }};
+{% endif %}
diff --git a/aleksis/apps/lesrooster/templates/lesrooster/partials/supervision.html b/aleksis/apps/lesrooster/templates/lesrooster/partials/supervision.html
new file mode 100644
index 0000000..6abd44e
--- /dev/null
+++ b/aleksis/apps/lesrooster/templates/lesrooster/partials/supervision.html
@@ -0,0 +1,15 @@
+{% load i18n %}
+
+<div class="card lesson-card supervision-card">
+  <div class="card-content">
+    {% if supervision %}
+      <div>
+        <p>
+          <strong>{% trans "Supervision" %}</strong>
+          {% include "lesrooster/partials/rooms.html" with rooms=supervision.rooms.all %}
+          {% include "lesrooster/partials/teachers.html" with teachers=supervision.teachers.all %}
+        </p>
+      </div>
+    {% endif %}
+  </div>
+</div>
diff --git a/aleksis/apps/lesrooster/templates/lesrooster/partials/teachers.html b/aleksis/apps/lesrooster/templates/lesrooster/partials/teachers.html
new file mode 100644
index 0000000..73afb6e
--- /dev/null
+++ b/aleksis/apps/lesrooster/templates/lesrooster/partials/teachers.html
@@ -0,0 +1,3 @@
+{% for teacher in teachers %}
+          {{ teacher.short_name }}{% if not forloop.last %},{% endif %}
+{% endfor %}
diff --git a/aleksis/apps/lesrooster/templates/lesrooster/timetable_print.html b/aleksis/apps/lesrooster/templates/lesrooster/timetable_print.html
new file mode 100644
index 0000000..fd4de7a
--- /dev/null
+++ b/aleksis/apps/lesrooster/templates/lesrooster/timetable_print.html
@@ -0,0 +1,56 @@
+{% extends 'core/base_print.html' %}
+
+{% load data_helpers static i18n %}
+
+{% block extra_head %}
+  <link rel="stylesheet" href="{% static 'css/lesrooster/timetable_print.css' %}">
+{% endblock %}
+
+{% block page_title %}
+  {% trans "Timetable" %} <i>{{ el.short_name }}</i>
+{% endblock %}
+{% block content %}
+
+  <div class="timetable-plan">
+    {#  Week days #}
+    <div class="row">
+      <div class="col s2">
+
+      </div>
+      {% for weekday in weekdays %}
+        <div class="col s2">
+          <div class="card timetable-title-card">
+            <div class="card-content">
+              <span class="card-title">
+                {{ weekday.1 }}
+              </span>
+            </div>
+          </div>
+        </div>
+      {% endfor %}
+    </div>
+
+    {% for row in timetable %}
+      <div class="row">
+        <div class="col s2">
+          {% if row.type == "period" %}
+            {% include "lesrooster/partials/slot_time.html" with slot=row.slot %}
+          {% endif %}
+        </div>
+
+        {% for col in row.cols %}
+          {# A lesson #}
+          <div class="col s2">
+            {% if col.type == "period" %}
+              {% include "lesrooster/partials/elements.html" with elements=col.col %}
+            {% else %}
+              {% include "lesrooster/partials/supervision.html" with supervision=col.col %}
+            {% endif %}
+          </div>
+        {% endfor %}
+      </div>
+    {% endfor %}
+  </div>
+
+  <small>{% trans "Validity range" %}: {{ time_grid.validity_range.date_start }}–{{ time_grid.validity_range.date_end }}</small>
+{% endblock %}
diff --git a/aleksis/apps/lesrooster/urls.py b/aleksis/apps/lesrooster/urls.py
new file mode 100644
index 0000000..5794d92
--- /dev/null
+++ b/aleksis/apps/lesrooster/urls.py
@@ -0,0 +1,11 @@
+from django.urls import path
+
+from . import views
+
+urlpatterns = [
+    path(
+        "timetable/<int:time_grid>/<str:type_>/<int:pk>/print/",
+        views.print_timetable,
+        name="timetable_print",
+    ),
+]
diff --git a/aleksis/apps/lesrooster/util/build.py b/aleksis/apps/lesrooster/util/build.py
new file mode 100644
index 0000000..217d7ec
--- /dev/null
+++ b/aleksis/apps/lesrooster/util/build.py
@@ -0,0 +1,116 @@
+from collections import OrderedDict
+from typing import Union
+
+from aleksis.apps.chronos.managers import TimetableType
+from aleksis.apps.lesrooster.models import BreakSlot, Lesson, Slot, Supervision, TimeGrid
+from aleksis.core.models import Group, Person, Room
+
+
+def build_timetable(
+    time_grid: TimeGrid,
+    type_: Union[TimetableType, str],
+    obj: Union[Group, Room, Person],
+) -> list | None:
+    """Build regular timetable for the given time grid."""
+    is_person = False
+    if type_ == "person":
+        is_person = True
+        type_ = obj.timetable_type
+
+    if type_ is None:
+        return None
+
+    slots = Slot.objects.filter(time_grid=time_grid).order_by("weekday", "time_start")
+    lesson_periods_per_slot = OrderedDict()
+    supervisions_per_slot = OrderedDict()
+    slot_map = OrderedDict()
+    for slot in slots:
+        lesson_periods_per_slot[slot] = []
+        supervisions_per_slot[slot] = []
+        slot_map.setdefault(slot.weekday, []).append(slot)
+
+    max_slots_weekday, max_slots = max(slot_map.items(), key=lambda x: len(x[1]))
+    max_slots = len(max_slots)
+
+    # Get matching lessons
+    lessons = Lesson.objects.filter(slot_start__time_grid=time_grid)
+
+    lessons = (
+        lessons.filter_from_person(obj) if is_person else lessons.filter_from_group(type_, obj)
+    )
+
+    # Sort lesson periods in a dict
+    for lesson in lessons:
+        print(
+            lesson.subject,
+            Slot.objects.filter(
+                time_grid=time_grid,
+                weekday=lesson.slot_start.weekday,
+                time_start__gte=lesson.slot_start.time_start,
+                time_end__lte=lesson.slot_end.time_end,
+            ).not_instance_of(BreakSlot),
+        )
+        for slot in Slot.objects.filter(
+            time_grid=time_grid,
+            weekday=lesson.slot_start.weekday,
+            time_start__gte=lesson.slot_start.time_start,
+            time_end__lte=lesson.slot_end.time_end,
+        ).not_instance_of(BreakSlot):
+            lesson_periods_per_slot[slot].append(lesson)
+
+    # Get matching supervisions
+    needed_break_slots = []
+
+    supervisions = Supervision.objects.filter(break_slot__time_grid=time_grid).all()
+    if is_person:
+        supervisions = supervisions.filter_from_person(obj)
+    else:
+        supervisions = supervisions.filter_from_type(type_, obj)
+
+    if supervisions:
+        for supervision in supervisions:
+            if supervision.break_slot not in needed_break_slots:
+                needed_break_slots.append(supervision.break_slot)
+
+            supervisions_per_slot[supervision.break_slot] = supervision
+
+    rows = []
+    for slot_idx in range(max_slots):  # period is period after break
+        left_slot = slot_map[max_slots_weekday][slot_idx]
+
+        if isinstance(left_slot, BreakSlot):
+            row = {"type": "break", "slot": left_slot}
+        else:
+            row = {
+                "type": "period",
+                "slot": left_slot,
+            }
+
+        cols = []
+
+        for weekday in range(time_grid.weekday_min, time_grid.weekday_max + 1):
+            if slot_idx > len(slot_map[weekday]) - 1:
+                continue
+            actual_slot = slot_map[weekday][slot_idx]
+
+            if isinstance(actual_slot, BreakSlot):
+                col = {"type": "break", "col": supervisions_per_slot.get(actual_slot)}
+
+            else:
+                print(lesson_periods_per_slot[actual_slot])
+                col = {
+                    "type": "period",
+                    "col": (
+                        lesson_periods_per_slot[actual_slot]
+                        if actual_slot in lesson_periods_per_slot
+                        else []
+                    ),
+                }
+
+            cols.append(col)
+
+        row["cols"] = cols
+
+        rows.append(row)
+
+    return rows
diff --git a/aleksis/apps/lesrooster/views.py b/aleksis/apps/lesrooster/views.py
new file mode 100644
index 0000000..d6bd303
--- /dev/null
+++ b/aleksis/apps/lesrooster/views.py
@@ -0,0 +1,41 @@
+from django.http import HttpRequest, HttpResponse, HttpResponseNotFound
+from django.shortcuts import get_object_or_404
+
+from rules.contrib.views import permission_required
+
+from aleksis.apps.chronos.managers import TimetableType
+from aleksis.apps.chronos.util.chronos_helpers import get_el_by_pk
+from aleksis.apps.lesrooster.models import Slot, TimeGrid
+from aleksis.apps.lesrooster.util.build import build_timetable
+from aleksis.core.util.pdf import render_pdf
+
+
+@permission_required("chronos.view_timetable_rule", fn=get_el_by_pk)
+def print_timetable(
+    request: HttpRequest,
+    time_grid: int,
+    type_: str,
+    pk: int,
+) -> HttpResponse:
+    """View a selected timetable for a person, group or room."""
+    context = {}
+
+    time_grid = get_object_or_404(TimeGrid, pk=time_grid)
+    el = get_el_by_pk(request, type_, pk, prefetch=True)
+
+    if isinstance(el, HttpResponseNotFound):
+        return HttpResponseNotFound()
+
+    type_ = TimetableType.from_string(type_)
+
+    timetable = build_timetable(time_grid, type_, el)
+    context["timetable"] = timetable
+
+    context["weekdays"] = Slot.WEEKDAY_CHOICES[time_grid.weekday_min : time_grid.weekday_max + 1]
+
+    context["time_grid"] = time_grid
+    context["type"] = type_
+    context["pk"] = pk
+    context["el"] = el
+
+    return render_pdf(request, "lesrooster/timetable_print.html", context)
parent 604247f6
No related branches found
No related tags found
Loading
Showing
with 558 additions and 4 deletions
Loading
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment