diff --git a/aleksis/apps/alsijil/migrations/0010_events_extra_lessons.py b/aleksis/apps/alsijil/migrations/0010_events_extra_lessons.py new file mode 100644 index 0000000000000000000000000000000000000000..34611bf386a55de933219217c7fabb0709f2f14e --- /dev/null +++ b/aleksis/apps/alsijil/migrations/0010_events_extra_lessons.py @@ -0,0 +1,91 @@ +# Generated by Django 3.1.5 on 2021-01-10 15:48 + +import aleksis.apps.chronos.util.date +import django.contrib.sites.managers +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('chronos', '0004_substitution_extra_lesson_year'), + ('alsijil', '0009_group_roles'), + ] + + operations = [ + migrations.AlterModelOptions( + name='lessondocumentation', + options={'ordering': ['year', 'week', 'lesson_period__period__weekday', 'lesson_period__period__period'], 'verbose_name': 'Lesson documentation', 'verbose_name_plural': 'Lesson documentations'}, + ), + migrations.AlterModelOptions( + name='personalnote', + options={'ordering': ['year', 'week', 'lesson_period__period__weekday', 'lesson_period__period__period', 'person__last_name', 'person__first_name'], 'verbose_name': 'Personal note', 'verbose_name_plural': 'Personal notes'}, + ), + migrations.AddField( + model_name='lessondocumentation', + name='event', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='documentations', to='chronos.event'), + ), + migrations.AddField( + model_name='lessondocumentation', + name='extra_lesson', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='documentations', to='chronos.extralesson'), + ), + migrations.AddField( + model_name='personalnote', + name='event', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='personal_notes', to='chronos.event'), + ), + migrations.AddField( + model_name='personalnote', + name='extra_lesson', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='personal_notes', to='chronos.extralesson'), + ), + migrations.AlterField( + model_name='lessondocumentation', + name='lesson_period', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='documentations', to='chronos.lessonperiod'), + ), + migrations.AlterField( + model_name='lessondocumentation', + name='week', + field=models.IntegerField(blank=True, null=True), + ), + migrations.AlterField( + model_name='lessondocumentation', + name='year', + field=models.IntegerField(blank=True, default=aleksis.apps.chronos.util.date.get_current_year, null=True, verbose_name='Year'), + ), + migrations.AlterField( + model_name='personalnote', + name='lesson_period', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='personal_notes', to='chronos.lessonperiod'), + ), + migrations.AlterField( + model_name='personalnote', + name='week', + field=models.IntegerField(blank=True, null=True), + ), + migrations.AlterField( + model_name='personalnote', + name='year', + field=models.IntegerField(blank=True, default=aleksis.apps.chronos.util.date.get_current_year, null=True, verbose_name='Year'), + ), + migrations.AlterUniqueTogether( + name='lessondocumentation', + unique_together=set(), + ), + migrations.AlterUniqueTogether( + name='personalnote', + unique_together=set(), + ), + migrations.AddConstraint( + model_name='lessondocumentation', + constraint=models.CheckConstraint(check=models.Q(models.Q(('event__isnull', True), ('extra_lesson__isnull', True), ('lesson_period__isnull', False), ('week__isnull', False), ('year__isnull', False)), models.Q(('event__isnull', False), ('extra_lesson__isnull', True), ('lesson_period__isnull', True), ('week__isnull', True), ('year__isnull', True)), models.Q(('event__isnull', True), ('extra_lesson__isnull', False), ('lesson_period__isnull', True), ('week__isnull', True), ('year__isnull', True)), _connector='OR'), name='one_relation_only_lesson_documentation'), + ), + migrations.AddConstraint( + model_name='personalnote', + constraint=models.CheckConstraint(check=models.Q(models.Q(('event__isnull', True), ('extra_lesson__isnull', True), ('lesson_period__isnull', False), ('week__isnull', False), ('year__isnull', False)), models.Q(('event__isnull', False), ('extra_lesson__isnull', True), ('lesson_period__isnull', True), ('week__isnull', True), ('year__isnull', True)), models.Q(('event__isnull', True), ('extra_lesson__isnull', False), ('lesson_period__isnull', True), ('week__isnull', True), ('year__isnull', True)), _connector='OR'), name='one_relation_only_personal_note'), + ), + ] diff --git a/aleksis/apps/alsijil/model_extensions.py b/aleksis/apps/alsijil/model_extensions.py index fdd05fd4bf3a867c98914b17ace921c65ffe052b..d7ebda3a49deadf724d18476d3c4dd9ab55abee5 100644 --- a/aleksis/apps/alsijil/model_extensions.py +++ b/aleksis/apps/alsijil/model_extensions.py @@ -3,16 +3,43 @@ from typing import Dict, Iterable, Iterator, Optional, Union from django.db.models import Exists, OuterRef, Q, QuerySet from django.db.models.aggregates import Count, Sum +from django.db.models.expressions import Subquery +from django.urls import reverse from django.utils.translation import gettext as _ from calendarweek import CalendarWeek -from aleksis.apps.chronos.models import LessonPeriod +from aleksis.apps.alsijil.managers import PersonalNoteQuerySet +from aleksis.apps.chronos.models import Event, ExtraLesson, LessonPeriod from aleksis.core.models import Group, Person from .models import ExcuseType, ExtraMark, LessonDocumentation, PersonalNote +def alsijil_url( + self: Union[LessonPeriod, Event, ExtraLesson], week: Optional[CalendarWeek] = None +) -> str: + """Build URL for the detail page of register objects. + + Works with `LessonPeriod`, `Event` and `ExtraLesson`. + + On `LessonPeriod` objects, it will work with annotated or passed weeks. + """ + if isinstance(self, LessonPeriod): + week = week or self.week + return reverse("lesson_period", args=[week.year, week.week, self.pk]) + else: + return reverse(self.label_, args=[self.pk]) + + +LessonPeriod.property_(alsijil_url) +LessonPeriod.method(alsijil_url, "get_alsijil_url") +Event.property_(alsijil_url) +Event.method(alsijil_url, "get_alsijil_url") +ExtraLesson.property_(alsijil_url) +ExtraLesson.method(alsijil_url, "get_alsijil_url") + + @Person.method def mark_absent( self, @@ -27,8 +54,8 @@ def mark_absent( ): """Mark a person absent for all lessons in a day, optionally starting with a selected period number. - This function creates `PersonalNote` objects for every `LessonPeriod` the person - participates in on the selected day and marks them as absent/excused. + This function creates `PersonalNote` objects for every `LessonPeriod` and `ExtraLesson` + the person participates in on the selected day and marks them as absent/excused. :param dry_run: With this activated, the function won't change any data and just return the count of affected lessons @@ -49,14 +76,28 @@ def mark_absent( .filter(period__period__gte=from_period) .annotate_week(wanted_week) ) + extra_lessons = ( + ExtraLesson.objects.filter(groups__members=self) + .on_day(day) + .filter(period__period__gte=from_period) + ) if to_period: lesson_periods = lesson_periods.filter(period__period__lte=to_period) + extra_lessons = extra_lessons.filter(period__period__lte=to_period) # Create and update all personal notes for the discovered lesson periods if not dry_run: - for lesson_period in lesson_periods: - sub = lesson_period.get_substitution() + for register_object in list(lesson_periods) + list(extra_lessons): + if isinstance(register_object, LessonPeriod): + sub = register_object.get_substitution() + q_attrs = dict( + week=wanted_week.week, year=wanted_week.year, lesson_period=register_object + ) + else: + sub = None + q_attrs = dict(extra_lesson=register_object) + if sub and sub.cancelled: continue @@ -65,10 +106,8 @@ def mark_absent( .prefetch_related(None) .update_or_create( person=self, - lesson_period=lesson_period, - week=wanted_week.week, - year=wanted_week.year, defaults={"absent": absent, "excused": excused, "excuse_type": excuse_type,}, + **q_attrs, ) ) personal_note.groups_of_person.set(self.member_of.all()) @@ -80,14 +119,18 @@ def mark_absent( personal_note.remarks = remarks personal_note.save() - return lesson_periods.count() + return lesson_periods.count() + extra_lessons.count() -@LessonPeriod.method -def get_personal_notes(self, persons: QuerySet, wanted_week: CalendarWeek): - """Get all personal notes for that lesson in a specified week. +def get_personal_notes( + self, persons: QuerySet, wanted_week: Optional[CalendarWeek] = None +) -> PersonalNoteQuerySet: + """Get all personal notes for that register object in a specified week. - Returns all linked `PersonalNote` objects, filtered by the given weeek, + The week is optional for extra lessons and events as they have own date information. + + Returns all linked `PersonalNote` objects, + filtered by the given week for `LessonPeriod` objects, creating those objects that haven't been created yet. ..note:: Only available when AlekSIS-App-Alsijil is installed. @@ -97,37 +140,30 @@ def get_personal_notes(self, persons: QuerySet, wanted_week: CalendarWeek): - Dominik George <dominik.george@teckids.org> """ # Find all persons in the associated groups that do not yet have a personal note for this lesson + if isinstance(self, LessonPeriod): + q_attrs = dict(week=wanted_week.week, year=wanted_week.year, lesson_period=self) + elif isinstance(self, Event): + q_attrs = dict(event=self) + else: + q_attrs = dict(extra_lesson=self) + missing_persons = persons.annotate( - no_personal_notes=~Exists( - PersonalNote.objects.filter( - week=wanted_week.week, - year=wanted_week.year, - lesson_period=self, - person__pk=OuterRef("pk"), - ) - ) + no_personal_notes=~Exists(PersonalNote.objects.filter(person__pk=OuterRef("pk"), **q_attrs)) ).filter( - member_of__in=Group.objects.filter(pk__in=self.lesson.groups.all()), + member_of__in=Group.objects.filter(pk__in=self.get_groups().all()), is_active=True, no_personal_notes=True, ) # Create all missing personal notes - new_personal_notes = [ - PersonalNote( - person=person, lesson_period=self, week=wanted_week.week, year=wanted_week.year, - ) - for person in missing_persons - ] + new_personal_notes = [PersonalNote(person=person, **q_attrs,) for person in missing_persons] PersonalNote.objects.bulk_create(new_personal_notes) for personal_note in new_personal_notes: personal_note.groups_of_person.set(personal_note.person.member_of.all()) return ( - PersonalNote.objects.filter( - lesson_period=self, week=wanted_week.week, year=wanted_week.year, person__in=persons, - ) + PersonalNote.objects.filter(**q_attrs, person__in=persons) .select_related(None) .prefetch_related(None) .select_related("person", "excuse_type") @@ -135,6 +171,10 @@ def get_personal_notes(self, persons: QuerySet, wanted_week: CalendarWeek): ) +LessonPeriod.method(get_personal_notes) +Event.method(get_personal_notes) +ExtraLesson.method(get_personal_notes) + # Dynamically add extra permissions to Group and Person models in core # Note: requires migrate afterwards Group.add_permission( @@ -175,6 +215,19 @@ def get_lesson_documentation( return None +def get_lesson_documentation_single( + self, week: Optional[CalendarWeek] = None +) -> Union[LessonDocumentation, None]: + """Get lesson documentation object for this event/extra lesson.""" + if self.documentations.exists(): + return self.documentations.all()[0] + return None + + +Event.method(get_lesson_documentation_single, "get_lesson_documentation") +ExtraLesson.method(get_lesson_documentation_single, "get_lesson_documentation") + + @LessonPeriod.method def get_or_create_lesson_documentation( self, week: Optional[CalendarWeek] = None @@ -182,12 +235,24 @@ def get_or_create_lesson_documentation( """Get or create lesson documentation object for this lesson.""" if not week: week = self.week - lesson_documentation, created = LessonDocumentation.objects.get_or_create( + lesson_documentation, __ = LessonDocumentation.objects.get_or_create( lesson_period=self, week=week.week, year=week.year ) return lesson_documentation +def get_or_create_lesson_documentation_single( + self, week: Optional[CalendarWeek] = None +) -> LessonDocumentation: + """Get or create lesson documentation object for this event/extra lesson.""" + lesson_documentation, created = LessonDocumentation.objects.get_or_create(**{self.label_: self}) + return lesson_documentation + + +Event.method(get_or_create_lesson_documentation_single, "get_or_create_lesson_documentation") +ExtraLesson.method(get_or_create_lesson_documentation_single, "get_or_create_lesson_documentation") + + @LessonPeriod.method def get_absences(self, week: Optional[CalendarWeek] = None) -> Iterator: """Get all personal notes of absent persons for this lesson.""" @@ -200,6 +265,15 @@ def get_absences(self, week: Optional[CalendarWeek] = None) -> Iterator: ) +def get_absences_simple(self, week: Optional[CalendarWeek] = None) -> Iterator: + """Get all personal notes of absent persons for this event/extra lesson.""" + return filter(lambda p: p.absent, self.personal_notes.all()) + + +Event.method(get_absences_simple, "get_absences") +ExtraLesson.method(get_absences_simple, "get_absences") + + @LessonPeriod.method def get_excused_absences(self, week: Optional[CalendarWeek] = None) -> QuerySet: """Get all personal notes of excused absent persons for this lesson.""" @@ -208,6 +282,15 @@ def get_excused_absences(self, week: Optional[CalendarWeek] = None) -> QuerySet: return self.personal_notes.filter(week=week.week, year=week.year, absent=True, excused=True) +def get_excused_absences_simple(self, week: Optional[CalendarWeek] = None) -> QuerySet: + """Get all personal notes of excused absent persons for this event/extra lesson.""" + return self.personal_notes.filter(absent=True, excused=True) + + +Event.method(get_excused_absences_simple, "get_excused_absences") +ExtraLesson.method(get_excused_absences_simple, "get_excused_absences") + + @LessonPeriod.method def get_unexcused_absences(self, week: Optional[CalendarWeek] = None) -> QuerySet: """Get all personal notes of unexcused absent persons for this lesson.""" @@ -216,6 +299,15 @@ def get_unexcused_absences(self, week: Optional[CalendarWeek] = None) -> QuerySe return self.personal_notes.filter(week=week.week, year=week.year, absent=True, excused=False) +def get_unexcused_absences_simple(self, week: Optional[CalendarWeek] = None) -> QuerySet: + """Get all personal notes of unexcused absent persons for this event/extra lesson.""" + return self.personal_notes.filter(absent=True, excused=False) + + +Event.method(get_unexcused_absences_simple, "get_unexcused_absences") +ExtraLesson.method(get_unexcused_absences_simple, "get_unexcused_absences") + + @LessonPeriod.method def get_tardinesses(self, week: Optional[CalendarWeek] = None) -> QuerySet: """Get all personal notes of late persons for this lesson.""" @@ -224,6 +316,15 @@ def get_tardinesses(self, week: Optional[CalendarWeek] = None) -> QuerySet: return self.personal_notes.filter(week=week.week, year=week.year, late__gt=0) +def get_tardinesses_simple(self, week: Optional[CalendarWeek] = None) -> QuerySet: + """Get all personal notes of late persons for this event/extra lesson.""" + return self.personal_notes.filter(late__gt=0) + + +Event.method(get_tardinesses_simple, "get_tardinesses") +ExtraLesson.method(get_tardinesses_simple, "get_tardinesses") + + @LessonPeriod.method def get_extra_marks(self, week: Optional[CalendarWeek] = None) -> Dict[ExtraMark, QuerySet]: """Get all statistics on extra marks for this lesson.""" @@ -239,6 +340,21 @@ def get_extra_marks(self, week: Optional[CalendarWeek] = None) -> Dict[ExtraMark return stats +def get_extra_marks_simple(self, week: Optional[CalendarWeek] = None) -> Dict[ExtraMark, QuerySet]: + """Get all statistics on extra marks for this event/extra lesson.""" + stats = {} + for extra_mark in ExtraMark.objects.all(): + qs = self.personal_notes.filter(extra_marks=extra_mark) + if qs: + stats[extra_mark] = qs + + return stats + + +Event.method(get_extra_marks_simple, "get_extra_marks") +ExtraLesson.method(get_extra_marks_simple, "get_extra_marks") + + @Group.class_method def get_groups_with_lessons(cls: Group): """Get all groups which have related lessons or child groups with related lessons.""" @@ -270,62 +386,59 @@ def generate_person_list_with_class_register_statistics( if persons is None: persons = self.members.all() - persons = persons.filter( - personal_notes__groups_of_person=self, - personal_notes__lesson_period__lesson__validity__school_term=self.school_term, - ).distinct() + # Build reusable Q objects for filtering by school term and by groups + # Necessary for the following annotations + school_term_q = ( + Q(personal_notes__lesson_period__lesson__validity__school_term=self.school_term) + | Q(personal_notes__extra_lesson__school_term=self.school_term) + | Q(personal_notes__event__school_term=self.school_term) + ) + groups_q = ( + Q(personal_notes__lesson_period__lesson__groups=self) + | Q(personal_notes__lesson_period__lesson__groups__parent_groups=self) + | Q(personal_notes__extra_lesson__groups=self) + | Q(personal_notes__extra_lesson__groups__parent_groups=self) + | Q(personal_notes__event__groups=self) + | Q(personal_notes__event__groups__parent_groups=self) + ) + + persons = persons.filter(personal_notes__groups_of_person=self).filter(school_term_q).distinct() + persons = persons.annotate( absences_count=Count( - "personal_notes__absent", - filter=Q( - personal_notes__absent=True, - personal_notes__lesson_period__lesson__validity__school_term=self.school_term, - ) - & ( - Q(personal_notes__lesson_period__lesson__groups=self) - | Q(personal_notes__lesson_period__lesson__groups__parent_groups=self) - ), + "personal_notes", + filter=Q(personal_notes__absent=True) & school_term_q & groups_q, + distinct=True, ), excused=Count( - "personal_notes__absent", + "personal_notes", filter=Q( personal_notes__absent=True, personal_notes__excused=True, personal_notes__excuse_type__isnull=True, - personal_notes__lesson_period__lesson__validity__school_term=self.school_term, ) - & ( - Q(personal_notes__lesson_period__lesson__groups=self) - | Q(personal_notes__lesson_period__lesson__groups__parent_groups=self) - ), + & school_term_q + & groups_q, + distinct=True, ), unexcused=Count( - "personal_notes__absent", - filter=Q( - personal_notes__absent=True, - personal_notes__excused=False, - personal_notes__lesson_period__lesson__validity__school_term=self.school_term, - ) - & ( - Q(personal_notes__lesson_period__lesson__groups=self) - | Q(personal_notes__lesson_period__lesson__groups__parent_groups=self) - ), + "personal_notes", + filter=Q(personal_notes__absent=True, personal_notes__excused=False) + & school_term_q + & groups_q, + distinct=True, ), - tardiness=Sum( - "personal_notes__late", - filter=( - Q(personal_notes__lesson_period__lesson__groups=self) - | Q(personal_notes__lesson_period__lesson__groups__parent_groups=self) - ), + tardiness=Subquery( + Person.objects.filter(school_term_q & groups_q) + .filter(pk=OuterRef("pk"),) + .distinct() + .annotate(tardiness=Sum("personal_notes__late")) + .values("tardiness") ), tardiness_count=Count( "personal_notes", - filter=~Q(personal_notes__late=0) - & Q(personal_notes__lesson_period__lesson__validity__school_term=self.school_term,) - & ( - Q(personal_notes__lesson_period__lesson__groups=self) - | Q(personal_notes__lesson_period__lesson__groups__parent_groups=self) - ), + filter=~Q(personal_notes__late=0) & school_term_q & groups_q, + distinct=True, ), ) @@ -334,14 +447,7 @@ def generate_person_list_with_class_register_statistics( **{ extra_mark.count_label: Count( "personal_notes", - filter=Q( - personal_notes__extra_marks=extra_mark, - personal_notes__lesson_period__lesson__validity__school_term=self.school_term, # noqa - ) - & ( - Q(personal_notes__lesson_period__lesson__groups=self) - | Q(personal_notes__lesson_period__lesson__groups__parent_groups=self) - ), + filter=Q(personal_notes__extra_marks=extra_mark) & school_term_q & groups_q, ) } ) @@ -351,15 +457,9 @@ def generate_person_list_with_class_register_statistics( **{ excuse_type.count_label: Count( "personal_notes__absent", - filter=Q( - personal_notes__absent=True, - personal_notes__excuse_type=excuse_type, - personal_notes__lesson_period__lesson__validity__school_term=self.school_term, # noqa - ) - & ( - Q(personal_notes__lesson_period__lesson__groups=self) - | Q(personal_notes__lesson_period__lesson__groups__parent_groups=self) - ), + filter=Q(personal_notes__absent=True, personal_notes__excuse_type=excuse_type,) + & school_term_q + & groups_q, ) } ) diff --git a/aleksis/apps/alsijil/models.py b/aleksis/apps/alsijil/models.py index a0447a17423e487a68dbf57a80cc9ca15b0a54cc..163ef2b8636b25b9cbb3ec7f7d4e5f4d0e9926eb 100644 --- a/aleksis/apps/alsijil/models.py +++ b/aleksis/apps/alsijil/models.py @@ -1,5 +1,10 @@ +from datetime import date +from typing import Optional, Union +from urllib.parse import urlparse + from django.db import models -from django.urls import reverse +from django.db.models.constraints import CheckConstraint +from django.db.models.query_utils import Q from django.utils.formats import date_format from django.utils.translation import gettext_lazy as _ @@ -25,9 +30,9 @@ from aleksis.apps.alsijil.managers import ( ) from aleksis.apps.chronos.managers import GroupPropertiesMixin from aleksis.apps.chronos.mixins import WeekRelatedMixin -from aleksis.apps.chronos.models import LessonPeriod -from aleksis.apps.chronos.util.date import get_current_year +from aleksis.apps.chronos.models import Event, ExtraLesson, LessonPeriod from aleksis.core.mixins import ExtensibleModel +from aleksis.core.models import SchoolTerm from aleksis.core.util.core_helpers import get_site_preferences from aleksis.core.util.model_helpers import ICONS @@ -58,7 +63,104 @@ class ExcuseType(ExtensibleModel): verbose_name_plural = _("Excuse types") -class PersonalNote(ExtensibleModel, WeekRelatedMixin): +lesson_related_constraint_q = ( + Q( + lesson_period__isnull=False, + event__isnull=True, + extra_lesson__isnull=True, + week__isnull=False, + year__isnull=False, + ) + | Q( + lesson_period__isnull=True, + event__isnull=False, + extra_lesson__isnull=True, + week__isnull=True, + year__isnull=True, + ) + | Q( + lesson_period__isnull=True, + event__isnull=True, + extra_lesson__isnull=False, + week__isnull=True, + year__isnull=True, + ) +) + + +class RegisterObjectRelatedMixin(WeekRelatedMixin): + """Mixin with common API for lesson documentations and personal notes.""" + + @property + def register_object( + self: Union["LessonDocumentation", "PersonalNote"] + ) -> Union[LessonPeriod, Event, ExtraLesson]: + """Get the object related to this lesson documentation or personal note.""" + if self.lesson_period: + return self.lesson_period + elif self.event: + return self.event + else: + return self.extra_lesson + + @property + def calendar_week(self: Union["LessonDocumentation", "PersonalNote"]) -> CalendarWeek: + """Get the calendar week of this lesson documentation or personal note. + + .. note:: + + As events can be longer than one week, + this will return the week of the start date for events. + """ + if self.lesson_period: + return CalendarWeek(week=self.week, year=self.year) + elif self.extra_lesson: + return self.extra_lesson.calendar_week + else: + return CalendarWeek.from_date(self.register_object.date_start) + + @property + def school_term(self: Union["LessonDocumentation", "PersonalNote"]) -> SchoolTerm: + """Get the school term of the related register object.""" + if self.lesson_period: + return self.lesson_period.lesson.validity.school_term + else: + return self.register_object.school_term + + @property + def date(self: Union["LessonDocumentation", "PersonalNote"]) -> Optional[date]: + """Get the date of this lesson documentation or personal note. + + :: warning:: + + As events can be longer than one day, + this will return `None` for events. + """ + if self.lesson_period: + return super().date + elif self.extra_lesson: + return self.extra_lesson.date + return None + + @property + def date_formatted(self: Union["LessonDocumentation", "PersonalNote"]) -> str: + """Get a formatted version of the date of this object. + + Lesson periods, extra lessons: formatted date + Events: formatted date range + """ + return ( + date_format(self.date) + if self.date + else f"{date_format(self.event.date_start)}–{date_format(self.event.date_end)}" + ) + + def get_absolute_url(self: Union["LessonDocumentation", "PersonalNote"]) -> str: + """Get the absolute url of the detail view for the related register object.""" + return self.register_object.get_alsijil_url(self.calendar_week) + + +class PersonalNote(RegisterObjectRelatedMixin, ExtensibleModel): """A personal note about a single person. Used in the class register to note absences, excuses @@ -77,11 +179,17 @@ class PersonalNote(ExtensibleModel, WeekRelatedMixin): person = models.ForeignKey("core.Person", models.CASCADE, related_name="personal_notes") groups_of_person = models.ManyToManyField("core.Group", related_name="+") - week = models.IntegerField() - year = models.IntegerField(verbose_name=_("Year"), default=get_current_year) + week = models.IntegerField(blank=True, null=True) + year = models.IntegerField(verbose_name=_("Year"), blank=True, null=True) lesson_period = models.ForeignKey( - "chronos.LessonPeriod", models.CASCADE, related_name="personal_notes" + "chronos.LessonPeriod", models.CASCADE, related_name="personal_notes", blank=True, null=True + ) + event = models.ForeignKey( + "chronos.Event", models.CASCADE, related_name="personal_notes", blank=True, null=True + ) + extra_lesson = models.ForeignKey( + "chronos.ExtraLesson", models.CASCADE, related_name="personal_notes", blank=True, null=True ) absent = models.BooleanField(default=False) @@ -119,21 +227,16 @@ class PersonalNote(ExtensibleModel, WeekRelatedMixin): self.remarks = defaults.remarks self.extra_marks.clear() - def __str__(self): - return f"{date_format(self.date)}, {self.lesson_period}, {self.person}" + def __str__(self) -> str: + return f"{self.date_formatted}, {self.lesson_period}, {self.person}" - def get_absolute_url(self): - return ( - reverse( - "lesson_by_week_and_period", args=[self.year, self.week, self.lesson_period.pk], - ) - + "#personal-notes" - ) + def get_absolute_url(self) -> str: + """Get the absolute url of the detail view for the related register object.""" + return urlparse(super().get_absolute_url())._replace(fragment="personal-notes").geturl() class Meta: verbose_name = _("Personal note") verbose_name_plural = _("Personal notes") - unique_together = [["lesson_period", "week", "person"]] ordering = [ "year", "week", @@ -142,9 +245,14 @@ class PersonalNote(ExtensibleModel, WeekRelatedMixin): "person__last_name", "person__first_name", ] + constraints = [ + CheckConstraint( + check=lesson_related_constraint_q, name="one_relation_only_personal_note" + ) + ] -class LessonDocumentation(ExtensibleModel, WeekRelatedMixin): +class LessonDocumentation(RegisterObjectRelatedMixin, ExtensibleModel): """A documentation on a single lesson period. Non-personal, includes the topic and homework of the lesson. @@ -154,11 +262,17 @@ class LessonDocumentation(ExtensibleModel, WeekRelatedMixin): data_checks = [LessonDocumentationOnHolidaysDataCheck] - week = models.IntegerField() - year = models.IntegerField(verbose_name=_("Year"), default=get_current_year) + week = models.IntegerField(blank=True, null=True) + year = models.IntegerField(verbose_name=_("Year"), blank=True, null=True) lesson_period = models.ForeignKey( - "chronos.LessonPeriod", models.CASCADE, related_name="documentations" + "chronos.LessonPeriod", models.CASCADE, related_name="documentations", blank=True, null=True + ) + event = models.ForeignKey( + "chronos.Event", models.CASCADE, related_name="documentations", blank=True, null=True + ) + extra_lesson = models.ForeignKey( + "chronos.ExtraLesson", models.CASCADE, related_name="documentations", blank=True, null=True ) topic = models.CharField(verbose_name=_("Lesson topic"), max_length=200, blank=True) @@ -197,17 +311,14 @@ class LessonDocumentation(ExtensibleModel, WeekRelatedMixin): if changed: lesson_documentation.save() - def __str__(self): - return f"{self.lesson_period}, {date_format(self.date)}" - - def get_absolute_url(self): - return reverse( - "lesson_by_week_and_period", args=[self.year, self.week, self.lesson_period.pk], - ) + def __str__(self) -> str: + return f"{self.lesson_period}, {self.date_formatted}" def save(self, *args, **kwargs): - if get_site_preferences()["alsijil__carry_over"] and ( - self.topic or self.homework or self.group_note + if ( + get_site_preferences()["alsijil__carry_over"] + and (self.topic or self.homework or self.group_note) + and self.lesson_period ): self._carry_over_data() super().save(*args, **kwargs) @@ -215,13 +326,17 @@ class LessonDocumentation(ExtensibleModel, WeekRelatedMixin): class Meta: verbose_name = _("Lesson documentation") verbose_name_plural = _("Lesson documentations") - unique_together = [["lesson_period", "week"]] ordering = [ "year", "week", "lesson_period__period__weekday", "lesson_period__period__period", ] + constraints = [ + CheckConstraint( + check=lesson_related_constraint_q, name="one_relation_only_lesson_documentation", + ) + ] class ExtraMark(ExtensibleModel): diff --git a/aleksis/apps/alsijil/rules.py b/aleksis/apps/alsijil/rules.py index 6f2a8d614aab4ba0f8a5bb1d83fcfd7b6c0fd4f5..f36db7aa5a3b532d2b143c109c7daf089391c96a 100644 --- a/aleksis/apps/alsijil/rules.py +++ b/aleksis/apps/alsijil/rules.py @@ -29,7 +29,7 @@ from .util.predicates import ( ) # View lesson -view_lesson_predicate = has_person & ( +view_register_object_predicate = has_person & ( is_none # View is opened as "Current lesson" | is_lesson_teacher | is_lesson_participant @@ -37,19 +37,19 @@ view_lesson_predicate = has_person & ( | has_global_perm("alsijil.view_lesson") | has_lesson_group_object_perm("core.view_week_class_register_group") ) -add_perm("alsijil.view_lesson", view_lesson_predicate) +add_perm("alsijil.view_register_object", view_register_object_predicate) # View lesson in menu add_perm("alsijil.view_lesson_menu", has_person) # View lesson personal notes -view_lesson_personal_notes_predicate = view_lesson_predicate & ( +view_lesson_personal_notes_predicate = view_register_object_predicate & ( ~is_lesson_participant | is_lesson_teacher | has_global_perm("alsijil.view_personalnote") | has_lesson_group_object_perm("core.view_personalnote_group") ) -add_perm("alsijil.view_lesson_personalnote", view_lesson_personal_notes_predicate) +add_perm("alsijil.view_register_object_personalnote", view_lesson_personal_notes_predicate) # Edit personal note edit_lesson_personal_note_predicate = view_lesson_personal_notes_predicate & ( @@ -57,7 +57,7 @@ edit_lesson_personal_note_predicate = view_lesson_personal_notes_predicate & ( | has_global_perm("alsijil.change_personalnote") | has_lesson_group_object_perm("core.edit_personalnote_group") ) -add_perm("alsijil.edit_lesson_personalnote", edit_lesson_personal_note_predicate) +add_perm("alsijil.edit_register_object_personalnote", edit_lesson_personal_note_predicate) # View personal note view_personal_note_predicate = has_person & ( @@ -78,11 +78,11 @@ edit_personal_note_predicate = view_personal_note_predicate & ( add_perm("alsijil.edit_personalnote", edit_personal_note_predicate) # View lesson documentation -view_lesson_documentation_predicate = view_lesson_predicate +view_lesson_documentation_predicate = view_register_object_predicate add_perm("alsijil.view_lessondocumentation", view_lesson_documentation_predicate) # Edit lesson documentation -edit_lesson_documentation_predicate = view_lesson_predicate & ( +edit_lesson_documentation_predicate = view_register_object_predicate & ( is_lesson_teacher | has_global_perm("alsijil.change_lessondocumentation") | has_lesson_group_object_perm("core.edit_lessondocumentation_group") diff --git a/aleksis/apps/alsijil/templates/alsijil/class_register/lesson.html b/aleksis/apps/alsijil/templates/alsijil/class_register/lesson.html index f04e06fc317d58d202501ff09ff38a76f1c85e1a..f21d7686acb763c6f63942dcab91517020b7e0e2 100644 --- a/aleksis/apps/alsijil/templates/alsijil/class_register/lesson.html +++ b/aleksis/apps/alsijil/templates/alsijil/class_register/lesson.html @@ -10,21 +10,21 @@ {% endblock %} {% block content %} - {% if next_lesson_person or prev_lesson_person %} + {% if next_lesson_person or prev_lesson_person or lesson_documentation %} <div class="row no-margin"> <div class="col s12 no-padding"> {# Back to week view #} - {% with lesson_period.get_lesson_documentation as lesson_doc %} - <a href="{% url "week_view_by_week" lesson_doc.year lesson_doc.week "group" lesson_period.lesson.groups.all.0.pk %}" + {% if lesson_documentation %} + <a href="{% url "week_view_by_week" lesson_documentation.calendar_week.year lesson_documentation.calendar_week.week "group" register_object.get_groups.all.0.pk %}" class="btn primary-color waves-light waves-effect alsijil-top-button"> <i class="material-icons left">chevron_left</i> {% trans "Back to week view" %} </a> - {% endwith %} + {% endif %} {# Next lesson #} {% if prev_lesson_person %} <a class="btn primary waves-effect waves-light alsijil-top-button" - href="{% url "lesson_by_week_and_period" prev_lesson_person.week.year prev_lesson_person.week.week prev_lesson_person.id %}"> + href="{% url "lesson_period" prev_lesson_person.week.year prev_lesson_person.week.week prev_lesson_person.id %}"> <i class="material-icons left">arrow_back</i> {% trans "My previous lesson" %} </a> @@ -33,7 +33,7 @@ {# Previous lesson #} {% if next_lesson_person %} <a class="btn primary right waves-effect waves-light alsijil-top-button" - href="{% url "lesson_by_week_and_period" next_lesson_person.week.year next_lesson_person.week.week next_lesson_person.id %}"> + href="{% url "lesson_period" next_lesson_person.week.year next_lesson_person.week.week next_lesson_person.id %}"> <i class="material-icons right">arrow_forward</i> {% trans "My next lesson" %} </a> @@ -43,51 +43,60 @@ {% endif %} <h4> - {{ day }}, {% blocktrans with period=lesson_period.period.period %}{{ period }}. period{% endblocktrans %} – + {% if register_object.label_ == "event" %} + {{ register_object.date_start }} {{ register_object.period_from.period }}.–{{ register_object.date_end }} + {{ register_object.period_to.period }}., + {% else %} + {{ day }}, {% blocktrans with period=register_object.period.period %}{{ period }}. period{% endblocktrans %} – + {% endif %} - {% for group in lesson_period.get_groups.all %} - <span>{{ group.name }}</span>, - {% endfor %} + {{ register_object.group_names }}, - {{ lesson_period.get_subject.name }}, + {% if register_object.label_ == "event" %} + {% trans "Event" %} ({{ register_object.title }}) + {% else %} + {{ register_object.get_subject.name }} + {% endif %}, - {% for teacher in lesson_period.get_teachers.all %} - {{ teacher.short_name }} - {% endfor %} + {{ register_object.teacher_short_names }} <span class="right"> - {% include "alsijil/partials/lesson_status_icon.html" with period=lesson_period css_class="medium" %} - </span> + {% include "alsijil/partials/lesson_status_icon.html" with register_object=register_object css_class="medium" %} + </span> </h4> <br/> - {% has_perm "alsijil.view_lessondocumentation" user lesson_period as can_view_lesson_documentation %} - {% has_perm "alsijil.edit_lessondocumentation" user lesson_period as can_edit_lesson_documentation %} - {% has_perm "alsijil.edit_lesson_personalnote" user lesson_period as can_edit_lesson_personalnote %} + {% has_perm "alsijil.view_lessondocumentation" user register_object as can_view_lesson_documentation %} + {% has_perm "alsijil.edit_lessondocumentation" user register_object as can_edit_lesson_documentation %} + {% has_perm "alsijil.edit_register_object_personalnote" user register_object as can_edit_register_object_personalnote %} <form method="post" class="row"> <p> {% if not blocked_because_holidays %} - {% if can_edit_lesson_documentation or can_edit_lesson_personalnote %} + {% if can_edit_lesson_documentation or can_edit_register_object_personalnote %} {% include "core/partials/save_button.html" %} {% endif %} {% endif %} - <a class="btn waves-effect waves-light primary" - href="{% url "lesson_by_week_and_period" prev_lesson.week.year prev_lesson.week.week prev_lesson.id %}"> - <i class="material-icons left">arrow_back</i> - {% blocktrans with subject=lesson_period.get_subject.name %} - Previous {{ subject }} lesson - {% endblocktrans %} - </a> - - <a class="btn right waves-effect waves-light primary" - href="{% url "lesson_by_week_and_period" next_lesson.week.year next_lesson.week.week next_lesson.id %}"> - <i class="material-icons right">arrow_forward</i> - {% blocktrans with subject=lesson_period.get_subject.name %} - Next {{ subject }} lesson - {% endblocktrans %} - </a> + {% if prev_lesson %} + <a class="btn waves-effect waves-light primary" + href="{% url "lesson_period" prev_lesson.week.year prev_lesson.week.week prev_lesson.id %}"> + <i class="material-icons left">arrow_back</i> + {% blocktrans with subject=register_object.get_subject.name %} + Previous {{ subject }} lesson + {% endblocktrans %} + </a> + {% endif %} + + {% if next_lesson %} + <a class="btn right waves-effect waves-light primary" + href="{% url "lesson_period" next_lesson.week.year next_lesson.week.week next_lesson.id %}"> + <i class="material-icons right">arrow_forward</i> + {% blocktrans with subject=register_object.get_subject.name %} + Next {{ subject }} lesson + {% endblocktrans %} + </a> + {% endif %} </p> {% csrf_token %} @@ -100,13 +109,13 @@ <li class="tab"> <a href="#lesson-documentation">{% trans "Lesson documentation" %}</a> </li> - {% if not lesson_period.get_substitution.cancelled or not request.site.preferences.alsijil__block_personal_notes_for_cancelled %} + {% if register_object.label_ != "lesson_period" or not register_object.get_substitution.cancelled or not request.site.preferences.alsijil__block_personal_notes_for_cancelled %} <li class="tab"> <a href="#personal-notes">{% trans "Personal notes" %}</a> </li> {% endif %} - {% has_perm "alsijil.view_lessondocumentation" user lesson_period.prev as can_view_prev_lesson_documentation %} - {% if lesson_period.prev.get_lesson_documentation and can_view_prev_lesson_documentation %} + {% has_perm "alsijil.view_lessondocumentation" user register_object.prev as can_view_prev_lesson_documentation %} + {% if register_object.prev.get_lesson_documentation and can_view_prev_lesson_documentation %} <li class="tab"> <a href="#previous-lesson">{% trans "Previous lesson" %}</a> </li> @@ -163,7 +172,7 @@ </div> </div> - {% with prev_lesson=lesson_period.prev prev_doc=prev_lesson.get_lesson_documentation %} + {% with prev_lesson=register_object.prev prev_doc=prev_lesson.get_lesson_documentation %} {% with absences=prev_lesson.get_absences tardinesses=prev_lesson.get_tardinesses extra_marks=prev_lesson.get_extra_marks %} {% has_perm "alsijil.view_lessondocumentation" user prev_lesson as can_view_prev_lesson_documentation %} {% if prev_doc and can_view_prev_lesson_documentation %} @@ -234,14 +243,14 @@ {% endwith %} {% endwith %} - {% if not lesson_period.get_substitution.cancelled or not request.site.preferences.alsijil__block_personal_notes_for_cancelled %} + {% if register_object.label_ != "lesson_period" or not register_object.get_substitution.cancelled or not request.site.preferences.alsijil__block_personal_notes_for_cancelled %} <div class="col s12" id="personal-notes"> <div class="card"> <div class="card-content"> <span class="card-title"> {% blocktrans %}Personal notes{% endblocktrans %} </span> - {% if can_edit_lesson_personalnote %} + {% if can_edit_register_object_personalnote %} {% form form=personal_note_formset.management_form %}{% endform %} {% endif %} @@ -259,7 +268,7 @@ </thead> <tbody> {% for form in personal_note_formset %} - {% if can_edit_lesson_personalnote %} + {% if can_edit_register_object_personalnote %} <tr> {{ form.id }} <td>{{ form.person_name }}{{ form.person_name.value }} @@ -357,7 +366,7 @@ {% if group_roles %} <div class="col s12" id="group-roles"> - {% include "alsijil/group_role/partials/assigned_roles.html" with roles=group_roles group=lesson_period.lesson.groups.first back_url=back_url %} + {% include "alsijil/group_role/partials/assigned_roles.html" with roles=group_roles group=register_object.get_groups.first back_url=back_url %} </div> {% endif %} @@ -377,25 +386,29 @@ <p> - {% if can_edit_lesson_documentation or can_edit_lesson_personalnote %} + {% if can_edit_lesson_documentation or can_edit_register_object_personalnote %} {% include "core/partials/save_button.html" %} {% endif %} - <a class="btn primary waves-effect waves-light" - href="{% url "lesson_by_week_and_period" prev_lesson.week.year prev_lesson.week.week prev_lesson.id %}"> - <i class="material-icons left">arrow_back</i> - {% blocktrans with subject=lesson_period.get_subject.name %} - Previous {{ subject }} lesson - {% endblocktrans %} - </a> + {% if prev_lesson %} + <a class="btn primary waves-effect waves-light" + href="{% url "lesson_period" prev_lesson.week.year prev_lesson.week.week prev_lesson.id %}"> + <i class="material-icons left">arrow_back</i> + {% blocktrans with subject=register_object.get_subject.name %} + Previous {{ subject }} lesson + {% endblocktrans %} + </a> + {% endif %} - <a class="btn primary right waves-effect waves-light" - href="{% url "lesson_by_week_and_period" next_lesson.week.year next_lesson.week.week next_lesson.id %}"> - <i class="material-icons right">arrow_forward</i> - {% blocktrans with subject=lesson_period.get_subject.name %} - Next {{ subject }} lesson - {% endblocktrans %} - </a> + {% if next_lesson %} + <a class="btn primary right waves-effect waves-light" + href="{% url "lesson_period" next_lesson.week.year next_lesson.week.week next_lesson.id %}"> + <i class="material-icons right">arrow_forward</i> + {% blocktrans with subject=register_object.get_subject.name %} + Next {{ subject }} lesson + {% endblocktrans %} + </a> + {% endif %} </p> {% else %} diff --git a/aleksis/apps/alsijil/templates/alsijil/class_register/person.html b/aleksis/apps/alsijil/templates/alsijil/class_register/person.html index 259b7b3471c1508f08b29188e5e4a578204db8f2..7254e0e24be0f979f9fa1f01a05a0b4c8f10e532 100644 --- a/aleksis/apps/alsijil/templates/alsijil/class_register/person.html +++ b/aleksis/apps/alsijil/templates/alsijil/class_register/person.html @@ -37,7 +37,6 @@ <ul class="collection"> {% for note in unexcused_absences %} - {% weekday_to_date note.calendar_week note.lesson_period.period.weekday as note_date %} <li class="collection-item"> {% has_perm "alsijil.edit_personalnote" user note as can_edit_personal_note %} {% if can_edit_personal_note %} @@ -54,7 +53,7 @@ {% endif %} <i class="material-icons left red-text">warning</i> <p class="no-margin"> - <a href="{% url "lesson_by_week_and_period" note.year note.week note.lesson_period.pk %}">{{ note_date }}, {{ note.lesson_period }}</a> + <a href="{{ note.get_absolute_url }}">{{ note.date }}, {{ note.lesson_period }}</a> </p> {% if note.remarks %} <p class="no-margin"><em>{{ note.remarks }}</em></p> @@ -130,38 +129,44 @@ <div> <ul> {% for note in personal_notes %} - {% ifchanged note.lesson_period.lesson.validity.school_term %}</ul></div></li> + {% ifchanged note.school_term %}</ul></div></li> <li {% if forloop.first %}class="active"{% endif %}> <div class="collapsible-header"><i - class="material-icons">date_range</i>{{ note.lesson_period.lesson.validity.school_term }}</div> + class="material-icons">date_range</i>{{ note.school_term }}</div> <div class="collapsible-body"> <ul class="collection"> {% endifchanged %} {% ifchanged note.week %} <li class="collection-item"> - <strong>{% blocktrans with week=note.week %}Week {{ week }}{% endblocktrans %}</strong> + <strong>{% blocktrans with week=note.calendar_week.week %}Week {{ week }}{% endblocktrans %}</strong> </li> {% endifchanged %} - {% weekday_to_date note.calendar_week note.lesson_period.period.weekday as note_date %} - {% ifchanged note_date %} + {% ifchanged note.date %} <li class="collection-item"> - {% if can_mark_all_as_excused %} + {% if can_mark_all_as_excused and note.date %} <form action="" method="post" class="right hide-on-small-only" style="margin-top: -7px;"> {% csrf_token %} {% trans "Mark all as" %} - <input type="hidden" value="{{ note_date|date:"Y-m-d" }}" name="date"> + <input type="hidden" value="{{ note.date|date:"Y-m-d" }}" name="date"> {% include "alsijil/partials/mark_as_buttons.html" %} </form> {% endif %} <i class="material-icons left">schedule</i> - {{ note_date }} - {% if can_mark_all_as_excused %} + {% if note.date %} + {{ note.date }} + {% else %} + {{ note.register_object.date_start }} + {{ note.register_object.period_from.period }}.–{{ note.register_object.date_end }} + {{ note.register_object.period_to.period }}. + {% endif %} + + {% if can_mark_all_as_excused and note.date %} <form action="" method="post" class="hide-on-med-and-up"> {% csrf_token %} {% trans "Mark all as" %} - <input type="hidden" value="{{ note_date|date:"Y-m-d" }}" name="date"> + <input type="hidden" value="{{ note.date|date:"Y-m-d" }}" name="date"> {% include "alsijil/partials/mark_as_buttons.html" %} </form> {% endif %} @@ -171,14 +176,20 @@ <li class="collection-item"> <div class="row no-margin"> <div class="col s2 m1"> - {{ note.lesson_period.period.period }}. + {% if note.register_object.period %} + {{ note.register_object.period.period }}. + {% endif %} </div> <div class="col s10 m4"> <i class="material-icons left">event_note</i> - <a href="{% url "lesson_by_week_and_period" note.year note.week note.lesson_period.pk %}"> - {{ note.lesson_period.get_subject.name }}<br/> - {{ note.lesson_period.get_teacher_names }} + <a href="{{ note.get_absolute_url }}"> + {% if note.register_object.get_subject %} + {{ note.register_object.get_subject.name }} + {% else %} + {% trans "Event" %} ({{ note.register_object.title }}) + {% endif %}<br/> + {{ note.register_object.teacher_names }} </a> </div> diff --git a/aleksis/apps/alsijil/templates/alsijil/class_register/week_view.html b/aleksis/apps/alsijil/templates/alsijil/class_register/week_view.html index 24780eba16318c05d925d57aed87637caa9c2a85..92f92a2b242e497b60cf3a79fde1542810b650e2 100644 --- a/aleksis/apps/alsijil/templates/alsijil/class_register/week_view.html +++ b/aleksis/apps/alsijil/templates/alsijil/class_register/week_view.html @@ -78,17 +78,14 @@ </ul> </div> <div class="col s12" id="week-overview"> - {% regroup lesson_periods by period.get_weekday_display as periods_by_day %} - {% for weekday, periods in periods_by_day %} - {% with weekdays|get_dict:periods.0.period.weekday as advanced_weekday %} - {% weekday_to_date week periods.0.period.weekday as current_date %} - + {% for weekday, objects in regrouped_objects.items %} + {% with weekdays|get_dict:forloop.counter0 as advanced_weekday %} {% if advanced_weekday.holiday and not request.site.preferences.alsijil__allow_entries_in_holidays %} <div class="card"> <div class="card-content"> - {% weekday_to_date week periods.0.period.weekday as current_date %} <span class="card-title"> - {{ weekday }}, {{ current_date }} <span class="badge new blue no-float">{{ advanced_weekday.holiday }}</span> + {{ advanced_weekday.name }}, {{ advanced_weekday.date }} <span + class="badge new blue no-float">{{ advanced_weekday.holiday }}</span> </span> </div> </div> @@ -96,7 +93,7 @@ <div class="card show-on-extra-large"> <div class="card-content"> <span class="card-title"> - {{ weekday }}, {{ current_date }} + {{ advanced_weekday.name }}, {{ advanced_weekday.date }} </span> <table class="striped datatable"> <thead> @@ -114,55 +111,69 @@ </tr> </thead> <tbody> - {% for period in periods %} - {% has_perm "alsijil.view_lessondocumentation" user period as can_view_lesson_documentation %} + {% for register_object in objects %} + {% has_perm "alsijil.view_lessondocumentation" user register_object as can_view_lesson_documentation %} {% if can_view_lesson_documentation %} <tr> <td class="center-align"> - {% include "alsijil/partials/lesson_status_icon.html" with period=period %} + {% include "alsijil/partials/lesson_status_icon.html" with register_object=register_object %} </td> <td class="tr-link"> <a class="tr-link" - href="{% url 'lesson_by_week_and_period' week.year week.week period.id %}"> - {{ period.period.period }}. + href="{{ register_object.alsijil_url }}"> + {% if register_object.period %} + {{ register_object.period.period }}. + {% else %} + {{ register_object.period_from_on_day }}.–{{ register_object.period_to_on_day }}. + {% endif %} </a> </td> {% if not group %} <td> <a class="tr-link" - href="{% url 'lesson_by_week_and_period' week.year week.week period.id %}"> - {{ period.lesson.group_names }} + href="{{ register_object.alsijil_url }}"> + {% if register_object.lesson %} + {{ register_object.lesson.group_names }} + {% else %} + {{ register_object.group_names }} + {% endif %} </a> </td> {% endif %} <td> <a class="tr-link" - href="{% url 'lesson_by_week_and_period' week.year week.week period.id %}"> - {{ period.get_subject.name }} + href="{{ register_object.alsijil_url }}"> + {% if register_object.get_subject %} + {{ register_object.get_subject.name }} + {% elif register_object.subject %} + {{ register_object.subject }} + {% else %} + {% trans "Event" %} ({{ register_object.title }}) + {% endif %} </a> </td> <td> <a class="tr-link" - href="{% url 'lesson_by_week_and_period' week.year week.week period.id %}"> - {{ period.get_teacher_names }} + href="{{ register_object.alsijil_url }}"> + {{ register_object.teacher_names }} </a> </td> <td> <a class="tr-link" - href="{% url 'lesson_by_week_and_period' week.year week.week period.id %}"> - {% firstof period.get_lesson_documentation.topic "–" %} + href="{{ register_object.alsijil_url }}"> + {% firstof register_object.get_lesson_documentation.topic "–" %} </a> </td> <td> <a class="tr-link" - href="{% url 'lesson_by_week_and_period' week.year week.week period.id %}"> - {% firstof period.get_lesson_documentation.homework "–" %} + href="{{ register_object.alsijil_url }}"> + {% firstof register_object.get_lesson_documentation.homework "–" %} </a> </td> <td> <a class="tr-link" - href="{% url 'lesson_by_week_and_period' week.year week.week period.id %}"> - {% firstof period.get_lesson_documentation.group_note "–" %} + href="{{ register_object.alsijil_url }}"> + {% firstof register_object.get_lesson_documentation.group_note "–" %} </a> </td> </tr> @@ -174,48 +185,69 @@ </div> <ul class="collapsible hide-on-extra-large-only"> <li class=""> - {% weekday_to_date week periods.0.period.weekday as current_date %} <div class="collapsible-header flow-text"> - {{ weekday }}, {{ current_date }} <i class="material-icons collapsible-icon-right">expand_more</i> + {{ advanced_weekday.name }}, {{ advanced_weekday.date }} <i + class="material-icons collapsible-icon-right">expand_more</i> </div> <div class="collapsible-body"> <div class="collection"> - {% for period in periods %} - {% has_perm "alsijil.view_lessondocumentation" user period as can_view_lesson_documentation %} + {% for register_object in objects %} + {% has_perm "alsijil.view_lessondocumentation" user register_object as can_view_lesson_documentation %} {% if can_view_lesson_documentation %} <a class="collection-item avatar" - href="{% url 'lesson_by_week_and_period' week.year week.week period.id %}"> - {% include "alsijil/partials/lesson_status_icon.html" with period=period css_class="materialize-circle" color_suffix=" " %} + href="{{ register_object.alsijil_url }}"> + {% include "alsijil/partials/lesson_status_icon.html" with register_object=register_object css_class="materialize-circle" color_suffix=" " %} <table class="hide-on-med-and-down"> <tr> <th>{% trans "Subject" %}</th> - <td>{{ period.period.period }}. {{ period.get_subject.name }}</td> + <td> + {% if register_object.period %} + {{ register_object.period.period }}. + {% else %} + {{ register_object.period_from_on_day }}.–{{ register_object.period_to_on_day }}. + {% endif %} + {% if register_object.get_subject %} + {{ register_object.get_subject.name }} + {% elif register_object.subject %} + {{ register_object.subject }} + {% else %} + {% trans "Event" %} + {% endif %} + </td> </tr> {% if not group %} <tr> - <th>{% trans "Group" %}</th> - <td>{{ period.lesson.group_names }}</td> + <th>{% trans "Groups" %}</th> + <td> + {% if register_object.lesson %} + {{ register_object.lesson.group_names }} + {% else %} + {{ register_object.group_names }} + {% endif %} + </td> </tr> {% endif %} <tr> <th>{% trans "Teachers" %}</th> - <td>{{ period.lesson.teacher_names }}</td> + <td> + {{ register_object.teacher_names }} + </td> </tr> <tr> <th>{% trans "Lesson topic" %}</th> - <td>{% firstof period.get_lesson_documentation.topic "–" %}</td> + <td>{% firstof register_object.get_lesson_documentation.topic "–" %}</td> </tr> {% with period.get_lesson_documentation as lesson_documentation %} {% if lesson_documentation.homework %} <tr> <th>{% trans "Homework" %}</th> - <td>{% firstof period.get_lesson_documentation.homework "–" %}</td> + <td>{% firstof register_object.get_lesson_documentation.homework "–" %}</td> </tr> {% endif %} {% if lesson_documentation.group_note %} <tr> <th>{% trans "Group note" %}</th> - <td>{% firstof period.get_lesson_documentation.group_note "–" %}</td> + <td>{% firstof register_object.get_lesson_documentation.group_note "–" %}</td> </tr> {% endif %} {% endwith %} @@ -223,32 +255,45 @@ <div class="hide-on-large-only"> <ul class="collection"> <li class="collection-item"> - {{ period.period.period }}. {{ period.get_subject.name }} + {% if register_object.period %} + {{ register_object.period.period }}. + {% else %} + {{ register_object.period_from_on_day }}.–{{ register_object.period_to_on_day }}. + {% endif %} + {% if register_object.get_subject %} + {{ register_object.get_subject.name }} + {% elif register_object.subject %} + {{ register_object.subject }} + {% else %} + {% trans "Event" %} ({{ register_object.title }}) + {% endif %} </li> {% if not group %} <li class="collection-item"> - - {{ period.lesson.group_names }} - + {% if register_object.lesson %} + {{ register_object.lesson.group_names }} + {% else %} + {{ register_object.group_names }} + {% endif %} </li> {% endif %} <li class="collection-item"> - {{ period.lesson.teacher_names }} + {{ register_object.teacher_names }} </li> <li class="collection-item"> - {{ period.get_lesson_documentation.topic }} + {{ register_object.get_lesson_documentation.topic }} </li> {% with period.get_lesson_documentation as lesson_documentation %} {% if lesson_documentation.homework %} <li class="collection-item"> <strong>{% trans "Homework" %}</strong> - {% firstof period.get_lesson_documentation.homework "–" %} + {% firstof register_object.get_lesson_documentation.homework "–" %} </li> {% endif %} {% if lesson_documentation.group_note %} <li class="collection-item"> <strong>{% trans "Group note" %}</strong> - {% firstof period.get_lesson_documentation.group_note "–" %} + {% firstof register_object.get_lesson_documentation.group_note "–" %} </li> {% endif %} {% endwith %} @@ -312,10 +357,10 @@ {% if note.remarks %} <blockquote> {{ note.remarks }} - {% weekday_to_date week note.lesson_period.period.weekday as note_date %} + {% weekday_to_date week note.register_object.period.weekday as note_date %} <em class="right"> - <a href="{% url 'lesson_by_week_and_period' week.year week.week note.lesson_period.id %}"> - {{ note_date }}, {{ note.lesson_period.get_subject.name }} + <a href="{{ note.register_object.alsijil_url }}"> + {{ note.date }}, {{ note.register_object.get_subject.name }} </a> </em> </blockquote> diff --git a/aleksis/apps/alsijil/templates/alsijil/partials/lesson_status_icon.html b/aleksis/apps/alsijil/templates/alsijil/partials/lesson_status_icon.html index 4ad4aa8251059cbe5453f24ca5e00cd6a4490bd2..ceff8c1a17ea0452b1f013d759dbee7edfd6c630 100644 --- a/aleksis/apps/alsijil/templates/alsijil/partials/lesson_status_icon.html +++ b/aleksis/apps/alsijil/templates/alsijil/partials/lesson_status_icon.html @@ -2,19 +2,30 @@ {% now_datetime as now_dt %} -{% if period.has_documentation %} +{% if register_object.has_documentation %} <i class="material-icons green{% firstof color_suffix "-text"%} tooltipped {{ css_class }}" data-position="bottom" data-tooltip="{% trans "Data complete" %}" title="{% trans "Data complete" %}">check_circle</i> +{% elif not register_object.period %} + {% period_to_time_start week register_object.raw_period_from_on_day as time_start %} + {% period_to_time_end week register_object.raw_period_to_on_day as time_end %} + + {% if now_dt > time_end %} + <i class="material-icons red{% firstof color_suffix "-text"%} tooltipped {{ css_class }}" data-position="bottom" data-tooltip="{% trans "Missing data" %}" title="{% trans "Missing data" %}">history</i> + {% elif now_dt > time_start and now_dt < time_end %} + <i class="material-icons orange{% firstof color_suffix "-text"%} tooltipped {{ css_class }}" data-position="bottom" data-tooltip="{% trans "Pending" %}" title="{% trans "Pending" %}">more_horiz</i> + {% else %} + <i class="material-icons purple{% firstof color_suffix "-text"%} tooltipped {{ css_class }}" data-position="bottom" data-tooltip="{% trans "Event" %}" title="{% trans "Event" %}">event</i> + {% endif %} {% else %} - {% period_to_time_start week period.period as time_start %} - {% period_to_time_end week period.period as time_end %} + {% period_to_time_start week register_object.period as time_start %} + {% period_to_time_end week register_object.period as time_end %} - {% if period.get_substitution.cancelled %} + {% if register_object.get_substitution.cancelled %} <i class="material-icons red{% firstof color_suffix "-text"%} tooltipped {{ css_class }}" data-position="bottom" data-tooltip="{% trans "Lesson cancelled" %}" title="{% trans "Lesson cancelled" %}">cancel</i> {% elif now_dt > time_end %} <i class="material-icons red{% firstof color_suffix "-text"%} tooltipped {{ css_class }}" data-position="bottom" data-tooltip="{% trans "Missing data" %}" title="{% trans "Missing data" %}">history</i> {% elif now_dt > time_start and now_dt < time_end %} <i class="material-icons orange{% firstof color_suffix "-text"%} tooltipped {{ css_class }}" data-position="bottom" data-tooltip="{% trans "Pending" %}" title="{% trans "Pending" %}">more_horiz</i> - {% elif period.get_substitution %} + {% elif register_object.get_substitution %} <i class="material-icons orange{% firstof color_suffix "-text"%} tooltipped {{ css_class }}" data-position="bottom" data-tooltip="{% trans "Substitution" %}" title="{% trans "Substitution" %}">update</i> {% endif %} {% endif %} diff --git a/aleksis/apps/alsijil/templates/alsijil/print/full_register.html b/aleksis/apps/alsijil/templates/alsijil/print/full_register.html index c95a168f7dc6bbe82f6a8823b0a074abf836b756..ca2d801d60cf9657be5378c38abf5903304a2ed7 100644 --- a/aleksis/apps/alsijil/templates/alsijil/print/full_register.html +++ b/aleksis/apps/alsijil/templates/alsijil/print/full_register.html @@ -318,12 +318,24 @@ <tbody> {% for note in person.filtered_notes %} {% if note.absent or note.late or note.remarks or note.extra_marks.all %} - {% weekday_to_date note.calendar_week note.lesson_period.period.weekday as note_date %} <tr> - <td>{{ note_date }}</td> - <td>{{ note.lesson_period.period.period }}</td> - <td>{{ note.lesson_period.get_subject.short_name }} </td> - <td>{{ note.lesson_period.get_teachers.first.short_name }}</td> + {% if note.date %} + <td>{{ note.date }}</td> + <td>{{ note.register_object.period.period }}</td> + {% else %} + <td colspan="2"> + {{ note.register_object.date_start }} {{ note.register_object.period_from.period }}.–{{ note.register_object.date_end }} + {{ note.register_object.period_to.period }}. + </td> + {% endif %} + <td> + {% if note.register_object.label_ != "event" %} + {{ note.register_object.get_subject.short_name }} + {% else %} + {% trans "Event" %} + {% endif %} + </td> + <td>{{ note.register_object.teacher_short_names }}</td> <td> {% if note.absent %} {% trans 'Yes' %} @@ -374,8 +386,8 @@ </thead> <tbody> {% for day in week %} - {% with periods_by_day|get_dict:day as periods %} - {% for period, documentations, notes, substitution in periods %} + {% with register_objects_by_day|get_dict:day as register_objects %} + {% for register_object, documentations, notes, substitution in register_objects %} <tr class=" {% if substitution %} {% if substitution.cancelled %} @@ -389,18 +401,28 @@ {% endif %} "> {% if forloop.first %} - <th rowspan="{{ periods|length }}" class="lessons-day-head">{{ day }}</th> + <th rowspan="{{ register_objects|length }}" class="lessons-day-head">{{ day }}</th> {% endif %} - <td class="lesson-pe">{{ period.period.period }}</td> + <td class="lesson-pe"> + {% if register_object.label_ != "event" %} + {{ register_object.period.period }} + {% else %} + {{ register_object.period_from_on_day }}.–{{ register_object.period_to_on_day }}. + {% endif %} + </td> <td class="lesson-subj"> - {% if substitution %} + {% if register_object.label_ == "event" %} + <strong>{% trans "Event" %}</strong> + {% elif substitution %} {% include "chronos/partials/subs/subject.html" with type="substitution" el=substitution %} {% else %} - {% include "chronos/partials/subject.html" with subject=period.lesson.subject %} + {% include "chronos/partials/subject.html" with subject=register_object.get_subject %} {% endif %} </td> <td class="lesson-topic"> - {% if substitution.cancelled %} + {% if register_object.label_ == "event" %} + {{ register_object.title }}: {{ documentations.0.topic }} + {% elif substitution.cancelled %} {% trans 'Lesson cancelled' %} {% else %} {{ documentations.0.topic }} @@ -451,7 +473,7 @@ </td> <td class="lesson-te"> {% if documentations.0.topic %} - {{ substitution.teachers.first.short_name|default:period.lesson.teachers.first.short_name }} + {{ substitution.teachers.first.short_name|default:register_object.get_teachers.first.short_name }} {% endif %} </td> </tr> diff --git a/aleksis/apps/alsijil/urls.py b/aleksis/apps/alsijil/urls.py index 864c862ae36490c5a7817c7e7ddfce17042930c6..16ba0d0b9c5c4c976c26222a8542d3e96a93b1aa 100644 --- a/aleksis/apps/alsijil/urls.py +++ b/aleksis/apps/alsijil/urls.py @@ -3,12 +3,20 @@ from django.urls import path from . import views urlpatterns = [ - path("lesson", views.lesson, name="lesson"), + path("lesson", views.register_object, {"model": "lesson"}, name="lesson_period"), path( - "lesson/<int:year>/<int:week>/<int:period_id>", - views.lesson, - name="lesson_by_week_and_period", + "lesson/<int:year>/<int:week>/<int:id_>", + views.register_object, + {"model": "lesson"}, + name="lesson_period", ), + path( + "extra_lesson/<int:id_>/", + views.register_object, + {"model": "extra_lesson"}, + name="extra_lesson", + ), + path("event/<int:id_>/", views.register_object, {"model": "event"}, name="event",), path("week/", views.week_view, name="week_view"), path("week/<int:year>/<int:week>/", views.week_view, name="week_view_by_week"), path("week/year/cw/", views.week_view, name="week_view_placeholders"), diff --git a/aleksis/apps/alsijil/util/alsijil_helpers.py b/aleksis/apps/alsijil/util/alsijil_helpers.py index 432afb18f2edf21b118539992342cb674bd8d0ae..9aedcb24ea2c8dd2c529d273b701b082281786ea 100644 --- a/aleksis/apps/alsijil/util/alsijil_helpers.py +++ b/aleksis/apps/alsijil/util/alsijil_helpers.py @@ -1,35 +1,44 @@ -from typing import Optional +from typing import List, Optional, Union +from django.db.models.expressions import Exists, OuterRef +from django.db.models.query import Prefetch, QuerySet +from django.db.models.query_utils import Q from django.http import HttpRequest from calendarweek import CalendarWeek -from aleksis.apps.chronos.models import LessonPeriod +from aleksis.apps.alsijil.models import LessonDocumentation +from aleksis.apps.chronos.models import Event, ExtraLesson, LessonPeriod from aleksis.apps.chronos.util.chronos_helpers import get_el_by_pk -def get_lesson_period_by_pk( +def get_register_object_by_pk( request: HttpRequest, + model: Optional[str] = None, year: Optional[int] = None, week: Optional[int] = None, - period_id: Optional[int] = None, -): - """Get LessonPeriod object either by given object_id or by time and current person.""" + id_: Optional[int] = None, +) -> Optional[Union[LessonPeriod, Event, ExtraLesson]]: + """Get register object either by given object_id or by time and current person.""" wanted_week = CalendarWeek(year=year, week=week) - if period_id: - lesson_period = LessonPeriod.objects.annotate_week(wanted_week).get(pk=period_id) + if id_ and model == "lesson": + register_object = LessonPeriod.objects.annotate_week(wanted_week).get(pk=id_) + elif id_ and model == "event": + register_object = Event.objects.get(pk=id_) + elif id_ and model == "extra_lesson": + register_object = ExtraLesson.objects.get(pk=id_) elif hasattr(request, "user") and hasattr(request.user, "person"): if request.user.person.lessons_as_teacher.exists(): - lesson_period = ( + register_object = ( LessonPeriod.objects.at_time().filter_teacher(request.user.person).first() ) else: - lesson_period = ( + register_object = ( LessonPeriod.objects.at_time().filter_participant(request.user.person).first() ) else: - lesson_period = None - return lesson_period + register_object = None + return register_object def get_timetable_instance_by_pk( @@ -44,3 +53,49 @@ def get_timetable_instance_by_pk( return get_el_by_pk(request, type_, id_) elif hasattr(request, "user") and hasattr(request.user, "person"): return request.user.person + + +def annotate_documentations( + klass: Union[Event, LessonPeriod, ExtraLesson], wanted_week: CalendarWeek, pks: List[int] +) -> QuerySet: + """Return an annotated queryset of all provided register objects.""" + if isinstance(klass, LessonPeriod): + prefetch = Prefetch( + "documentations", + queryset=LessonDocumentation.objects.filter( + week=wanted_week.week, year=wanted_week.year + ), + ) + else: + prefetch = Prefetch("documentations") + instances = klass.objects.prefetch_related(prefetch).filter(pk__in=pks) + + if klass == LessonPeriod: + instances = instances.annotate_week(wanted_week) + elif klass in (LessonPeriod, ExtraLesson): + instances = instances.order_by("period__weekday", "period__period") + else: + instances = instances.order_by("period_from__weekday", "period_from__period") + + instances = instances.annotate( + has_documentation=Exists( + LessonDocumentation.objects.filter( + ~Q(topic__exact=""), week=wanted_week.week, year=wanted_week.year, + ).filter(**{klass.label_: OuterRef("pk")}) + ) + ) + + return instances + + +def register_objects_sorter(register_object: Union[LessonPeriod, Event, ExtraLesson]) -> int: + """Sort key for sorted/sort for sorting a list of class register objects. + + This will sort the objects by the start period. + """ + if hasattr(register_object, "period"): + return register_object.period.period + elif isinstance(register_object, Event): + return register_object.period_from_on_day + else: + return 0 diff --git a/aleksis/apps/alsijil/util/predicates.py b/aleksis/apps/alsijil/util/predicates.py index e039a3b4232a67db3069cbbe05c79538fd22c027..27bdc4b9798c304b0ad20981bc918449f800db2b 100644 --- a/aleksis/apps/alsijil/util/predicates.py +++ b/aleksis/apps/alsijil/util/predicates.py @@ -5,7 +5,7 @@ from django.contrib.auth.models import Permission, User from guardian.models import UserObjectPermission from rules import predicate -from aleksis.apps.chronos.models import LessonPeriod +from aleksis.apps.chronos.models import Event, ExtraLesson, LessonPeriod from aleksis.core.models import Group, Person from aleksis.core.util.core_helpers import get_content_type_by_perm @@ -19,17 +19,17 @@ def is_none(user: User, obj: Any) -> bool: @predicate -def is_lesson_teacher(user: User, obj: LessonPeriod) -> bool: +def is_lesson_teacher(user: User, obj: Union[LessonPeriod, Event, ExtraLesson]) -> bool: """Predicate for teachers of a lesson. Checks whether the person linked to the user is a teacher in the lesson or the substitution linked to the given LessonPeriod. """ if obj: - sub = obj.get_substitution() + sub = obj.get_substitution() if isinstance(obj, LessonPeriod) else None if sub and sub in user.person.lesson_substitutions.all(): return True - return user.person in obj.lesson.teachers.all() + return user.person in obj.get_teachers().all() return False @@ -40,8 +40,8 @@ def is_lesson_participant(user: User, obj: LessonPeriod) -> bool: Checks whether the person linked to the user is a member in the groups linked to the given LessonPeriod. """ - if hasattr(obj, "lesson"): - for group in obj.lesson.groups.all(): + if hasattr(obj, "lesson") or hasattr(obj, "groups"): + for group in obj.get_groups().all(): if user.person in list(group.members.all()): return True return False @@ -55,8 +55,8 @@ def is_lesson_parent_group_owner(user: User, obj: LessonPeriod) -> bool: Checks whether the person linked to the user is the owner of any parent groups of any groups of the given LessonPeriods lesson. """ - if hasattr(obj, "lesson"): - for group in obj.lesson.groups.all(): + if hasattr(obj, "lesson") or hasattr(obj, "groups"): + for group in obj.get_groups().all(): for parent_group in group.parent_groups.all(): if user.person in list(parent_group.owners.all()): return True @@ -211,15 +211,15 @@ def is_personal_note_lesson_teacher(user: User, obj: PersonalNote) -> bool: Checks whether the person linked to the user is a teacher in the lesson or the substitution linked to the LessonPeriod of the given PersonalNote. """ - if hasattr(obj, "lesson_period"): - if hasattr(obj.lesson_period, "lesson"): + if hasattr(obj, "register_object"): + if getattr(obj, "lesson_period", None): sub = obj.lesson_period.get_substitution() if sub and user.person in Person.objects.filter( lesson_substitutions=obj.lesson_period.get_substitution() ): return True - return user.person in obj.lesson_period.lesson.teachers.all() + return user.person in obj.register_object.get_teachers().all() return False return False @@ -233,12 +233,11 @@ def is_personal_note_lesson_parent_group_owner(user: User, obj: PersonalNote) -> Checks whether the person linked to the user is the owner of any parent groups of any groups of the given LessonPeriod lesson of the given PersonalNote. """ - if hasattr(obj, "lesson_period"): - if hasattr(obj.lesson_period, "lesson"): - for group in obj.lesson_period.lesson.groups.all(): - for parent_group in group.parent_groups.all(): - if user.person in list(parent_group.owners.all()): - return True + if hasattr(obj, "register_object"): + for group in obj.register_object.get_groups().all(): + for parent_group in group.parent_groups.all(): + if user.person in list(parent_group.owners.all()): + return True return False diff --git a/aleksis/apps/alsijil/views.py b/aleksis/apps/alsijil/views.py index 91199a3d6eb9a49d8cfd4cf477103f83ecfc2f7a..b2378f891f579b328c9c51114a7d212adba5d1ec 100644 --- a/aleksis/apps/alsijil/views.py +++ b/aleksis/apps/alsijil/views.py @@ -1,9 +1,12 @@ from contextlib import nullcontext +from copy import deepcopy from datetime import date, datetime, timedelta from typing import Any, Dict, Optional from django.core.exceptions import PermissionDenied from django.db.models import Count, Exists, OuterRef, Prefetch, Q, Subquery, Sum +from django.db.models.expressions import Case, When +from django.db.models.functions import Extract from django.http import Http404, HttpRequest, HttpResponse, HttpResponseNotFound from django.shortcuts import get_object_or_404, redirect, render from django.urls import reverse, reverse_lazy @@ -20,7 +23,7 @@ from reversion.views import RevisionMixin from rules.contrib.views import PermissionRequiredMixin, permission_required from aleksis.apps.chronos.managers import TimetableType -from aleksis.apps.chronos.models import Holiday, LessonPeriod, TimePeriod +from aleksis.apps.chronos.models import Event, ExtraLesson, Holiday, LessonPeriod, TimePeriod from aleksis.apps.chronos.util.build import build_weekdays from aleksis.apps.chronos.util.date import get_weeks_for_year, week_weekday_to_date from aleksis.core.mixins import ( @@ -53,33 +56,41 @@ from .models import ( PersonalNote, ) from .tables import ExcuseTypeTable, ExtraMarkTable, GroupRoleTable -from .util.alsijil_helpers import get_lesson_period_by_pk, get_timetable_instance_by_pk +from .util.alsijil_helpers import ( + annotate_documentations, + get_register_object_by_pk, + get_timetable_instance_by_pk, + register_objects_sorter, +) -@permission_required("alsijil.view_lesson", fn=get_lesson_period_by_pk) -def lesson( +@permission_required("alsijil.view_register_object", fn=get_register_object_by_pk) # FIXME +def register_object( request: HttpRequest, + model: Optional[str] = None, year: Optional[int] = None, week: Optional[int] = None, - period_id: Optional[int] = None, + id_: Optional[int] = None, ) -> HttpResponse: context = {} - lesson_period = get_lesson_period_by_pk(request, year, week, period_id) + register_object = get_register_object_by_pk(request, model, year, week, id_) - if period_id: + if id_ and model == "lesson": wanted_week = CalendarWeek(year=year, week=week) + elif id_ and model == "extra_lesson": + wanted_week = register_object.calendar_week elif hasattr(request, "user") and hasattr(request.user, "person"): wanted_week = CalendarWeek() else: wanted_week = None - if not all((year, week, period_id)): - if lesson_period: + if not all((year, week, id_)): + if register_object and model == "lesson": return redirect( - "lesson_by_week_and_period", wanted_week.year, wanted_week.week, lesson_period.pk, + "lesson_period", wanted_week.year, wanted_week.week, register_object.pk, ) - else: + elif not register_object: raise Http404( _( "You either selected an invalid lesson or " @@ -87,22 +98,30 @@ def lesson( ) ) - date_of_lesson = week_weekday_to_date(wanted_week, lesson_period.period.weekday) + date_of_lesson = ( + week_weekday_to_date(wanted_week, register_object.period.weekday) + if not isinstance(register_object, Event) + else register_object.date_start + ) + start_time = ( + register_object.period.time_start + if not isinstance(register_object, Event) + else register_object.period_from.time_start + ) - if ( - date_of_lesson < lesson_period.lesson.validity.date_start - or date_of_lesson > lesson_period.lesson.validity.date_end + if isinstance(register_object, Event): + register_object.annotate_day(date_of_lesson) + if isinstance(register_object, LessonPeriod) and ( + date_of_lesson < register_object.lesson.validity.date_start + or date_of_lesson > register_object.lesson.validity.date_end ): return HttpResponseNotFound() if ( - datetime.combine( - wanted_week[lesson_period.period.weekday], lesson_period.period.time_start, - ) - > datetime.now() + datetime.combine(date_of_lesson, start_time) > datetime.now() and not ( get_site_preferences()["alsijil__open_periods_same_day"] - and wanted_week[lesson_period.period.weekday] <= datetime.now().date() + and date_of_lesson <= datetime.now().date() ) and not request.user.is_superuser ): @@ -117,45 +136,56 @@ def lesson( context["blocked_because_holidays"] = blocked_because_holidays context["holiday"] = holiday + next_lesson = ( + request.user.person.next_lesson(register_object, date_of_lesson) + if isinstance(register_object, LessonPeriod) + else None + ) + prev_lesson = ( + request.user.person.previous_lesson(register_object, date_of_lesson) + if isinstance(register_object, LessonPeriod) + else None + ) back_url = reverse( - "lesson_by_week_and_period", args=[wanted_week.year, wanted_week.week, lesson_period.pk] + "lesson_period", args=[wanted_week.year, wanted_week.week, register_object.pk] ) context["back_url"] = back_url - next_lesson = request.user.person.next_lesson(lesson_period, date_of_lesson) - prev_lesson = request.user.person.previous_lesson(lesson_period, date_of_lesson) - - context["lesson_period"] = lesson_period + context["register_object"] = register_object context["week"] = wanted_week - context["day"] = wanted_week[lesson_period.period.weekday] + context["day"] = date_of_lesson context["next_lesson_person"] = next_lesson context["prev_lesson_person"] = prev_lesson - context["prev_lesson"] = lesson_period.prev - context["next_lesson"] = lesson_period.next + context["prev_lesson"] = ( + register_object.prev if isinstance(register_object, LessonPeriod) else None + ) + context["next_lesson"] = ( + register_object.next if isinstance(register_object, LessonPeriod) else None + ) if not blocked_because_holidays: # Group roles show_group_roles = request.user.person.preferences[ "alsijil__group_roles_in_lesson_view" - ] and request.user.has_perm("alsijil.view_assigned_grouproles", lesson_period) + ] and request.user.has_perm("alsijil.view_assigned_grouproles", register_object) if show_group_roles: - groups = lesson_period.lesson.groups.all() + groups = register_object.get_groups().all() group_roles = GroupRole.objects.with_assignments(date_of_lesson, groups) context["group_roles"] = group_roles # Create or get lesson documentation object; can be empty when first opening lesson - lesson_documentation = lesson_period.get_or_create_lesson_documentation(wanted_week) + lesson_documentation = register_object.get_or_create_lesson_documentation(wanted_week) lesson_documentation_form = LessonDocumentationForm( request.POST or None, instance=lesson_documentation, prefix="lesson_documentation", ) # Create a formset that holds all personal notes for all persons in this lesson - if not request.user.has_perm("alsijil.view_lesson_personalnote", lesson_period): + if not request.user.has_perm("alsijil.view_register_object_personalnote", register_object): persons = Person.objects.filter(pk=request.user.person.pk) else: persons = Person.objects.all() - persons_qs = lesson_period.get_personal_notes(persons, wanted_week) + persons_qs = register_object.get_personal_notes(persons, wanted_week) # Annotate group roles if show_group_roles: @@ -172,7 +202,7 @@ def lesson( if request.method == "POST": if lesson_documentation_form.is_valid() and request.user.has_perm( - "alsijil.edit_lessondocumentation", lesson_period + "alsijil.edit_lessondocumentation", register_object ): with reversion.create_revision(): reversion.set_user(request.user) @@ -180,19 +210,25 @@ def lesson( messages.success(request, _("The lesson documentation has been saved.")) - substitution = lesson_period.get_substitution() + substitution = ( + register_object.get_substitution() + if isinstance(register_object, LessonPeriod) + else None + ) if ( not getattr(substitution, "cancelled", False) or not get_site_preferences()["alsijil__block_personal_notes_for_cancelled"] ): if personal_note_formset.is_valid() and request.user.has_perm( - "alsijil.edit_lesson_personalnote", lesson_period + "alsijil.edit_register_object_personalnote", register_object ): with reversion.create_revision(): reversion.set_user(request.user) instances = personal_note_formset.save() - if get_site_preferences()["alsijil__carry_over_personal_notes"]: + if (not isinstance(register_object, Event)) and get_site_preferences()[ + "alsijil__carry_over_personal_notes" + ]: # Iterate over personal notes # and carry changed absences to following lessons with reversion.create_revision(): @@ -243,8 +279,10 @@ def week_view( "lesson__groups__parent_groups", "lesson__groups__parent_groups__owners", ) + events = Event.objects.in_week(wanted_week) + extra_lessons = ExtraLesson.objects.in_week(wanted_week) - lesson_periods_query_exists = True + query_exists = True if type_ and id_: if isinstance(instance, HttpResponseNotFound): return HttpResponseNotFound() @@ -252,15 +290,26 @@ def week_view( type_ = TimetableType.from_string(type_) lesson_periods = lesson_periods.filter_from_type(type_, instance) + events = events.filter_from_type(type_, instance) + extra_lessons = extra_lessons.filter_from_Type(type_, instance) + elif hasattr(request, "user") and hasattr(request.user, "person"): if request.user.person.lessons_as_teacher.exists(): lesson_periods = lesson_periods.filter_teacher(request.user.person) + events = events.filter_teacher(request.user.person) + extra_lessons = extra_lessons.filter_teacher(request.user.person) + type_ = TimetableType.TEACHER else: lesson_periods = lesson_periods.filter_participant(request.user.person) + events = events.filter_participant(request.user.person) + extra_lessons = extra_lessons.filter_participant(request.user.person) + else: - lesson_periods_query_exists = False + query_exists = False lesson_periods = None + events = None + extra_lessons = None # Add a form to filter the view if type_: @@ -304,35 +353,21 @@ def week_view( extra_marks = ExtraMark.objects.all() - if lesson_periods_query_exists: + if query_exists: lesson_periods_pk = list(lesson_periods.values_list("pk", flat=True)) - lesson_periods = ( - LessonPeriod.objects.prefetch_related( - Prefetch( - "documentations", - queryset=LessonDocumentation.objects.filter( - week=wanted_week.week, year=wanted_week.year - ), - ) - ) - .filter(pk__in=lesson_periods_pk) - .annotate_week(wanted_week) - .annotate( - has_documentation=Exists( - LessonDocumentation.objects.filter( - ~Q(topic__exact=""), - lesson_period=OuterRef("pk"), - week=wanted_week.week, - year=wanted_week.year, - ) - ) - ) - .order_by("period__weekday", "period__period") - ) + lesson_periods = annotate_documentations(LessonPeriod, wanted_week, lesson_periods_pk) + + events_pk = list(events.values_list("pk", flat=True)) + events = annotate_documentations(Event, wanted_week, events_pk) + + extra_lessons_pk = list(extra_lessons.values_list("pk", flat=True)) + extra_lessons = annotate_documentations(ExtraLesson, wanted_week, extra_lessons_pk) else: lesson_periods_pk = [] + events_pk = [] + extra_lessons_pk = [] - if lesson_periods_pk: + if lesson_periods_pk or events_pk or extra_lessons_pk: # Aggregate all personal notes for this group and week persons_qs = Person.objects.filter(is_active=True) @@ -341,15 +376,49 @@ def week_view( elif group: persons_qs = persons_qs.filter(member_of=group) else: - persons_qs = persons_qs.filter(member_of__lessons__lesson_periods__in=lesson_periods_pk) + persons_qs = persons_qs.filter( + Q(member_of__lessons__lesson_periods__in=lesson_periods_pk) + | Q(member_of__events__in=events_pk) + | Q(member_of__extra_lessons__in=extra_lessons_pk) + ) + + personal_notes_q = ( + Q( + personal_notes__week=wanted_week.week, + personal_notes__year=wanted_week.year, + personal_notes__lesson_period__in=lesson_periods_pk, + ) + | Q( + personal_notes__event__date_start__lte=wanted_week[6], + personal_notes__event__date_end__gte=wanted_week[0], + personal_notes__event__in=events_pk, + ) + | Q( + personal_notes__extra_lesson__week=wanted_week.week, + personal_notes__extra_lesson__year=wanted_week.year, + personal_notes__extra_lesson__in=extra_lessons_pk, + ) + ) persons_qs = persons_qs.distinct().prefetch_related( Prefetch( "personal_notes", queryset=PersonalNote.objects.filter( - week=wanted_week.week, - year=wanted_week.year, - lesson_period__in=lesson_periods_pk, + Q( + week=wanted_week.week, + year=wanted_week.year, + lesson_period__in=lesson_periods_pk, + ) + | Q( + event__date_start__lte=wanted_week[6], + event__date_end__gte=wanted_week[0], + event__in=events_pk, + ) + | Q( + extra_lesson__week=wanted_week.week, + extra_lesson__year=wanted_week.year, + extra_lesson__in=extra_lessons_pk, + ) ), ), "member_of__owners", @@ -363,48 +432,28 @@ def week_view( queryset=GroupRoleAssignment.objects.in_week(wanted_week).for_group(group), ), ) - persons_qs = persons_qs.annotate( absences_count=Count( "personal_notes", - filter=Q( - personal_notes__lesson_period__in=lesson_periods_pk, - personal_notes__week=wanted_week.week, - personal_notes__year=wanted_week.year, - personal_notes__absent=True, - ), + filter=personal_notes_q & Q(personal_notes__absent=True,), distinct=True, ), unexcused_count=Count( "personal_notes", - filter=Q( - personal_notes__lesson_period__in=lesson_periods_pk, - personal_notes__week=wanted_week.week, - personal_notes__year=wanted_week.year, - personal_notes__absent=True, - personal_notes__excused=False, - ), + filter=personal_notes_q + & Q(personal_notes__absent=True, personal_notes__excused=False,), distinct=True, ), tardiness_sum=Subquery( - Person.objects.filter( - pk=OuterRef("pk"), - personal_notes__lesson_period__in=lesson_periods_pk, - personal_notes__week=wanted_week.week, - personal_notes__year=wanted_week.year, - ) + Person.objects.filter(personal_notes_q) + .filter(pk=OuterRef("pk"),) .distinct() .annotate(tardiness_sum=Sum("personal_notes__late")) .values("tardiness_sum") ), tardiness_count=Count( "personal_notes", - filter=Q( - personal_notes__lesson_period__in=lesson_periods_pk, - personal_notes__week=wanted_week.week, - personal_notes__year=wanted_week.year, - ) - & ~Q(personal_notes__late=0), + filter=personal_notes_q & ~Q(personal_notes__late=0), distinct=True, ), ) @@ -414,12 +463,7 @@ def week_view( **{ extra_mark.count_label: Count( "personal_notes", - filter=Q( - personal_notes__lesson_period__in=lesson_periods_pk, - personal_notes__week=wanted_week.week, - personal_notes__year=wanted_week.year, - personal_notes__extra_marks=extra_mark, - ), + filter=personal_notes_q & Q(personal_notes__extra_marks=extra_mark,), distinct=True, ) } @@ -427,20 +471,53 @@ def week_view( persons = [] for person in persons_qs: - persons.append({"person": person, "personal_notes": list(person.personal_notes.all())}) + personal_notes = [] + for note in person.personal_notes.all(): + if note.lesson_period: + note.lesson_period.annotate_week(wanted_week) + personal_notes.append(note) + persons.append({"person": person, "personal_notes": personal_notes}) else: persons = None context["extra_marks"] = extra_marks context["week"] = wanted_week context["weeks"] = get_weeks_for_year(year=wanted_week.year) + context["lesson_periods"] = lesson_periods + context["events"] = events + context["extra_lessons"] = extra_lessons + context["persons"] = persons context["group"] = group context["select_form"] = select_form context["instance"] = instance context["weekdays"] = build_weekdays(TimePeriod.WEEKDAY_CHOICES, wanted_week) + regrouped_objects = {} + + for register_object in list(lesson_periods) + list(extra_lessons): + regrouped_objects.setdefault(register_object.period.weekday, []) + regrouped_objects[register_object.period.weekday].append(register_object) + + for event in events: + weekday_from = event.get_start_weekday(wanted_week) + weekday_to = event.get_end_weekday(wanted_week) + + for weekday in range(weekday_from, weekday_to + 1): + # Make a copy in order to keep the annotation only on this weekday + event_copy = deepcopy(event) + event_copy.annotate_day(wanted_week[weekday]) + + regrouped_objects.setdefault(weekday, []) + regrouped_objects[weekday].append(event_copy) + + # Sort register objects + for weekday in regrouped_objects.keys(): + to_sort = regrouped_objects[weekday] + regrouped_objects[weekday] = sorted(to_sort, key=register_objects_sorter) + context["regrouped_objects"] = regrouped_objects + week_prev = wanted_week - 1 week_next = wanted_week + 1 args_prev = [week_prev.year, week_prev.week] @@ -467,33 +544,61 @@ 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.select_related("lesson_period") .prefetch_related( "lesson_period__substitutions", "lesson_period__lesson__teachers", "groups_of_person" ) .not_empty() - .filter( - Q(lesson_period__lesson__groups=group) - | Q(lesson_period__lesson__groups__parent_groups=group) - ) + .filter(groups_q) ) documentations = ( - LessonDocumentation.objects.select_related("lesson_period") - .not_empty() - .filter( - Q(lesson_period__lesson__groups=group) - | Q(lesson_period__lesson__groups__parent_groups=group) - ) + LessonDocumentation.objects.select_related("lesson_period").not_empty().filter(groups_q) ) # 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, + list(filter(lambda d: d.extra_lesson == extra_lesson, documentations)), + list(filter(lambda d: d.extra_lesson == extra_lesson, personal_notes)), + 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, + list(filter(lambda d: d.event == event, documentations)), + list(filter(lambda d: d.event == event, personal_notes)), + None, + ) + ) weeks = CalendarWeek.weeks_within(group.school_term.date_start, group.school_term.date_end,) - periods_by_day = {} for lesson_period in lesson_periods: for week in weeks: day = week[lesson_period.period.weekday] @@ -521,7 +626,7 @@ def full_register_group(request: HttpRequest, id_: int) -> HttpResponse: ) substitution = lesson_period.get_substitution(week) - periods_by_day.setdefault(day, []).append( + register_objects_by_day.setdefault(day, []).append( (lesson_period, filtered_documentations, filtered_personal_notes, substitution) ) @@ -539,8 +644,8 @@ def full_register_group(request: HttpRequest, id_: int) -> HttpResponse: context["extra_marks"] = ExtraMark.objects.all() context["group"] = group context["weeks"] = weeks - context["periods_by_day"] = periods_by_day - context["lesson_periods"] = lesson_periods + 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() @@ -639,13 +744,17 @@ def overview_person(request: HttpRequest, id_: Optional[int] = None) -> HttpResp ): raise PermissionDenied() - notes = person.personal_notes.filter( - week=date.isocalendar()[1], - lesson_period__period__weekday=date.weekday(), - lesson_period__lesson__validity__date_start__lte=date, - lesson_period__lesson__validity__date_end__gte=date, - absent=True, - excused=False, + notes = person.personal_notes.filter(absent=True, excused=False,).filter( + Q( + week=date.isocalendar()[1], + lesson_period__period__weekday=date.weekday(), + lesson_period__lesson__validity__date_start__lte=date, + lesson_period__lesson__validity__date_end__gte=date, + ) + | Q( + extra_lesson__week=date.isocalendar()[1], + extra_lesson__period__weekday=date.weekday(), + ) ) for note in notes: note.excused = True @@ -685,17 +794,50 @@ def overview_person(request: HttpRequest, id_: Optional[int] = None) -> HttpResp allowed_personal_notes = person_personal_notes.all() else: allowed_personal_notes = person_personal_notes.filter( - lesson_period__lesson__groups__owners=request.user.person + Q(lesson_period__lesson__groups__owners=request.user.person) + | Q(extra_lesson__groups__owners=request.user.person) + | Q(event__groups__owners=request.user.person) ) unexcused_absences = allowed_personal_notes.filter(absent=True, excused=False) context["unexcused_absences"] = unexcused_absences - personal_notes = allowed_personal_notes.not_empty().order_by( - "-lesson_period__lesson__validity__date_start", - "-week", - "lesson_period__period__weekday", - "lesson_period__period__period", + personal_notes = ( + allowed_personal_notes.not_empty() + .filter(Q(absent=True) | Q(late__gt=0) | ~Q(remarks="") | Q(extra_marks__isnull=False)) + .annotate( + school_term_start=Case( + When(event__isnull=False, then="event__school_term__date_start"), + When(extra_lesson__isnull=False, then="extra_lesson__school_term__date_start"), + When( + lesson_period__isnull=False, + then="lesson_period__lesson__validity__school_term__date_start", + ), + ), + order_year=Case( + When(event__isnull=False, then=Extract("event__date_start", "year")), + When(extra_lesson__isnull=False, then="extra_lesson__year"), + When(lesson_period__isnull=False, then="year"), + ), + order_week=Case( + When(event__isnull=False, then=Extract("event__date_start", "week")), + When(extra_lesson__isnull=False, then="extra_lesson__week"), + When(lesson_period__isnull=False, then="week"), + ), + order_weekday=Case( + When(event__isnull=False, then="event__period_from__weekday"), + When(extra_lesson__isnull=False, then="extra_lesson__period__weekday"), + When(lesson_period__isnull=False, then="lesson_period__period__weekday"), + ), + order_period=Case( + When(event__isnull=False, then="event__period_from__period"), + When(extra_lesson__isnull=False, then="extra_lesson__period__period"), + When(lesson_period__isnull=False, then="lesson_period__period__period"), + ), + ) + .order_by( + "-school_term_start", "-order_year", "-order_week", "-order_weekday", "order_period", + ) ) context["personal_notes"] = personal_notes context["excuse_types"] = ExcuseType.objects.all() @@ -707,8 +849,10 @@ def overview_person(request: HttpRequest, id_: Optional[int] = None) -> HttpResp stats = [] for school_term in school_terms: stat = {} - personal_notes = PersonalNote.objects.filter( - person=person, lesson_period__lesson__validity__school_term=school_term + personal_notes = PersonalNote.objects.filter(person=person,).filter( + Q(lesson_period__lesson__validity__school_term=school_term) + | Q(extra_lesson__school_term=school_term) + | Q(event__school_term=school_term) ) if not personal_notes.exists():