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(LessonDocumentationForm, self).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]