Commits on Source (11)
-
Jonathan Weth authored
diff --git a/aleksis/apps/lesrooster/frontend/components/timetable_management/TimetableManagement.vue b/aleksis/apps/lesrooster/frontend/components/timetable_management/TimetableManagement.vue index 931306c..5370144 100644 --- a/aleksis/apps/lesrooster/frontend/components/timetable_management/TimetableManagement.vue +++ b/aleksis/apps/lesrooster/frontend/components/timetable_management/TimetableManagement.vue @@ -22,8 +22,8 @@ import SubjectField from "aleksis.apps.cursus/components/SubjectField.vue"; import LessonCard from "./LessonCard.vue"; import { RRule } from "rrule"; -import TeacherTimeTable from "./timetables/TeacherTimeTable.vue"; -import RoomTimeTable from "./timetables/RoomTimeTable.vue"; +import TeacherTimeTable from "../timetables/TeacherTimeTable.vue"; +import RoomTimeTable from "../timetables/RoomTimeTable.vue"; import LessonRatioChip from "./LessonRatioChip.vue"; import TimeGridField from "../validity_range/TimeGridField.vue"; import BlockingCard from "./BlockingCard.vue"; @@ -1138,13 +1138,13 @@ export default defineComponent({ > <teacher-time-table v-if="internalTimeGrid && selectedObjectType === 'teacher'" - :teacher-id="selectedObject" + :id="selectedObject" :time-grid="timeGrid" class="fill-height" /> <room-time-table v-if="internalTimeGrid && selectedObjectType === 'room'" - :room-id="selectedObject" + :id="selectedObject" :time-grid="timeGrid" class="fill-height" /> diff --git a/aleksis/apps/lesrooster/frontend/components/timetable_management/timetables/MiniTimeTable.vue b/aleksis/apps/lesrooster/frontend/components/timetables/MiniTimeTable.vue similarity index 91% rename from aleksis/apps/lesrooster/frontend/components/timetable_management/timetables/MiniTimeTable.vue rename to aleksis/apps/lesrooster/frontend/components/timetables/MiniTimeTable.vue index 988a9b8..e69b9b6 100644 --- a/aleksis/apps/lesrooster/frontend/components/timetable_management/timetables/MiniTimeTable.vue +++ b/aleksis/apps/lesrooster/frontend/components/timetables/MiniTimeTable.vue @@ -1,7 +1,7 @@ <script> import { defineComponent } from "vue"; -import { slots } from "../../breaks_and_slots/slot.graphql"; -import LessonCard from "../LessonCard.vue"; +import { slots } from "../breaks_and_slots/slot.graphql"; +import LessonCard from "../timetable_management/LessonCard.vue"; import MessageBox from "aleksis.core/components/generic/MessageBox.vue"; export default defineComponent({ @@ -93,6 +93,9 @@ export default defineComponent({ return weekdayPeriodSlots; }, + loading() { + return false; + }, }, methods: { styleForWeekdayAndPeriod(weekday, period) { @@ -105,7 +108,14 @@ export default defineComponent({ </script> <template> - <div class="timetable"> + <div v-if="loading" class="d-flex justify-center pa-10"> + <v-progress-circular + indeterminate + color="primary" + :size="50" + ></v-progress-circular> + </div> + <div v-else class="timetable"> <!-- Empty div to fill top-left corner --> <div></div> <v-card diff --git a/aleksis/apps/lesrooster/frontend/components/timetable_management/timetables/RoomTimeTable.vue b/aleksis/apps/lesrooster/frontend/components/timetables/RoomTimeTable.vue similarity index 84% rename from aleksis/apps/lesrooster/frontend/components/timetable_management/timetables/RoomTimeTable.vue rename to aleksis/apps/lesrooster/frontend/components/timetables/RoomTimeTable.vue index a50154b..a667de1 100644 --- a/aleksis/apps/lesrooster/frontend/components/timetable_management/timetables/RoomTimeTable.vue +++ b/aleksis/apps/lesrooster/frontend/components/timetables/RoomTimeTable.vue @@ -7,7 +7,7 @@ export default defineComponent({ name: "RoomTimeTable", extends: MiniTimeTable, props: { - roomId: { + id: { type: String, required: true, }, @@ -16,6 +16,9 @@ export default defineComponent({ lessons() { return this.lessonsRoom; }, + loading() { + return this.$apollo.queries.lessonsRoom.loading; + }, }, apollo: { lessonsRoom: { @@ -23,7 +26,7 @@ export default defineComponent({ variables() { return { timeGrid: this.timeGrid.id, - room: this.roomId, + room: this.id, }; }, skip() { diff --git a/aleksis/apps/lesrooster/frontend/components/timetable_management/timetables/TeacherTimeTable.vue b/aleksis/apps/lesrooster/frontend/components/timetables/TeacherTimeTable.vue similarity index 84% rename from aleksis/apps/lesrooster/frontend/components/timetable_management/timetables/TeacherTimeTable.vue rename to aleksis/apps/lesrooster/frontend/components/timetables/TeacherTimeTable.vue index 5ed3211..f8d89a4 100644 --- a/aleksis/apps/lesrooster/frontend/components/timetable_management/timetables/TeacherTimeTable.vue +++ b/aleksis/apps/lesrooster/frontend/components/timetables/TeacherTimeTable.vue @@ -7,7 +7,7 @@ export default defineComponent({ name: "TeacherTimeTable", extends: MiniTimeTable, props: { - teacherId: { + id: { type: String, required: true, }, @@ -16,6 +16,9 @@ export default defineComponent({ lessons() { return this.lessonsTeacher; }, + loading() { + return this.$apollo.queries.lessonsTeacher.loading; + }, }, apollo: { lessonsTeacher: { @@ -23,7 +26,7 @@ export default defineComponent({ variables() { return { timeGrid: this.timeGrid.id, - teacher: this.teacherId, + teacher: this.id, }; }, skip() { diff --git a/aleksis/apps/lesrooster/frontend/components/timetable_management/timetables/timetables.graphql b/aleksis/apps/lesrooster/frontend/components/timetables/timetables.graphql similarity index 66% rename from aleksis/apps/lesrooster/frontend/components/timetable_management/timetables/timetables.graphql rename to aleksis/apps/lesrooster/frontend/components/timetables/timetables.graphql index 3bd1897..e72e857 100644 --- a/aleksis/apps/lesrooster/frontend/components/timetable_management/timetables/timetables.graphql +++ b/aleksis/apps/lesrooster/frontend/components/timetables/timetables.graphql @@ -110,3 +110,58 @@ query lessonsRoom($room: ID!, $timeGrid: ID!) { canDelete } } + +query lessonsGroup($group: ID!, $timeGrid: ID!) { + lessonsGroup: lessonObjectsForGroup(group: $group, timeGrid: $timeGrid) { + id + slotStart { + id + period + weekday + } + slotEnd { + id + period + weekday + } + subject { + id + name + colourFg + colourBg + } + teachers { + id + fullName + shortName + } + rooms { + id + name + shortName + } + course { + id + name + subject { + id + name + colourFg + colourBg + } + teachers { + id + fullName + shortName + } + groups { + id + name + shortName + } + } + recurrence + canEdit + canDelete + } +}
-
Jonathan Weth authored
-
Jonathan Weth authored
-
Jonathan Weth authored
-
Jonathan Weth authored
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)
-
Jonathan Weth authored
-
Jonathan Weth authored
-
Jonathan Weth authored
-
Jonathan Weth authored
-
Jonathan Weth authored
-
Jonathan Weth authored
Showing
- CHANGELOG.rst 8 additions, 0 deletionsCHANGELOG.rst
- aleksis/apps/lesrooster/frontend/components/lesson_raster/LessonRaster.vue 2 additions, 2 deletions...ooster/frontend/components/lesson_raster/LessonRaster.vue
- aleksis/apps/lesrooster/frontend/components/lesson_raster/SlotCard.vue 1 addition, 1 deletion...lesrooster/frontend/components/lesson_raster/SlotCard.vue
- aleksis/apps/lesrooster/frontend/components/supervision/Supervision.vue 1 addition, 1 deletion...esrooster/frontend/components/supervision/Supervision.vue
- aleksis/apps/lesrooster/frontend/components/timebound_course_config/TimeboundCourseConfigRaster.vue 1 addition, 1 deletion...s/timebound_course_config/TimeboundCourseConfigRaster.vue
- aleksis/apps/lesrooster/frontend/components/timetable_management/TimetableManagement.vue 6 additions, 6 deletions...d/components/timetable_management/TimetableManagement.vue
- aleksis/apps/lesrooster/frontend/components/timetables/GroupTimeTable.vue 38 additions, 0 deletions...rooster/frontend/components/timetables/GroupTimeTable.vue
- aleksis/apps/lesrooster/frontend/components/timetables/MiniTimeTable.vue 13 additions, 3 deletions...srooster/frontend/components/timetables/MiniTimeTable.vue
- aleksis/apps/lesrooster/frontend/components/timetables/RoomTimeTable.vue 5 additions, 2 deletions...srooster/frontend/components/timetables/RoomTimeTable.vue
- aleksis/apps/lesrooster/frontend/components/timetables/TeacherTimeTable.vue 5 additions, 2 deletions...oster/frontend/components/timetables/TeacherTimeTable.vue
- aleksis/apps/lesrooster/frontend/components/timetables/Timetable.vue 127 additions, 0 deletions...s/lesrooster/frontend/components/timetables/Timetable.vue
- aleksis/apps/lesrooster/frontend/components/timetables/timetables.graphql 57 additions, 0 deletions...rooster/frontend/components/timetables/timetables.graphql
- aleksis/apps/lesrooster/frontend/components/validity_range/CopyFromTimeGridMenu.vue 3 additions, 3 deletions...ontend/components/validity_range/CopyFromTimeGridMenu.vue
- aleksis/apps/lesrooster/frontend/components/validity_range/TimeGridField.vue 21 additions, 9 deletions...ster/frontend/components/validity_range/TimeGridField.vue
- aleksis/apps/lesrooster/frontend/index.js 42 additions, 1 deletionaleksis/apps/lesrooster/frontend/index.js
- aleksis/apps/lesrooster/frontend/messages/de.json 15 additions, 11 deletionsaleksis/apps/lesrooster/frontend/messages/de.json
- aleksis/apps/lesrooster/frontend/messages/en.json 19 additions, 13 deletionsaleksis/apps/lesrooster/frontend/messages/en.json
- aleksis/apps/lesrooster/managers.py 135 additions, 3 deletionsaleksis/apps/lesrooster/managers.py
- aleksis/apps/lesrooster/models.py 44 additions, 1 deletionaleksis/apps/lesrooster/models.py
- aleksis/apps/lesrooster/static/css/lesrooster/timetable_print.css 63 additions, 0 deletions...apps/lesrooster/static/css/lesrooster/timetable_print.css