Skip to content
Snippets Groups Projects
forms.py 15.41 KiB
from datetime import datetime, timedelta
from typing import Optional, Sequence

from django import forms
from django.core.exceptions import ValidationError
from django.db.models import Count, Q
from django.http import HttpRequest
from django.utils import timezone
from django.utils.translation import gettext_lazy as _

from django_select2.forms import ModelSelect2MultipleWidget, ModelSelect2Widget, Select2Widget
from guardian.shortcuts import get_objects_for_user
from material import Fieldset, Layout, Row

from aleksis.apps.chronos.managers import TimetableType
from aleksis.apps.chronos.models import LessonPeriod, Subject, TimePeriod
from aleksis.core.forms import ActionForm, ListActionForm
from aleksis.core.models import Group, Person, SchoolTerm
from aleksis.core.util.core_helpers import get_site_preferences
from aleksis.core.util.predicates import check_global_permission

from .actions import (
    delete_personal_note,
    mark_as_excuse_type_generator,
    mark_as_excused,
    mark_as_unexcused,
    send_request_to_check_entry,
)
from .models import (
    ExcuseType,
    ExtraMark,
    GroupRole,
    GroupRoleAssignment,
    LessonDocumentation,
    PersonalNote,
)


class LessonDocumentationForm(forms.ModelForm):
    class Meta:
        model = LessonDocumentation
        fields = ["topic", "homework", "group_note"]

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)

        self.fields["homework"].label = _("Homework for the next lesson")
        if (
            self.instance.lesson_period
            and get_site_preferences()["alsijil__allow_carry_over_same_week"]
        ):
            self.fields["carry_over_week"] = forms.BooleanField(
                label=_("Carry over data to all other lessons with the same subject in this week"),
                initial=True,
                required=False,
            )

    def save(self, **kwargs):
        lesson_documentation = super().save(commit=True)
        if (
            get_site_preferences()["alsijil__allow_carry_over_same_week"]
            and self.cleaned_data["carry_over_week"]
            and (
                lesson_documentation.topic
                or lesson_documentation.homework
                or lesson_documentation.group_note
            )
            and lesson_documentation.lesson_period
        ):
            lesson_documentation.carry_over_data(
                LessonPeriod.objects.filter(lesson=lesson_documentation.lesson_period.lesson)
            )


class PersonalNoteForm(forms.ModelForm):
    class Meta:
        model = PersonalNote
        fields = ["absent", "tardiness", "excused", "excuse_type", "extra_marks", "remarks"]

    person_name = forms.CharField(disabled=True)

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.fields["person_name"].widget.attrs.update(
            {"class": "alsijil-lesson-personal-note-name"}
        )
        self.fields["person_name"].widget = forms.HiddenInput()

        if self.instance and getattr(self.instance, "person", None):
            self.fields["person_name"].initial = str(self.instance.person)


class SelectForm(forms.Form):
    layout = Layout(Row("group", "teacher"))

    group = forms.ModelChoiceField(
        queryset=None,
        label=_("Group"),
        required=False,
        widget=Select2Widget,
    )
    teacher = forms.ModelChoiceField(
        queryset=None,
        label=_("Teacher"),
        required=False,
        widget=Select2Widget,
    )

    def clean(self) -> dict:
        data = super().clean()

        if data.get("group") and not data.get("teacher"):
            type_ = TimetableType.GROUP
            instance = data["group"]
        elif data.get("teacher") and not data.get("group"):
            type_ = TimetableType.TEACHER
            instance = data["teacher"]
        elif not data.get("teacher") and not data.get("group"):
            return data
        else:
            raise ValidationError(_("You can't select a group and a teacher both."))

        data["type_"] = type_
        data["instance"] = instance
        return data

    def __init__(self, request, *args, **kwargs):
        self.request = request
        super().__init__(*args, **kwargs)

        person = self.request.user.person

        group_qs = Group.get_groups_with_lessons()

        # Filter selectable groups by permissions
        if not check_global_permission(self.request.user, "alsijil.view_week"):
            # 1) All groups the user is allowed to see the week view by object permissions
            # 2) All groups the user is a member of an owner of
            # 3) If the corresponding preference is turned on:
            # All groups that have a parent group the user is an owner of
            group_qs = (
                group_qs.filter(
                    pk__in=get_objects_for_user(
                        self.request.user, "core.view_week_class_register_group", Group
                    ).values_list("pk", flat=True)
                )
            ).union(
                group_qs.filter(
                    Q(members=person) | Q(owners=person) | Q(parent_groups__owners=person)
                    if get_site_preferences()["alsijil__inherit_privileges_from_parent_group"]
                    else Q(members=person) | Q(owners=person)
                )
            )

        # Flatten query by filtering groups by pk
        self.fields["group"].queryset = Group.objects.filter(
            pk__in=list(group_qs.values_list("pk", flat=True))
        )

        teacher_qs = Person.objects.annotate(lessons_count=Count("lessons_as_teacher")).filter(
            lessons_count__gt=0
        )

        # Filter selectable teachers by permissions
        if not check_global_permission(self.request.user, "alsijil.view_week"):
            # If the user hasn't got the global permission and the inherit privileges preference is
            # turned off, the user is only allowed to see their own person. Otherwise, the user
            # is allowed to see all persons that teach lessons that the given groups attend.
            if get_site_preferences()["alsijil__inherit_privileges_from_parent_group"]:
                teacher_pks = []
                for group in group_qs:
                    for lesson in group.lessons.all():
                        for teacher in lesson.teachers.all():
                            teacher_pks.append(teacher.pk)
                teacher_qs = teacher_qs.filter(pk__in=teacher_pks)
            else:
                teacher_qs = teacher_qs.filter(pk=person.pk)

        self.fields["teacher"].queryset = teacher_qs


