diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 643ff17db9651a866c10dedddd59f1feb9d6f84f..5389b8747a7f76495756ad47fda11d9e410319cf 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -9,6 +9,12 @@ and this project adheres to `Semantic Versioning`_. Unreleased ---------- +Added +~~~~~ + +* New timetable interface based on calendar system. +* [Dev] LessonEvent and SupervisionEvent basing on calendar system. + `3.0.2`_ - 2023-09-10 --------------------- diff --git a/aleksis/apps/chronos/frontend/components/SelectTimetable.vue b/aleksis/apps/chronos/frontend/components/SelectTimetable.vue index 4f880bdc2bf7952017f7c92cd149e1cd5ad23c27..6e1931e71f89a0fc608ce6714866c5174592947d 100644 --- a/aleksis/apps/chronos/frontend/components/SelectTimetable.vue +++ b/aleksis/apps/chronos/frontend/components/SelectTimetable.vue @@ -5,7 +5,7 @@ export default { name: "SelectTimetable", props: { value: { - type: String, + type: Object, required: false, default: null, }, diff --git a/aleksis/apps/chronos/managers.py b/aleksis/apps/chronos/managers.py index fae25d1817462aa3f0fd0babf619bf480cb65320..b61587a8867745914f158c3d419e7c48c148d908 100644 --- a/aleksis/apps/chronos/managers.py +++ b/aleksis/apps/chronos/managers.py @@ -816,7 +816,7 @@ class GroupPropertiesMixin: return sep.join([group.short_name for group in self.get_groups()]) @property - def groups_to_show(self) -> models.QuerySet: + def groups_to_show(self) -> QuerySet[Group]: groups = self.get_groups() if ( groups.count() == 1 @@ -869,13 +869,15 @@ class RoomPropertiesMixin: class LessonEventQuerySet(PolymorphicQuerySet): """Queryset with special query methods for lesson events.""" - def for_teacher(self, teacher: Union[int, Person]): + def for_teacher(self, teacher: Union[int, Person]) -> "LessonEventQuerySet": + """Get all lesson events for a certain person as teacher (including amends).""" amended = self.filter(Q(amended_by__isnull=False) & (Q(teachers=teacher))).values_list( "amended_by__pk", flat=True ) return self.filter(Q(teachers=teacher) | Q(pk__in=amended)).distinct() - def for_group(self, group: Union[int, Group]): + def for_group(self, group: Union[int, Group]) -> "LessonEventQuerySet": + """Get all lesson events for a certain group (including amends/as parent group).""" amended = self.filter( Q(amended_by__isnull=False) & (Q(groups=group) | Q(groups__parent_groups=group)) ).values_list("amended_by__pk", flat=True) @@ -883,19 +885,22 @@ class LessonEventQuerySet(PolymorphicQuerySet): Q(groups=group) | Q(groups__parent_groups=group) | Q(pk__in=amended) ).distinct() - def for_room(self, room: Union[int, Room]): + def for_room(self, room: Union[int, Room]) -> "LessonEventQuerySet": + """Get all lesson events for a certain room (including amends).""" amended = self.filter(Q(amended_by__isnull=False) & (Q(rooms=room))).values_list( "amended_by__pk", flat=True ) return self.filter(Q(rooms=room) | Q(pk__in=amended)).distinct() - def for_course(self, course: Union[int, Course]): + def for_course(self, course: Union[int, Course]) -> "LessonEventQuerySet": + """Get all lesson events for a certain course (including amends).""" amended = self.filter(Q(amended_by__isnull=False) & (Q(course=course))).values_list( "amended_by__pk", flat=True ) return self.filter(Q(course=course) | Q(pk__in=amended)).distinct() - def for_person(self, person: Union[int, Person]): + def for_person(self, person: Union[int, Person]) -> "LessonEventQuerySet": + """Get all lesson events for a certain person (as teacher/participant, including amends).""" amended = self.filter( Q(amended_by__isnull=False) & (Q(teachers=person) | Q(groups__members=person)) ).values_list("amended_by__pk", flat=True) @@ -903,17 +908,34 @@ class LessonEventQuerySet(PolymorphicQuerySet): Q(teachers=person) | Q(groups__members=person) | Q(pk__in=amended) ).distinct() - def related_to_person(self, person: Union[int, Person]): + def related_to_person(self, person: Union[int, Person]) -> "LessonEventQuerySet": + """Get all lesson events a certain person is allowed to see. + + This includes all lesson events the person is assigned to as + teacher/participant/group owner/parent group owner, + including those amended. + """ amended = self.filter( Q(amended_by__isnull=False) - & (Q(teachers=person) | Q(groups__members=person) | Q(groups__owners=person)) + & ( + Q(teachers=person) + | Q(groups__members=person) + | Q(groups__owners=person) + | Q(groups__parent_groups__owners=person) + ) ).values_list("amended_by__pk", flat=True) return self.filter( - Q(teachers=person) | Q(groups__members=person) | Q(groups__owners=person) + Q(teachers=person) + | Q(groups__members=person) + | Q(groups__owners=person) + | Q(groups__parent_groups__owners=person) + | Q(pk__in=amended) ).distinct() - def not_amended(self): + def not_amended(self) -> "LessonEventQuerySet": + """Get all lesson events that are not amended.""" return self.filter(amended_by__isnull=True) - def not_amending(self): + def not_amending(self) -> "LessonEventQuerySet": + """Get all lesson events that are not amending other events.""" return self.filter(amends__isnull=True) diff --git a/aleksis/apps/chronos/models.py b/aleksis/apps/chronos/models.py index 60ffa45c6bfca6addfd0e32ecec2b6ace7a1e1ce..12df770fc553cc7bb969a5de1467a66d96dc2c70 100644 --- a/aleksis/apps/chronos/models.py +++ b/aleksis/apps/chronos/models.py @@ -11,10 +11,11 @@ from django.contrib.contenttypes.models import ContentType from django.core.exceptions import PermissionDenied, ValidationError from django.core.validators import MinValueValidator from django.db import models -from django.db.models import Max, Min, Q +from django.db.models import Max, Min, Q, QuerySet from django.db.models.functions import Coalesce from django.dispatch import receiver from django.forms import Media +from django.http import HttpRequest from django.template.loader import render_to_string from django.urls import reverse from django.utils import timezone @@ -1324,6 +1325,8 @@ class ChronosGlobalPermissions(GlobalPermissionModel): class LessonEvent(CalendarEvent): + """Calendar feed for lessons.""" + name = "lesson" verbose_name = _("Lessons") @@ -1374,36 +1377,43 @@ class LessonEvent(CalendarEvent): ) @property - def actual_groups(self: LessonEvent): - return self.groups.all() if self.amends else self.real_amends.groups.all() + def actual_groups(self: LessonEvent) -> QuerySet[Group]: + """Get list of the groups of this lesson event.""" + return self.amends.groups.all() if self.amends else self.groups.all() @property def all_members(self: LessonEvent) -> list[Person]: + """Get list of all group members for this lesson event.""" return list(itertools.chain(*[list(g.members.all()) for g in self.actual_groups])) @property def all_teachers(self: LessonEvent) -> list[Person]: + """Get list of all teachers for this lesson event.""" all_teachers = list(self.teachers.all()) if self.amends: - all_teachers += list(self.real_amends.teachers.all()) + all_teachers += list(self.amends.teachers.all()) return all_teachers @property def group_names(self: LessonEvent) -> str: + """Get comma-separated string with all group names.""" return ", ".join([g.name for g in self.actual_groups]) @property def teacher_names(self: LessonEvent) -> str: + """Get comma-separated string with all teacher names.""" return ", ".join([t.full_name for t in self.teachers.all()]) @property def room_names(self: LessonEvent) -> str: + """Get comma-separated string with all room names.""" return ", ".join([r.name for r in self.rooms.all()]) @property def room_names_with_amends(self: LessonEvent) -> str: + """Get comma-separated string with all room names (including amends).""" my_room_names = self.room_names - amended_room_names = self.real_amends.room_names if self.amends else "" + amended_room_names = self.amends.room_names if self.amends else "" if my_room_names and amended_room_names: return _("{} (instead of {})").format(my_room_names, amended_room_names) @@ -1413,8 +1423,9 @@ class LessonEvent(CalendarEvent): @property def teacher_names_with_amends(self: LessonEvent) -> str: + """Get comma-separated string with all teacher names (including amends).""" my_teacher_names = self.teacher_names - amended_teacher_names = self.real_amends.teacher_names if self.amends else "" + amended_teacher_names = self.amends.teacher_names if self.amends else "" if my_teacher_names and amended_teacher_names: return _("{} (instead of {})").format(my_teacher_names, amended_teacher_names) @@ -1424,8 +1435,9 @@ class LessonEvent(CalendarEvent): @property def subject_name_with_amends(self: LessonEvent) -> str: + """Get formatted subject name (including amends).""" my_subject = self.subject.name if self.subject else "" - amended_subject = self.real_amends.subject.name if self.amends else "" + amended_subject = self.amends.subject.name if self.amends and self.amends.subject else "" if my_subject and amended_subject: return _("{} (instead of {})").format(my_subject, amended_subject) @@ -1435,26 +1447,21 @@ class LessonEvent(CalendarEvent): return my_subject return _("Lesson") - @property - def real_amends(self: LessonEvent) -> LessonEvent: - # FIXME THIS IS AWFUL SLOW - if self.amends: - return LessonEvent.objects.get(pk=self.amends.pk) - return self - @classmethod - def value_title(cls, reference_object: LessonEvent, request) -> str: - """Get the title of the event.""" + def value_title(cls, reference_object: LessonEvent, request: HttpRequest | None = None) -> str: + """Get the title of the lesson event.""" if reference_object.title: return reference_object.title elif reference_object.subject or ( - reference_object.amends and reference_object.real_amends.subject + reference_object.amends and reference_object.amends.subject ): title = reference_object.subject_name_with_amends - if request.user.person in reference_object.teachers.all(): + if request and request.user.person in reference_object.teachers.all(): title += " · " + reference_object.group_names - else: + elif request: title += " · " + reference_object.teacher_names_with_amends + else: + title += f" · {reference_object.group_names} · {reference_object.teacher_names_with_amends}" if reference_object.rooms.all().exists(): title += " · " + reference_object.room_names_with_amends return title @@ -1462,47 +1469,59 @@ class LessonEvent(CalendarEvent): return _("Lesson") @classmethod - def value_description(cls, reference_object: LessonEvent, request) -> str: + def value_description( + cls, reference_object: LessonEvent, request: HttpRequest | None = None + ) -> str: + """Get the description of the lesson event.""" return render_to_string("chronos/lesson_event_description.txt", {"event": reference_object}) @classmethod - def value_color(cls, reference_object: LessonEvent, request) -> str: - """Get the color of the event.""" + def value_color(cls, reference_object: LessonEvent, request: HttpRequest | None = None) -> str: + """Get the color of the lesson event.""" if reference_object.cancelled: return "#eeeeee" if reference_object.subject: return reference_object.subject.colour_bg - if reference_object.amends and reference_object.real_amends.subject: - return reference_object.real_amends.subject.colour_bg + if reference_object.amends and reference_object.amends.subject: + return reference_object.amends.subject.colour_bg return super().value_color(reference_object, request) @classmethod - def value_attendee(cls, reference_object: LessonEvent, request) -> str: - """Get the attendees of the event.""" + def value_attendee( + cls, reference_object: LessonEvent, request: HttpRequest | None = None + ) -> str: + """Get the attendees of the lesson event.""" + # Only teachers due to privacy concerns attendees = [t.get_vcal_address(role="CHAIR") for t in reference_object.teachers.all()] return [a for a in attendees if a] @classmethod - def value_location(cls, reference_object: LessonEvent, request) -> str: - """Get the location of the event.""" - return ", ".join([r.name for r in reference_object.rooms.all()]) + def value_location( + cls, reference_object: LessonEvent, request: HttpRequest | None = None + ) -> str: + """Get the location of the lesson event.""" + return reference_object.room_names_with_amends @classmethod - def value_status(cls, reference_object: LessonEvent, request) -> str: - """Get the status of the event.""" + def value_status(cls, reference_object: LessonEvent, request: HttpRequest | None = None) -> str: + """Get the status of the lesson event.""" if reference_object.cancelled: return "CANCELLED" return "CONFIRMED" @classmethod - def value_meta(cls, reference_object: LessonEvent, request) -> str: - """Get the meta of the event.""" - real_amends = reference_object.real_amends + def value_meta(cls, reference_object: LessonEvent, request: HttpRequest | None = None) -> str: + """Get the meta of the lesson event. + + These information will be primarly used in our own calendar frontend. + """ return { "id": reference_object.id, "amended": bool(reference_object.amends), - "amends": cls.value_meta(real_amends, request) if reference_object.amends else None, + "amends": cls.value_meta(reference_object.amends, request) + if reference_object.amends + else None, "teachers": [ { "id": t.pk, @@ -1513,12 +1532,12 @@ class LessonEvent(CalendarEvent): } for t in reference_object.teachers.all() ], - "is_teacher": request.user.person in reference_object.all_teachers, + "is_teacher": request.user.person in reference_object.all_teachers if request else None, "groups": [ {"id": g.pk, "name": g.name, "short_name": g.short_name} for g in reference_object.actual_groups ], - "is_member": request.user.person in reference_object.all_members, + "is_member": request.user.person in reference_object.all_members if request else None, "rooms": [ {"id": r.pk, "name": r.name, "short_name": r.short_name} for r in reference_object.rooms.all() @@ -1537,11 +1556,13 @@ class LessonEvent(CalendarEvent): } @classmethod - def get_objects(cls, request, params=None) -> Iterable: + def get_objects( + cls, request: HttpRequest | None = None, params: dict[str, any] | None = None + ) -> Iterable: """Return all objects that should be included in the calendar.""" objs = super().get_objects(request, params).not_instance_of(SupervisionEvent) - if not has_person(request.user): + if request and not has_person(request.user): raise PermissionDenied() if params: @@ -1557,7 +1578,7 @@ class LessonEvent(CalendarEvent): if not_amending: objs = objs.not_amending() - if "own" in params: + if request and "own" in params: if own: objs = objs.for_person(request.user.person) else: @@ -1572,9 +1593,9 @@ class LessonEvent(CalendarEvent): return objs.for_room(obj_id) elif type_ == "COURSE": return objs.for_course(obj_id) - elif "own" in params: - return objs - return objs.for_person(request.user.person) + if request: + return objs.for_person(request.user.person) + return objs class Meta: verbose_name = _("Lesson Event") @@ -1582,25 +1603,31 @@ class LessonEvent(CalendarEvent): class SupervisionEvent(LessonEvent): + """Calendar feed for supervisions.""" + name = "supervision" verbose_name = _("Supervisions") objects = PolymorphicBaseManager.from_queryset(LessonEventQuerySet)() @classmethod - def value_title(cls, reference_object: LessonEvent, request) -> str: + def value_title(cls, reference_object: LessonEvent, request: HttpRequest | None = None) -> str: """Get the title of the event.""" return _("Supervision: {}").format(reference_object.room_names) @classmethod - def value_description(cls, reference_object: LessonEvent, request) -> str: + def value_description( + cls, reference_object: LessonEvent, request: HttpRequest | None = None + ) -> str: return render_to_string( "chronos/supervision_event_description.txt", {"event": reference_object} ) @classmethod - def get_objects(cls, request, params=None) -> Iterable: + def get_objects( + cls, request: HttpRequest | None = None, params: dict[str, any] | None = None + ) -> Iterable: """Return all objects that should be included in the calendar.""" objs = cls.objects.instance_of(cls) if params: @@ -1614,4 +1641,6 @@ class SupervisionEvent(LessonEvent): return objs.for_group(obj_id) elif type_ == "ROOM": return objs.for_room(obj_id) - return objs.for_person(request.user.person) + if request: + return objs.for_person(request.user.person) + return objs