from datetime import date 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.utils.translation import gettext as _ from calendarweek import CalendarWeek from aleksis.apps.chronos.models import LessonPeriod from aleksis.core.models import Group, Person from .models import ExcuseType, ExtraMark, LessonDocumentation, PersonalNote @Person.method def mark_absent( self, day: date, from_period: int = 0, absent: bool = True, excused: bool = False, excuse_type: Optional[ExcuseType] = None, remarks: str = "", to_period: Optional[int] = None, dry_run: bool = False, ): """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. :param dry_run: With this activated, the function won't change any data and just return the count of affected lessons :return: Count of affected lesson periods ..note:: Only available when AlekSIS-App-Alsijil is installed. :Date: 2019-11-10 :Authors: - Dominik George <dominik.george@teckids.org> """ wanted_week = CalendarWeek.from_date(day) # Get all lessons of this person on the specified day lesson_periods = ( self.lesson_periods_as_participant.on_day(day) .filter(period__period__gte=from_period) .annotate_week(wanted_week) ) if to_period: lesson_periods = lesson_periods.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() if sub and sub.cancelled: continue personal_note, created = ( PersonalNote.objects.select_related(None) .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,}, ) ) personal_note.groups_of_person.set(self.member_of.all()) if remarks: if personal_note.remarks: personal_note.remarks += "; %s" % remarks else: personal_note.remarks = remarks personal_note.save() return lesson_periods.count() @LessonPeriod.method def get_personal_notes(self, persons: QuerySet, wanted_week: CalendarWeek): """Get all personal notes for that lesson in a specified week. Returns all linked `PersonalNote` objects, filtered by the given weeek, creating those objects that haven't been created yet. ..note:: Only available when AlekSIS-App-Alsijil is installed. :Date: 2019-11-09 :Authors: - Dominik George <dominik.george@teckids.org> """ # Find all persons in the associated groups that do not yet have a personal note for this lesson 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"), ) ) ).filter( member_of__in=Group.objects.filter(pk__in=self.lesson.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 ] 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, ) .select_related(None) .prefetch_related(None) .select_related("person", "excuse_type") .prefetch_related("extra_marks") ) # Dynamically add extra permissions to Group and Person models in core # Note: requires migrate afterwards Group.add_permission( "view_week_class_register_group", _("Can view week overview of group class register"), ) Group.add_permission( "view_lesson_class_register_group", _("Can view lesson overview of group class register"), ) Group.add_permission("view_personalnote_group", _("Can view all personal notes of a group")) Group.add_permission("edit_personalnote_group", _("Can edit all personal notes of a group")) Group.add_permission( "view_lessondocumentation_group", _("Can view all lesson documentation of a group") ) Group.add_permission( "edit_lessondocumentation_group", _("Can edit all lesson documentation of a group") ) Group.add_permission("view_full_register_group", _("Can view full register of a group")) Group.add_permission( "register_absence_group", _("Can register an absence for all members of a group") ) Person.add_permission("register_absence_person", _("Can register an absence for a person")) @LessonPeriod.method def get_lesson_documentation( self, week: Optional[CalendarWeek] = None ) -> Union[LessonDocumentation, None]: """Get lesson documentation object for this lesson.""" if not week: week = self.week # Use all to make effect of prefetched data doc_filter = filter( lambda d: d.week == week.week and d.year == week.year, self.documentations.all(), ) try: return next(doc_filter) except StopIteration: return None @LessonPeriod.method def get_or_create_lesson_documentation( self, week: Optional[CalendarWeek] = None ) -> LessonDocumentation: """Get or create lesson documentation object for this lesson.""" if not week: week = self.week lesson_documentation, created = LessonDocumentation.objects.get_or_create( lesson_period=self, week=week.week, year=week.year ) return lesson_documentation @LessonPeriod.method def get_absences(self, week: Optional[CalendarWeek] = None) -> Iterator: """Get all personal notes of absent persons for this lesson.""" if not week: week = self.week return filter( lambda p: p.week == week.week and p.year == week.year and p.absent, self.personal_notes.all(), ) @LessonPeriod.method def get_excused_absences(self, week: Optional[CalendarWeek] = None) -> QuerySet: """Get all personal notes of excused absent persons for this lesson.""" if not week: week = self.week return self.personal_notes.filter(week=week.week, year=week.year, absent=True, excused=True) @LessonPeriod.method def get_unexcused_absences(self, week: Optional[CalendarWeek] = None) -> QuerySet: """Get all personal notes of unexcused absent persons for this lesson.""" if not week: week = self.week return self.personal_notes.filter(week=week.week, year=week.year, absent=True, excused=False) @LessonPeriod.method def get_tardinesses(self, week: Optional[CalendarWeek] = None) -> QuerySet: """Get all personal notes of late persons for this lesson.""" if not week: week = self.week return self.personal_notes.filter(week=week.week, year=week.year, late__gt=0) @LessonPeriod.method def get_extra_marks(self, week: Optional[CalendarWeek] = None) -> Dict[ExtraMark, QuerySet]: """Get all statistics on extra marks for this lesson.""" if not week: week = self.week stats = {} for extra_mark in ExtraMark.objects.all(): qs = self.personal_notes.filter(week=week.week, year=week.year, extra_marks=extra_mark) if qs: stats[extra_mark] = qs return stats @Group.class_method def get_groups_with_lessons(cls: Group): """Get all groups which have related lessons or child groups with related lessons.""" group_pks = ( cls.objects.for_current_school_term_or_all() .annotate(lessons_count=Count("lessons")) .filter(lessons_count__gt=0) .values_list("pk", flat=True) ) groups = cls.objects.filter(Q(child_groups__pk__in=group_pks) | Q(pk__in=group_pks)).distinct() return groups @Person.method def get_owner_groups_with_lessons(self: Person): """Get all groups the person is an owner of and which have related lessons. Groups which have child groups with related lessons are also included. """ return Group.get_groups_with_lessons().filter(owners=self).distinct() @Group.method def generate_person_list_with_class_register_statistics( self: Group, persons: Optional[Iterable] = None ) -> QuerySet: """Get with class register statistics annotated list of all members.""" 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() 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) ), ), excused=Count( "personal_notes__absent", 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) ), ), 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) ), ), 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_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) ), ), ) for extra_mark in ExtraMark.objects.all(): persons = persons.annotate( **{ 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) ), ) } ) for excuse_type in ExcuseType.objects.all(): persons = persons.annotate( **{ 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) ), ) } ) return persons