PersonalNoteFormSet = forms.modelformset_factory(
    PersonalNote, form=PersonalNoteForm, max_num=0, extra=0
)


class RegisterAbsenceForm(forms.Form):
    layout = Layout(
        Fieldset("", "person"),
        Fieldset("", Row("date_start", "date_end"), Row("from_period", "to_period")),
        Fieldset("", Row("absent", "excused"), Row("excuse_type"), Row("remarks")),
    )
    person = forms.ModelChoiceField(label=_("Person"), queryset=None, widget=Select2Widget)
    date_start = forms.DateField(label=_("Start date"), initial=datetime.today)
    date_end = forms.DateField(label=_("End date"), initial=datetime.today)
    from_period = forms.ChoiceField(label=_("Start period"))
    to_period = forms.ChoiceField(label=_("End period"))
    absent = forms.BooleanField(label=_("Absent"), initial=True, required=False)
    excused = forms.BooleanField(label=_("Excused"), initial=True, required=False)
    excuse_type = forms.ModelChoiceField(
        label=_("Excuse type"),
        queryset=ExcuseType.objects.all(),
        widget=Select2Widget,
        required=False,
    )
    remarks = forms.CharField(label=_("Remarks"), max_length=30, required=False)

    def __init__(self, request, *args, **kwargs):
        self.request = request
        super().__init__(*args, **kwargs)
        period_choices = TimePeriod.period_choices

        if self.request.user.has_perm("alsijil.register_absence"):
            self.fields["person"].queryset = Person.objects.all()
        else:
            persons_qs = Person.objects.filter(
                Q(
                    pk__in=get_objects_for_user(
                        self.request.user, "core.register_absence_person", Person
                    )
                )
                | Q(primary_group__owners=self.request.user.person)
                | Q(
                    member_of__in=get_objects_for_user(
                        self.request.user, "core.register_absence_group", Group
                    )
                )
            ).distinct()

            self.fields["person"].queryset = persons_qs

        self.fields["from_period"].choices = period_choices
        self.fields["to_period"].choices = period_choices
        self.fields["from_period"].initial = TimePeriod.period_min
        self.fields["to_period"].initial = TimePeriod.period_max


class ExtraMarkForm(forms.ModelForm):
    layout = Layout("short_name", "name")

    class Meta:
        model = ExtraMark
        fields = ["short_name", "name"]


class ExcuseTypeForm(forms.ModelForm):
    layout = Layout("short_name", "name", "count_as_absent")

    class Meta:
        model = ExcuseType
        fields = ["short_name", "name", "count_as_absent"]


class PersonOverviewForm(ActionForm):
    def get_actions(self):
        return (
            [mark_as_excused, mark_as_unexcused]
            + [
                mark_as_excuse_type_generator(excuse_type)
                for excuse_type in ExcuseType.objects.all()
            ]
            + [delete_personal_note]
        )


class GroupRoleForm(forms.ModelForm):
    layout = Layout("name", "icon", "colour")

    class Meta:
        model = GroupRole
        fields = ["name", "icon", "colour"]


