Skip to content
Snippets Groups Projects
views.py 57.6 KiB
Newer Older
from contextlib import nullcontext
from copy import deepcopy
Tom Teichler's avatar
Tom Teichler committed
from datetime import date, datetime, timedelta
from typing import Any, Dict, Optional
from django.apps import apps
Tom Teichler's avatar
Tom Teichler committed
from django.core.exceptions import PermissionDenied
from django.db.models import Count, Exists, FilteredRelation, OuterRef, Prefetch, Q, Sum
from django.db.models.expressions import Case, When
from django.db.models.functions import Extract
Jonathan Weth's avatar
Jonathan Weth committed
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
from django.utils import timezone
Hangzhi Yu's avatar
Hangzhi Yu committed
from django.utils.decorators import method_decorator
from django.utils.http import url_has_allowed_host_and_scheme
from django.utils.translation import ugettext as _
Hangzhi Yu's avatar
Hangzhi Yu committed
from django.views.decorators.cache import never_cache
Julian's avatar
Julian committed
from django.views.generic import DetailView, TemplateView
Nik | Klampfradler's avatar
Nik | Klampfradler committed

Tom Teichler's avatar
Tom Teichler committed
from calendarweek import CalendarWeek
from django_tables2 import RequestConfig, SingleTableView
from guardian.core import ObjectPermissionChecker
from guardian.shortcuts import get_objects_for_user
from reversion.views import RevisionMixin
from rules.contrib.views import PermissionRequiredMixin, permission_required
from aleksis.apps.chronos.managers import TimetableType
magicfelix's avatar
magicfelix committed
from aleksis.apps.chronos.models import (
    Event,
    ExtraLesson,
    Holiday,
    Lesson,
    LessonPeriod,
    TimePeriod,
)
from aleksis.apps.chronos.util.build import build_weekdays
Tom Teichler's avatar
Tom Teichler committed
from aleksis.apps.chronos.util.date import (
    get_current_year,
    get_weeks_for_year,
    week_weekday_to_date,
)
from aleksis.core.mixins import (
    AdvancedCreateView,
    AdvancedDeleteView,
    AdvancedEditView,
    SuccessNextMixin,
)
from aleksis.core.models import Group, Person, SchoolTerm
from aleksis.core.util import messages
Jonathan Weth's avatar
Jonathan Weth committed
from aleksis.core.util.core_helpers import get_site_preferences, objectgetter_optional
from aleksis.core.util.pdf import render_pdf
from aleksis.core.util.predicates import check_global_permission
from .filters import PersonalNoteFilter
from .forms import (
    AssignGroupRoleForm,
    ExcuseTypeForm,
    ExtraMarkForm,
    GroupRoleAssignmentEditForm,
    GroupRoleForm,
    LessonDocumentationForm,
    PersonalNoteFormSet,
Jonathan Weth's avatar
Jonathan Weth committed
    PersonOverviewForm,
    RegisterAbsenceForm,
from .models import (
    ExcuseType,
    ExtraMark,
    GroupRole,
    GroupRoleAssignment,
    LessonDocumentation,
    PersonalNote,
)
from .tables import (
    ExcuseTypeTable,
    ExtraMarkTable,
    GroupRoleTable,
    PersonalNoteTable,
    RegisterObjectSelectTable,
    RegisterObjectTable,
)
from .util.alsijil_helpers import (
    annotate_documentations,
    generate_list_of_all_register_objects,
    get_register_object_by_pk,
    get_timetable_instance_by_pk,
    register_objects_sorter,
)
Nik | Klampfradler's avatar
Nik | Klampfradler committed

Hangzhi Yu's avatar
Hangzhi Yu committed
@permission_required("alsijil.view_register_object_rule", fn=get_register_object_by_pk)  # FIXME
def register_object(
    request: HttpRequest,
    model: Optional[str] = None,
    year: Optional[int] = None,
    week: Optional[int] = None,
    id_: Optional[int] = None,
) -> HttpResponse:
    register_object = get_register_object_by_pk(request, model, year, week, 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, id_)):
        if register_object and model == "lesson":
                "lesson_period",
                wanted_week.year,
                wanted_week.week,
                register_object.pk,
        elif not register_object:
            raise Http404(
                _(
                    "You either selected an invalid lesson or "
                    "there is currently no lesson in progress."
    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 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
        datetime.combine(date_of_lesson, start_time) > datetime.now()
        and not (
            get_site_preferences()["alsijil__open_periods_same_day"]
            and date_of_lesson <= datetime.now().date()
        and not request.user.is_superuser
    ):
        raise PermissionDenied(
            _("You are not allowed to create a lesson documentation for a lesson in the future.")
    holiday = Holiday.on_day(date_of_lesson)
    blocked_because_holidays = (
        holiday is not None and not get_site_preferences()["alsijil__allow_entries_in_holidays"]
    )
    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_period", args=[wanted_week.year, wanted_week.week, register_object.pk]
    )
    context["back_url"] = back_url
    context["register_object"] = register_object
    context["week"] = wanted_week
    context["day"] = date_of_lesson
    context["next_lesson_person"] = next_lesson
    context["prev_lesson_person"] = prev_lesson
    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:
        groups = register_object.get_groups().all()
        if groups:
            first_group = groups.first()
            context["first_group"] = first_group

        # Group roles
        show_group_roles = request.user.person.preferences[
            "alsijil__group_roles_in_lesson_view"
Hangzhi Yu's avatar
Hangzhi Yu committed
            "alsijil.view_assigned_grouproles_for_register_object_rule", register_object
        if show_group_roles:
            group_roles = GroupRole.objects.with_assignments(date_of_lesson, groups)
            context["group_roles"] = group_roles
        with_seating_plan = (
            apps.is_installed("aleksis.apps.stoelindeling")
            and groups
            and request.user.has_perm("stoelindeling.view_seatingplan_for_group_rule", first_group)
        )
        context["with_seating_plan"] = with_seating_plan

        if with_seating_plan:
            seating_plan = register_object.seating_plan
            context["seating_plan"] = register_object.seating_plan
            if seating_plan and seating_plan.group != first_group:
                context["seating_plan_parent"] = True

        # Create or get lesson documentation object; can be empty when first opening lesson
        lesson_documentation = register_object.get_or_create_lesson_documentation(wanted_week)
        context["has_documentation"] = bool(lesson_documentation.topic)

        lesson_documentation_form = LessonDocumentationForm(
            request.POST or None,
            instance=lesson_documentation,
            prefix="lesson_documentation",
        # Prefetch object permissions for all related groups of the register object
        # because the object permissions are checked for all groups of the register object
        # That has to be set as an attribute of the register object,
        # so that the permission system can use the prefetched data.
        checker = ObjectPermissionChecker(request.user)
        checker.prefetch_perms(register_object.get_groups().all())
        register_object.set_object_permission_checker(checker)

        # Create a formset that holds all personal notes for all persons in this lesson
Hangzhi Yu's avatar
Hangzhi Yu committed
        if not request.user.has_perm(
            "alsijil.view_register_object_personalnote_rule", register_object
        ):
            persons = Person.objects.filter(pk=request.user.person.pk)
        else:
            persons = Person.objects.all()
        persons_qs = register_object.get_personal_notes(persons, wanted_week).filter(
            person__member_of__in=request.user.person.owner_of.all()
        )

        # Annotate group roles
        if show_group_roles:
            persons_qs = persons_qs.prefetch_related(
                Prefetch(
                    "person__group_roles",
                    queryset=GroupRoleAssignment.objects.on_day(date_of_lesson).for_groups(groups),
                ),
            )

        personal_note_formset = PersonalNoteFormSet(
            request.POST or None, queryset=persons_qs, prefix="personal_notes"
        )

        if request.method == "POST":
            if lesson_documentation_form.is_valid() and request.user.has_perm(
Hangzhi Yu's avatar
Hangzhi Yu committed
                "alsijil.edit_lessondocumentation_rule", register_object
                with reversion.create_revision():
                    reversion.set_user(request.user)
                    lesson_documentation_form.save()
                messages.success(request, _("The lesson documentation has been saved."))
            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(
Hangzhi Yu's avatar
Hangzhi Yu committed
                    "alsijil.edit_register_object_personalnote_rule", register_object
                ):
                    with reversion.create_revision():
                        reversion.set_user(request.user)
                        instances = personal_note_formset.save()

                    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():
                            reversion.set_user(request.user)
                            for instance in instances:
                                instance.person.mark_absent(
                                    wanted_week[register_object.period.weekday],
                                    register_object.period.period + 1,
                                    instance.absent,
                                    instance.excused,
                                    instance.excuse_type,
                                )
                messages.success(request, _("The personal notes have been saved."))

                # Regenerate form here to ensure that programmatically
                # changed data will be shown correctly
                personal_note_formset = PersonalNoteFormSet(
                    None, queryset=persons_qs, prefix="personal_notes"
                )

        back_url = request.GET.get("back", "")
        back_url_is_safe = url_has_allowed_host_and_scheme(
            url=back_url,
            allowed_hosts={request.get_host()},
            require_https=request.is_secure(),
        )
        if back_url_is_safe:
            context["back_to_week_url"] = back_url
        elif register_object.get_groups().all():
            context["back_to_week_url"] = reverse(
                "week_view_by_week",
                args=[
                    lesson_documentation.calendar_week.year,
                    lesson_documentation.calendar_week.week,
                    "group",
                    register_object.get_groups().all()[0].pk,
                ],
            )
        context["lesson_documentation"] = lesson_documentation
        context["lesson_documentation_form"] = lesson_documentation_form
        context["personal_note_formset"] = personal_note_formset
    return render(request, "alsijil/class_register/lesson.html", context)
Hangzhi Yu's avatar
Hangzhi Yu committed
@permission_required("alsijil.view_week_rule", fn=get_timetable_instance_by_pk)
def week_view(
    request: HttpRequest,
    year: Optional[int] = None,
    week: Optional[int] = None,
    type_: Optional[str] = None,
    id_: Optional[int] = None,
) -> HttpResponse:
    if year and week:
        wanted_week = CalendarWeek(year=year, week=week)
    else:
        wanted_week = CalendarWeek()

    instance = get_timetable_instance_by_pk(request, year, week, type_, id_)
Jonathan Weth's avatar
Jonathan Weth committed
    lesson_periods = LessonPeriod.objects.in_week(wanted_week).prefetch_related(
        "lesson__groups__members",
        "lesson__groups__parent_groups",
        "lesson__groups__parent_groups__owners",
    )
    events = Event.objects.in_week(wanted_week)
    extra_lessons = ExtraLesson.objects.in_week(wanted_week)
Jonathan Weth's avatar
Jonathan Weth committed
    if type_ and id_:
        if isinstance(instance, HttpResponseNotFound):
            return HttpResponseNotFound()

        type_ = TimetableType.from_string(type_)

        lesson_periods = lesson_periods.filter_from_type(type_, instance)
        events = events.filter_from_type(type_, instance)
Jonathan Weth's avatar
Jonathan Weth committed
        extra_lessons = extra_lessons.filter_from_type(type_, instance)
    elif hasattr(request, "user") and hasattr(request.user, "person"):
Jonathan Weth's avatar
Jonathan Weth committed
        if request.user.person.lessons_as_teacher.exists():
            inherit_privileges_preference = get_site_preferences()[
                "alsijil__inherit_privileges_from_parent_group"
            ]
            lesson_periods = (
                lesson_periods.filter_teacher(request.user.person).union(
                    lesson_periods.filter_groups(request.user.person.owner_of.all())
                )
                if inherit_privileges_preference
                else lesson_periods.filter_teacher(request.user.person)
            )
            events = (
                events.filter_teacher(request.user.person).union(
                    events.filter_groups(request.user.person.owner_of.all())
                )
                if inherit_privileges_preference
                else events.filter_teacher(request.user.person)
            )
            extra_lessons = (
                extra_lessons.filter_teacher(request.user.person).union(
                    extra_lessons.filter_groups(request.user.person.owner_of.all())
                )
                if inherit_privileges_preference
                else extra_lessons.filter_teacher(request.user.person)
            )
Jonathan Weth's avatar
Jonathan Weth committed
            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)

Nik | Klampfradler's avatar
Nik | Klampfradler committed
    else:
Nik | Klampfradler's avatar
Nik | Klampfradler committed
        lesson_periods = None
        events = None
        extra_lessons = None
Jonathan Weth's avatar
Jonathan Weth committed
    # Add a form to filter the view
    if type_:
        initial = {type_.value: instance}
        back_url = reverse(
            "week_view_by_week", args=[wanted_week.year, wanted_week.week, type_.value, instance.pk]
        )
Jonathan Weth's avatar
Jonathan Weth committed
    else:
        initial = {}
        back_url = reverse("week_view_by_week", args=[wanted_week.year, wanted_week.week])
    context["back_url"] = back_url
    select_form = SelectForm(request, request.POST or None, initial=initial)
Jonathan Weth's avatar
Jonathan Weth committed

    if request.method == "POST":
        if select_form.is_valid():
            if "type_" not in select_form.cleaned_data:
                return redirect("week_view_by_week", wanted_week.year, wanted_week.week)
            else:
                return redirect(
                    "week_view_by_week",
                    wanted_week.year,
                    wanted_week.week,
                    select_form.cleaned_data["type_"].value,
                    select_form.cleaned_data["instance"].pk,
                )
    if type_ == TimetableType.GROUP:
        group = instance
    else:
        group = None

    # Group roles
    show_group_roles = (
        group
        and request.user.person.preferences["alsijil__group_roles_in_week_view"]
Hangzhi Yu's avatar
Hangzhi Yu committed
        and request.user.has_perm("alsijil.view_assigned_grouproles_rule", group)
    )
    if show_group_roles:
        group_roles = GroupRole.objects.with_assignments(wanted_week, [group])
        context["group_roles"] = group_roles
        group_roles_persons = GroupRoleAssignment.objects.in_week(wanted_week).for_group(group)
        lesson_periods_pk = list(lesson_periods.values_list("pk", flat=True))
        lesson_periods = annotate_documentations(LessonPeriod, wanted_week, lesson_periods_pk)

        events_pk = [event.pk for event in events]
        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)
        groups = Group.objects.filter(
            Q(lessons__lesson_periods__in=lesson_periods_pk)
            | Q(events__in=events_pk)
            | Q(extra_lessons__in=extra_lessons_pk)
        )
        events_pk = []
        extra_lessons_pk = []
    if lesson_periods_pk or events_pk or extra_lessons_pk:
        # Aggregate all personal notes for this group and week
Hangzhi Yu's avatar
Hangzhi Yu committed
        if not request.user.has_perm("alsijil.view_week_personalnote_rule", instance):
Jonathan Weth's avatar
Jonathan Weth committed
            persons_qs = persons_qs.filter(pk=request.user.person.pk)
            persons_qs = persons_qs.filter(member_of=group).filter(
                member_of__in=request.user.person.owner_of.all()
            )
            persons_qs = persons_qs.filter(member_of__in=groups).filter(
                member_of__in=request.user.person.owner_of.all()
            )
        # Prefetch object permissions for persons and groups the persons are members of
        # because the object permissions are checked for both persons and groups
        checker = ObjectPermissionChecker(request.user)
        checker.prefetch_perms(persons_qs.prefetch_related(None))
        checker.prefetch_perms(groups)

        prefetched_personal_notes = list(
            PersonalNote.objects.filter(  #
                Q(event__in=events_pk)
                | Q(
                    week=wanted_week.week,
                    year=wanted_week.year,
                    lesson_period__in=lesson_periods_pk,
                )
                | Q(extra_lesson__in=extra_lessons_pk)
            ).filter(~Q(remarks=""))
        )
            persons_qs.select_related("primary_group")
            .prefetch_related(
                Prefetch(
                    "primary_group__owners",
                    queryset=Person.objects.filter(pk=request.user.person.pk),
                    to_attr="owners_prefetched",
                ),
                Prefetch("member_of", queryset=groups, to_attr="member_of_prefetched"),
            )
            .annotate(
                filtered_personal_notes=FilteredRelation(
                    "personal_notes",
                    condition=(
                        Q(personal_notes__event__in=events_pk)
                        | Q(
                            personal_notes__week=wanted_week.week,
                            personal_notes__year=wanted_week.year,
                            personal_notes__lesson_period__in=lesson_periods_pk,
                        )
                        | Q(personal_notes__extra_lesson__in=extra_lessons_pk)
                    ),
                )
        )

        persons_qs = persons_qs.annotate(
            absences_count=Count(
                "filtered_personal_notes",
                filter=Q(filtered_personal_notes__absent=True),
            ),
            unexcused_count=Count(
                "filtered_personal_notes",
                filter=Q(
                    filtered_personal_notes__absent=True, filtered_personal_notes__excused=False
                ),
            tardiness_sum=Sum("filtered_personal_notes__late"),
            tardiness_count=Count(
                "filtered_personal_notes",
                filter=Q(filtered_personal_notes__late__gt=0),
                **{
                    extra_mark.count_label: Count(
                        "filtered_personal_notes",
                        filter=Q(filtered_personal_notes__extra_marks=extra_mark),
            for note in filter(lambda note: note.person_id == person.pk, prefetched_personal_notes):
                if note.lesson_period:
                    note.lesson_period.annotate_week(wanted_week)
                personal_notes.append(note)
            person.set_object_permission_checker(checker)
            person_dict = {"person": person, "personal_notes": personal_notes}
            if show_group_roles:
                person_dict["group_roles"] = filter(
                    lambda role: role.person_id == person.pk, group_roles_persons
                )
            persons.append(person_dict)
    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
Jonathan Weth's avatar
Jonathan Weth committed
    context["instance"] = instance
    context["weekdays"] = build_weekdays(TimePeriod.WEEKDAY_CHOICES, wanted_week)
    regrouped_objects = {}

    for register_object in list(lesson_periods) + list(extra_lessons):
        register_object.weekday = register_object.period.weekday
        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)
Jonathan Weth's avatar
Jonathan Weth committed
            event_copy.annotate_day(wanted_week[weekday])
            event_copy.weekday = 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]
    args_next = [week_next.year, week_next.week]
    args_dest = []
    if type_ and id_:
        args_prev += [type_.value, id_]
        args_next += [type_.value, id_]
        args_dest += [type_.value, id_]
    context["week_select"] = {
        "year": wanted_week.year,
        "dest": reverse("week_view_placeholders", args=args_dest),

    context["url_prev"] = reverse("week_view_by_week", args=args_prev)
    context["url_next"] = reverse("week_view_by_week", args=args_next)
    return render(request, "alsijil/class_register/week_view.html", context)
Hangzhi Yu's avatar
Hangzhi Yu committed
@permission_required(
    "alsijil.view_full_register_rule", fn=objectgetter_optional(Group, None, False)
)
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)
    )
        PersonalNote.objects.prefetch_related(
            "lesson_period__substitutions", "lesson_period__lesson__teachers"
    documentations = LessonDocumentation.objects.not_empty().filter(groups_q)

    sorted_documentations = {"extra_lesson": {}, "event": {}, "lesson_period": {}}
    sorted_personal_notes = {"extra_lesson": {}, "event": {}, "lesson_period": {}, "person": {}}
    for documentation in documentations:
        key = documentation.register_object.label_
        sorted_documentations[key][documentation.register_object_key] = documentation

    for note in personal_notes:
        key = note.register_object.label_
        sorted_personal_notes[key].setdefault(note.register_object_key, [])
        sorted_personal_notes[key][note.register_object_key].append(note)
        sorted_personal_notes["person"].setdefault(note.person.pk, [])
        sorted_personal_notes["person"][note.person.pk].append(note)
    # 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,
                sorted_documentations["extra_lesson"].get(extra_lesson.pk),
                sorted_personal_notes["extra_lesson"].get(extra_lesson.pk, []),
    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,
                    sorted_documentations["event"].get(event.pk),
                    sorted_personal_notes["event"].get(event.pk, []),
    weeks = CalendarWeek.weeks_within(
        group.school_term.date_start,
        group.school_term.date_end,
    )
    for lesson_period in lesson_periods:
        for week in weeks:
            day = week[lesson_period.period.weekday]
Jonathan Weth's avatar
Jonathan Weth committed
            if (
                lesson_period.lesson.validity.date_start
                <= day
                <= lesson_period.lesson.validity.date_end
            ):
                filtered_documentation = sorted_documentations["lesson_period"].get(
                    f"{lesson_period.pk}_{week.week}_{week.year}"
                filtered_personal_notes = sorted_personal_notes["lesson_period"].get(
                    f"{lesson_period.pk}_{week.week}_{week.year}", []
                substitution = lesson_period.get_substitution(week)
                register_objects_by_day.setdefault(day, []).append(
                    (lesson_period, filtered_documentation, filtered_personal_notes, substitution)
    persons = group.members.prefetch_related(None).select_related(None)
    persons = group.generate_person_list_with_class_register_statistics(persons)
    prefetched_persons = []
    for person in persons:
        person.filtered_notes = sorted_personal_notes["person"][person.pk]
    context["school_term"] = group.school_term
    context["excuse_types"] = ExcuseType.objects.filter(count_as_absent=True)
    context["excuse_types_not_absent"] = ExcuseType.objects.filter(count_as_absent=False)
    context["extra_marks"] = ExtraMark.objects.all()
    context["group"] = group
    context["weeks"] = weeks
    context["register_objects_by_day"] = register_objects_by_day
    context["register_objects"] = list(lesson_periods) + list(events) + list(extra_lessons)
    context["today"] = date.today()
        .select_related(None)
        .prefetch_related(None)
        .select_related("validity", "subject")
        .prefetch_related("teachers", "lesson_periods")
    )
    context["child_groups"] = (
        group.child_groups.all()
        .select_related(None)
        .prefetch_related(None)
        .prefetch_related(
            "lessons",
            "lessons__validity",
            "lessons__subject",
            "lessons__teachers",
            "lessons__lesson_periods",
        )
    return render_pdf(request, "alsijil/print/full_register.html", context)
Hangzhi Yu's avatar
Hangzhi Yu committed
@permission_required("alsijil.view_my_students_rule")
def my_students(request: HttpRequest) -> HttpResponse:
Jonathan Weth's avatar
Jonathan Weth committed
    context = {}
    relevant_groups = (
        request.user.person.get_owner_groups_with_lessons()
        .annotate(has_parents=Exists(Group.objects.filter(child_groups=OuterRef("pk"))))
        .order_by("has_parents", "name")
    # Prefetch object permissions for persons and groups the persons are members of
    # because the object permissions are checked for both persons and groups
    all_persons = Person.objects.filter(member_of__in=relevant_groups)
    checker = ObjectPermissionChecker(request.user)
    checker.prefetch_perms(relevant_groups)
    checker.prefetch_perms(all_persons)

    new_groups = []
    for group in relevant_groups:
        persons = group.generate_person_list_with_class_register_statistics(
            group.members.prefetch_related(
                "primary_group__owners",
                Prefetch("member_of", queryset=relevant_groups, to_attr="member_of_prefetched"),
            )
        ).filter(member_of__in=request.user.person.owner_of.all())
        persons_for_group = []
        for person in persons:
            person.set_object_permission_checker(checker)
            persons_for_group.append(person)
        new_groups.append((group, persons_for_group))
    context["excuse_types"] = ExcuseType.objects.filter(count_as_absent=True)
    context["excuse_types_not_absent"] = ExcuseType.objects.filter(count_as_absent=False)
    context["extra_marks"] = ExtraMark.objects.all()
    return render(request, "alsijil/class_register/persons.html", context)


@permission_required(
    "alsijil.view_my_groups_rule",
)
def my_groups(request: HttpRequest) -> HttpResponse:
    context = {}
    context["groups"] = request.user.person.get_owner_groups_with_lessons().annotate(
        students_count=Count("members", distinct=True)
    return render(request, "alsijil/class_register/groups.html", context)

class StudentsList(PermissionRequiredMixin, DetailView):
    model = Group
    template_name = "alsijil/class_register/students_list.html"
Hangzhi Yu's avatar
Hangzhi Yu committed
    permission_required = "alsijil.view_students_list_rule"

    def get_context_data(self, **kwargs):
        context = super().get_context_data(**kwargs)
        context["group"] = self.object
        context[
            "persons"
        ] = self.object.generate_person_list_with_class_register_statistics().filter(
            member_of__in=self.request.user.person.owner_of.all()
        )
        context["extra_marks"] = ExtraMark.objects.all()
        context["excuse_types"] = ExcuseType.objects.filter(count_as_absent=True)
        context["excuse_types_not_absent"] = ExcuseType.objects.filter(count_as_absent=False)
@permission_required(
Hangzhi Yu's avatar
Hangzhi Yu committed
    "alsijil.view_person_overview_rule",
    fn=objectgetter_optional(
        Person.objects.prefetch_related("member_of__owners"), "request.user.person", True
    ),
def overview_person(request: HttpRequest, id_: Optional[int] = None) -> HttpResponse:
    context = {}
    person = objectgetter_optional(
        Person.objects.prefetch_related("member_of__owners"),
        default="request.user.person",
        default_eval=True,
    )(request, id_)
Jonathan Weth's avatar
Jonathan Weth committed
    context["person"] = person

    person_personal_notes = (
        person.personal_notes.all()
        .prefetch_related(
            "lesson_period__lesson__groups",
            "lesson_period__lesson__teachers",
            "lesson_period__substitutions",
        )
        .annotate_date_range()
    # Prefetch object permissions for groups the person is a member of
    # because the object permissions are checked for all groups the person is a member of
    # That has to be set as an attribute of the register object,
    # so that the permission system can use the prefetched data.
    checker = ObjectPermissionChecker(request.user)
    checker.prefetch_perms(Group.objects.filter(members=person))
    person.set_object_permission_checker(checker)
Hangzhi Yu's avatar
Hangzhi Yu committed
    if request.user.has_perm("alsijil.view_person_overview_personalnote_rule", person):
        allowed_personal_notes = person_personal_notes.all()
        allowed_personal_notes = person_personal_notes.filter(
            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)
Jonathan Weth's avatar
Jonathan Weth committed
    context["unexcused_absences"] = unexcused_absences

        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"),
            ),
Julian's avatar
Julian committed
            order_groups=Case(
                When(event__isnull=False, then="event__groups"),
                When(extra_lesson__isnull=False, then="extra_lesson__groups"),
                When(lesson_period__isnull=False, then="lesson_period__lesson__groups"),
            ),
            order_teachers=Case(
                When(event__isnull=False, then="event__teachers"),
                When(extra_lesson__isnull=False, then="extra_lesson__teachers"),
                When(lesson_period__isnull=False, then="lesson_period__lesson__teachers"),
            ),
            "-school_term_start",
            "-order_year",
            "-order_week",
            "-order_weekday",
            "order_period",
Julian's avatar
Julian committed
        .annotate_date_range()
        .annotate_subject()
Jonathan Weth's avatar
Jonathan Weth committed
    )
Julian's avatar
Julian committed

    personal_note_filter_object = PersonalNoteFilter(request.GET, queryset=personal_notes)
    filtered_personal_notes = personal_note_filter_object.qs
    context["personal_note_filter_form"] = personal_note_filter_object.form
Julian's avatar
Julian committed

    used_filters = list(personal_note_filter_object.data.values())
Julian's avatar
Julian committed
    context["num_filters"] = (
        len(used_filters) - used_filters.count("") - used_filters.count("unknown")
    )
    personal_notes_list = []
    for note in personal_notes:
        note.set_object_permission_checker(checker)
        personal_notes_list.append(note)
    context["personal_notes"] = personal_notes_list
    context["excuse_types"] = ExcuseType.objects.filter(count_as_absent=True)
    context["excuse_types_not_absent"] = ExcuseType.objects.filter(count_as_absent=False)
Julian's avatar
Julian committed
    form = PersonOverviewForm(request, request.POST or None, queryset=allowed_personal_notes)
    if request.method == "POST" and request.user.has_perm(
        "alsijil.edit_person_overview_personalnote_rule", person
    ):
        if form.is_valid():
            with reversion.create_revision():
                reversion.set_user(request.user)
                form.execute()
Jonathan Weth's avatar
Jonathan Weth committed
            person.refresh_from_db()
Julian's avatar
Julian committed

    table = PersonalNoteTable(filtered_personal_notes)
    RequestConfig(request, paginate={"per_page": 20}).configure(table)
    context["personal_notes_table"] = table
    extra_marks = ExtraMark.objects.all()
    excuse_types = ExcuseType.objects.all()
Hangzhi Yu's avatar
Hangzhi Yu committed
    if request.user.has_perm("alsijil.view_person_statistics_personalnote_rule", person):
        school_terms = SchoolTerm.objects.all().order_by("-date_start")
        stats = []
        for school_term in school_terms:
            stat = {}
            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():
                continue