Skip to content
Snippets Groups Projects
model_extensions.py 8.83 KiB
from datetime import date
from typing import Dict, Iterator, Optional, Union

from django.db.models import Exists, OuterRef, Q, QuerySet
from django.db.models.aggregates import Count
from django.utils.translation import gettext as _

import reversion
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,
):
    """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.

    ..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
    for lesson_period in lesson_periods:
        sub = lesson_period.get_substitution()
        if sub and sub.cancelled:
            continue

        with reversion.create_revision():
            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()


@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.dopycumentations.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)