class AssignGroupRoleForm(forms.ModelForm):
    layout_base = ["groups", "person", "role", Row("date_start", "date_end")]

    groups = forms.ModelMultipleChoiceField(
        label=_("Group"),
        required=True,
        queryset=Group.objects.all(),
        widget=ModelSelect2MultipleWidget(
            model=Group,
            search_fields=["name__icontains", "short_name__icontains"],
            attrs={
                "data-minimum-input-length": 0,
                "class": "browser-default",
            },
        ),
    )
    person = forms.ModelChoiceField(
        label=_("Person"),
        required=True,
        queryset=Person.objects.all(),
        widget=ModelSelect2Widget(
            model=Person,
            search_fields=[
                "first_name__icontains",
                "last_name__icontains",
                "short_name__icontains",
            ],
            attrs={"data-minimum-input-length": 0, "class": "browser-default"},
        ),
    )

    def __init__(self, request, *args, **kwargs):
        self.request = request
        initial = kwargs.get("initial", {})

        # Build layout with or without groups field
        base_layout = self.layout_base[:]
        if "groups" in initial:
            base_layout.remove("groups")
        self.layout = Layout(*base_layout)

        super().__init__(*args, **kwargs)

        if "groups" in initial:
            self.fields["groups"].required = False

        # Filter persons and groups by permissions
        if not self.request.user.has_perm("alsijil.assign_grouprole"):  # Global permission
            persons = Person.objects
            if initial.get("groups"):
                persons = persons.filter(member_of__in=initial["groups"])
            if get_site_preferences()["alsijil__group_owners_can_assign_roles_to_parents"]:
                persons = persons.filter(
                    Q(member_of__owners=self.request.user.person)
                    | Q(children__member_of__owners=self.request.user.person)
                )
            else:
                persons = persons.filter(member_of__owners=self.request.user.person)
            self.fields["person"].queryset = persons.distinct()

            if "groups" not in initial:
                groups = (
                    Group.objects.for_current_school_term_or_all()
                    .filter(
                        Q(owners=self.request.user.person)
                        | Q(parent_groups__owners=self.request.user.person)
                        if get_site_preferences()["alsijil__inherit_privileges_from_parent_group"]
                        else Q(owners=self.request.user.person)
                    )
                    .distinct()
                )
                self.fields["groups"].queryset = groups

    def clean_groups(self):
        """Ensure that only permitted groups are used."""
        return self.initial["groups"] if "groups" in self.initial else self.cleaned_data["groups"]

    class Meta:
        model = GroupRoleAssignment
        fields = ["groups", "person", "role", "date_start", "date_end"]


class GroupRoleAssignmentEditForm(forms.ModelForm):
    class Meta:
        model = GroupRoleAssignment
        fields = ["date_start", "date_end"]


class FilterRegisterObjectForm(forms.Form):
    """Form for filtering register objects in ``RegisterObjectTable``."""

    layout = Layout(
        Row("school_term", "date_start", "date_end"), Row("has_documentation", "group", "subject")
    )
    school_term = forms.ModelChoiceField(queryset=None, label=_("School term"))
    has_documentation = forms.NullBooleanField(label=_("Has lesson documentation"))
    group = forms.ModelChoiceField(queryset=None, label=_("Group"), required=False)
    subject = forms.ModelChoiceField(queryset=None, label=_("Subject"), required=False)
    date_start = forms.DateField(label=_("Start date"))
    date_end = forms.DateField(label=_("End date"))

    @classmethod
    def get_initial(cls, has_documentation: Optional[bool] = None):
        date_end = timezone.now().date()
        date_start = date_end - timedelta(days=30)
        school_term = SchoolTerm.current

        # If there is no current school year, use last known school year.
        if not school_term:
            school_term = SchoolTerm.objects.all().last()

        return {
            "school_term": school_term,
            "date_start": date_start,
            "date_end": date_end,
            "has_documentation": has_documentation,
        }

    def __init__(
        self,
        request: HttpRequest,
        *args,
        for_person: bool = True,
        default_documentation: Optional[bool] = None,
        groups: Optional[Sequence[Group]] = None,
        **kwargs,
    ):
        self.request = request
        person = self.request.user.person

        kwargs["initial"] = self.get_initial(has_documentation=default_documentation)
        super().__init__(*args, **kwargs)

        self.fields["school_term"].queryset = SchoolTerm.objects.all()

        if not groups and for_person:
            groups = Group.objects.filter(
                Q(lessons__teachers=person)
                | Q(lessons__lesson_periods__substitutions__teachers=person)
                | Q(events__teachers=person)
                | Q(extra_lessons__teachers=person)
            ).distinct()
        elif not for_person:
            groups = Group.objects.all()
        self.fields["group"].queryset = groups

        # Filter subjects by selectable groups
        subject_qs = Subject.objects.filter(
            Q(lessons__groups__in=groups) | Q(extra_lessons__groups__in=groups)
        ).distinct()
        self.fields["subject"].queryset = subject_qs


class RegisterObjectActionForm(ListActionForm):
    """Action form for managing register objects for use with ``RegisterObjectTable``."""

    actions = [send_request_to_check_entry]