diff --git a/aleksis/apps/alsijil/actions.py b/aleksis/apps/alsijil/actions.py deleted file mode 100644 index 623a96b95ad28e319d16b4b2595f74352a03655d..0000000000000000000000000000000000000000 --- a/aleksis/apps/alsijil/actions.py +++ /dev/null @@ -1,92 +0,0 @@ -from typing import Callable, Sequence - -from django.contrib import messages -from django.contrib.humanize.templatetags.humanize import apnumber -from django.http import HttpRequest -from django.template.loader import get_template -from django.urls import reverse -from django.utils.translation import gettext_lazy as _ - -from aleksis.apps.alsijil.models import PersonalNote -from aleksis.core.models import Notification - - -def mark_as_excused(modeladmin, request, queryset): - queryset.filter(absent=True).update(excused=True, excuse_type=None) - - -mark_as_excused.short_description = _("Mark as excused") - - -def mark_as_unexcused(modeladmin, request, queryset): - queryset.filter(absent=True).update(excused=False, excuse_type=None) - - -mark_as_unexcused.short_description = _("Mark as unexcused") - - -def mark_as_excuse_type_generator(excuse_type) -> Callable: - def mark_as_excuse_type(modeladmin, request, queryset): - queryset.filter(absent=True).update(excused=True, excuse_type=excuse_type) - - mark_as_excuse_type.short_description = _(f"Mark as {excuse_type.name}") - mark_as_excuse_type.__name__ = f"mark_as_excuse_type_{excuse_type.short_name}" - - return mark_as_excuse_type - - -def delete_personal_note(modeladmin, request, queryset): - notes = [] - for personal_note in queryset: - personal_note.reset_values() - notes.append(personal_note) - PersonalNote.objects.bulk_update( - notes, fields=["absent", "excused", "tardiness", "excuse_type", "remarks"] - ) - - -delete_personal_note.short_description = _("Delete") - - -def send_request_to_check_entry(modeladmin, request: HttpRequest, selected_items: Sequence[dict]): - """Send notifications to the teachers of the selected register objects. - - Action for use with ``RegisterObjectTable`` and ``RegisterObjectActionForm``. - """ - # Group class register entries by teachers so each teacher gets just one notification - grouped_by_teachers = {} - for entry in selected_items: - teachers = entry["register_object"].get_teachers().all() - for teacher in teachers: - grouped_by_teachers.setdefault(teacher, []) - grouped_by_teachers[teacher].append(entry) - - template = get_template("alsijil/notifications/check.html") - for teacher, items in grouped_by_teachers.items(): - msg = template.render({"items": items}) - - title = _("{} asks you to check some class register entries.").format( - request.user.person.addressing_name - ) - - n = Notification( - title=title, - description=msg, - sender=request.user.person.addressing_name, - recipient=teacher, - link=request.build_absolute_uri(reverse("overview_me")), - ) - n.save() - - count_teachers = len(grouped_by_teachers.keys()) - count_items = len(selected_items) - messages.success( - request, - _( - "We have successfully sent notifications to " - "{count_teachers} persons for {count_items} lessons." - ).format(count_teachers=apnumber(count_teachers), count_items=apnumber(count_items)), - ) - - -send_request_to_check_entry.short_description = _("Ask teacher to check data") diff --git a/aleksis/apps/alsijil/data_checks.py b/aleksis/apps/alsijil/data_checks.py index 87e703a0cb4fb17cff6be360cbea52895a6fa775..b9b88a1556c13980cd51f30c432b9933a7039638 100644 --- a/aleksis/apps/alsijil/data_checks.py +++ b/aleksis/apps/alsijil/data_checks.py @@ -159,23 +159,3 @@ class PersonalNoteOnHolidaysDataCheck(DataCheck): for note in personal_notes: logging.info(f"Personal note {note} is on holidays") cls.register_result(note) - - -class ExcusesWithoutAbsences(DataCheck): - name = "excuses_without_absences" - verbose_name = _("Ensure that there are no excused personal notes without an absence") - problem_name = _("The personal note is marked as excused, but not as absent.") - solve_options = { - ResetPersonalNoteSolveOption.name: ResetPersonalNoteSolveOption, - IgnoreSolveOption.name: IgnoreSolveOption, - } - - @classmethod - def check_data(cls): - from .models import PersonalNote - - personal_notes = PersonalNote.objects.filter(excused=True, absent=False) - - for note in personal_notes: - logging.info(f"Check personal note {note}") - cls.register_result(note) diff --git a/aleksis/apps/alsijil/filters.py b/aleksis/apps/alsijil/filters.py deleted file mode 100644 index 0de3f04876edf978403287ed1031be79ba88790c..0000000000000000000000000000000000000000 --- a/aleksis/apps/alsijil/filters.py +++ /dev/null @@ -1,47 +0,0 @@ -from django.utils.translation import gettext as _ - -from django_filters import CharFilter, DateFilter, FilterSet -from material import Layout, Row - -from aleksis.core.models import SchoolTerm - -from .models import PersonalNote - - -class PersonalNoteFilter(FilterSet): - day_start = DateFilter(lookup_expr="gte", label=_("After")) - day_end = DateFilter(lookup_expr="lte", label=_("Before")) - subject = CharFilter(lookup_expr="icontains", label=_("Subject")) - - def __init__(self, data=None, *args, **kwargs): - if data is not None: - data = data.copy() - - current_school_term = SchoolTerm.current - if not data.get("day_start") and current_school_term: - data["day_start"] = current_school_term.date_start - - for name, f in self.base_filters.items(): - initial = f.extra.get("initial") - if not data.get(name) and initial: - data[name] = initial - - super().__init__(data, *args, **kwargs) - self.form.fields["tardiness__lt"].label = _("Tardiness is lower than") - self.form.fields["tardiness__gt"].label = _("Tardiness is bigger than") - self.form.layout = Layout( - Row("subject"), - Row("day_start", "day_end"), - Row("absent", "excused", "excuse_type"), - Row("tardiness__gt", "tardiness__lt", "extra_marks"), - ) - - class Meta: - model = PersonalNote - fields = { - "excused": ["exact"], - "tardiness": ["lt", "gt"], - "absent": ["exact"], - "excuse_type": ["exact"], - "extra_marks": ["exact"], - } diff --git a/aleksis/apps/alsijil/forms.py b/aleksis/apps/alsijil/forms.py index 35cdf3b70552de594a0c7ef8811c53220ddef071..45784137cb114b360709eedfbdab7f5a16b3c94c 100644 --- a/aleksis/apps/alsijil/forms.py +++ b/aleksis/apps/alsijil/forms.py @@ -1,268 +1,19 @@ -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.db.models import Q 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 django_select2.forms import ModelSelect2MultipleWidget, ModelSelect2Widget +from material import 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.models import Group, Person 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") @@ -357,74 +108,3 @@ 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] diff --git a/aleksis/apps/alsijil/managers.py b/aleksis/apps/alsijil/managers.py index dc29fae25c189c7c2250279a6e8d86206f52e520..0eabc94d78bb261fa1b597bead2624ce4200d120 100644 --- a/aleksis/apps/alsijil/managers.py +++ b/aleksis/apps/alsijil/managers.py @@ -1,12 +1,9 @@ from datetime import date, datetime from typing import TYPE_CHECKING, Optional, Sequence, Union -from django.db.models import Case, ExpressionWrapper, F, Func, QuerySet, Value, When -from django.db.models.fields import DateField -from django.db.models.functions import Concat +from django.db.models import QuerySet from django.db.models.query import Prefetch from django.db.models.query_utils import Q -from django.utils.translation import gettext as _ from calendarweek import CalendarWeek @@ -17,125 +14,6 @@ if TYPE_CHECKING: from aleksis.core.models import Group -class RegisterObjectRelatedQuerySet(QuerySet): - """Common queryset for personal notes and lesson documentations with shared API.""" - - def _get_weekday_to_date(self, weekday_name, year_name="year", week_name="week"): - """Get a ORM function which converts a weekday, a week and a year to a date.""" - return ExpressionWrapper( - Func( - Concat(F(year_name), F(week_name)), - Value("IYYYIW"), - output_field=DateField(), - function="TO_DATE", - ) - + F(weekday_name), - output_field=DateField(), - ) - - def annotate_day(self) -> QuerySet: - """Annotate every personal note/lesson documentation with the real date. - - Attribute name: ``day`` - - .. note:: - For events, this will annotate ``None``. - """ - return self.annotate( - day=Case( - When( - lesson_period__isnull=False, - then=self._get_weekday_to_date("lesson_period__period__weekday"), - ), - When( - extra_lesson__isnull=False, - then=self._get_weekday_to_date( - "extra_lesson__period__weekday", "extra_lesson__year", "extra_lesson__week" - ), - ), - ) - ) - - def annotate_date_range(self) -> QuerySet: - """Annotate every personal note/lesson documentation with the real date. - - Attribute names: ``day_start``, ``day_end`` - - .. note:: - For lesson periods and extra lessons, - this will annotate the same date for start and end day. - """ - return self.annotate_day().annotate( - day_start=Case( - When(day__isnull=False, then="day"), - When(day__isnull=True, then="event__date_start"), - ), - day_end=Case( - When(day__isnull=False, then="day"), - When(day__isnull=True, then="event__date_end"), - ), - ) - - def annotate_subject(self) -> QuerySet: - """Annotate lesson documentations with the subjects.""" - return self.annotate( - subject=Case( - When( - lesson_period__isnull=False, - then="lesson_period__lesson__subject__name", - ), - When( - extra_lesson__isnull=False, - then="extra_lesson__subject__name", - ), - default=Value(_("Event")), - ) - ) - - -class PersonalNoteManager(AlekSISBaseManagerWithoutMigrations): - """Manager adding specific methods to personal notes.""" - - def get_queryset(self): - """Ensure all related lesson and person data are loaded as well.""" - return ( - super() - .get_queryset() - .select_related( - "person", - "excuse_type", - "lesson_period", - "lesson_period__lesson", - "lesson_period__lesson__subject", - "lesson_period__period", - "lesson_period__lesson__validity", - "lesson_period__lesson__validity__school_term", - "event", - "extra_lesson", - "extra_lesson__subject", - ) - .prefetch_related("extra_marks") - ) - - -class PersonalNoteQuerySet(RegisterObjectRelatedQuerySet, QuerySet): - def not_empty(self): - """Get all not empty personal notes.""" - return self.filter( - ~Q(remarks="") | Q(absent=True) | ~Q(tardiness=0) | Q(extra_marks__isnull=False) - ) - - -class LessonDocumentationManager(AlekSISBaseManagerWithoutMigrations): - pass - - -class LessonDocumentationQuerySet(RegisterObjectRelatedQuerySet, QuerySet): - def not_empty(self): - """Get all not empty lesson documentations.""" - return self.filter(~Q(topic="") | ~Q(group_note="") | ~Q(homework="")) - - class GroupRoleManager(AlekSISBaseManagerWithoutMigrations): pass diff --git a/aleksis/apps/alsijil/model_extensions.py b/aleksis/apps/alsijil/model_extensions.py index abbedd709dd3604172b831c2c931a2ea5f198b5a..b3186f6103de2d0f6781d03d9562e468124e80f3 100644 --- a/aleksis/apps/alsijil/model_extensions.py +++ b/aleksis/apps/alsijil/model_extensions.py @@ -1,188 +1,6 @@ -from datetime import date -from typing import Dict, Iterable, Iterator, Optional, Union - -from django.db.models import Exists, FilteredRelation, OuterRef, Q, QuerySet -from django.db.models.aggregates import Count, Sum -from django.urls import reverse from django.utils.translation import gettext as _ -from calendarweek import CalendarWeek - -from aleksis.apps.alsijil.managers import PersonalNoteQuerySet -from aleksis.apps.chronos.models import Event, ExtraLesson, LessonPeriod from aleksis.core.models import Group, Person -from aleksis.core.util.core_helpers import get_site_preferences - -from .models import ExcuseType, ExtraMark, LessonDocumentation, PersonalNote - - -def alsijil_url( - self: Union[LessonPeriod, Event, ExtraLesson], week: Optional[CalendarWeek] = None -) -> str: - """Build URL for the detail page of register objects. - - Works with `LessonPeriod`, `Event` and `ExtraLesson`. - - On `LessonPeriod` objects, it will work with annotated or passed weeks. - """ - if isinstance(self, LessonPeriod): - week = week or self.week - return reverse("lesson_period", args=[week.year, week.week, self.pk]) - else: - return reverse(self.label_, args=[self.pk]) - - -LessonPeriod.property_(alsijil_url) -LessonPeriod.method(alsijil_url, "get_alsijil_url") -Event.property_(alsijil_url) -Event.method(alsijil_url, "get_alsijil_url") -ExtraLesson.property_(alsijil_url) -ExtraLesson.method(alsijil_url, "get_alsijil_url") - - -@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 period number. - - This function creates `PersonalNote` objects for every `LessonPeriod` and `ExtraLesson` - 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) - ) - extra_lessons = ( - ExtraLesson.objects.filter(groups__members=self) - .on_day(day) - .filter(period__period__gte=from_period) - ) - - if to_period: - lesson_periods = lesson_periods.filter(period__period__lte=to_period) - extra_lessons = extra_lessons.filter(period__period__lte=to_period) - - # Create and update all personal notes for the discovered lesson periods - if not dry_run: - for register_object in list(lesson_periods) + list(extra_lessons): - if isinstance(register_object, LessonPeriod): - sub = register_object.get_substitution() - q_attrs = dict( - week=wanted_week.week, year=wanted_week.year, lesson_period=register_object - ) - else: - sub = None - q_attrs = dict(extra_lesson=register_object) - - if sub and sub.cancelled: - continue - - personal_note, created = ( - PersonalNote.objects.select_related(None) - .prefetch_related(None) - .update_or_create( - person=self, - defaults={ - "absent": absent, - "excused": excused, - "excuse_type": excuse_type, - }, - **q_attrs, - ) - ) - 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() + extra_lessons.count() - - -def get_personal_notes( - self, persons: QuerySet, wanted_week: Optional[CalendarWeek] = None -) -> PersonalNoteQuerySet: - """Get all personal notes for that register object in a specified week. - - The week is optional for extra lessons and events as they have own date information. - - Returns all linked `PersonalNote` objects, - filtered by the given week for `LessonPeriod` objects, - 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 - if isinstance(self, LessonPeriod): - q_attrs = dict(week=wanted_week.week, year=wanted_week.year, lesson_period=self) - elif isinstance(self, Event): - q_attrs = dict(event=self) - else: - q_attrs = dict(extra_lesson=self) - - missing_persons = persons.annotate( - no_personal_notes=~Exists(PersonalNote.objects.filter(person__pk=OuterRef("pk"), **q_attrs)) - ).filter( - member_of__in=Group.objects.filter(pk__in=self.get_groups().all()), - no_personal_notes=True, - ) - - # Create all missing personal notes - new_personal_notes = [ - PersonalNote( - person=person, - **q_attrs, - ) - 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(**q_attrs, person__in=persons) - .select_related(None) - .prefetch_related(None) - .select_related("person", "excuse_type") - .prefetch_related("extra_marks") - ) - - -LessonPeriod.method(get_personal_notes) -Event.method(get_personal_notes) -ExtraLesson.method(get_personal_notes) # Dynamically add extra permissions to Group and Person models in core # Note: requires migrate afterwards @@ -208,288 +26,3 @@ Group.add_permission( ) Group.add_permission("assign_grouprole", _("Can assign a group role for this 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 - - -def get_lesson_documentation_single( - self, week: Optional[CalendarWeek] = None -) -> Union[LessonDocumentation, None]: - """Get lesson documentation object for this event/extra lesson.""" - if self.documentations.exists(): - return self.documentations.all()[0] - return None - - -Event.method(get_lesson_documentation_single, "get_lesson_documentation") -ExtraLesson.method(get_lesson_documentation_single, "get_lesson_documentation") - - -@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, __ = LessonDocumentation.objects.get_or_create( - lesson_period=self, week=week.week, year=week.year - ) - return lesson_documentation - - -def get_or_create_lesson_documentation_single( - self, week: Optional[CalendarWeek] = None -) -> LessonDocumentation: - """Get or create lesson documentation object for this event/extra lesson.""" - lesson_documentation, created = LessonDocumentation.objects.get_or_create(**{self.label_: self}) - return lesson_documentation - - -Event.method(get_or_create_lesson_documentation_single, "get_or_create_lesson_documentation") -ExtraLesson.method(get_or_create_lesson_documentation_single, "get_or_create_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(), - ) - - -def get_absences_simple(self, week: Optional[CalendarWeek] = None) -> Iterator: - """Get all personal notes of absent persons for this event/extra lesson.""" - return filter(lambda p: p.absent, self.personal_notes.all()) - - -Event.method(get_absences_simple, "get_absences") -ExtraLesson.method(get_absences_simple, "get_absences") - - -@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) - - -def get_excused_absences_simple(self, week: Optional[CalendarWeek] = None) -> QuerySet: - """Get all personal notes of excused absent persons for this event/extra lesson.""" - return self.personal_notes.filter(absent=True, excused=True) - - -Event.method(get_excused_absences_simple, "get_excused_absences") -ExtraLesson.method(get_excused_absences_simple, "get_excused_absences") - - -@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) - - -def get_unexcused_absences_simple(self, week: Optional[CalendarWeek] = None) -> QuerySet: - """Get all personal notes of unexcused absent persons for this event/extra lesson.""" - return self.personal_notes.filter(absent=True, excused=False) - - -Event.method(get_unexcused_absences_simple, "get_unexcused_absences") -ExtraLesson.method(get_unexcused_absences_simple, "get_unexcused_absences") - - -@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, tardiness__gt=0) - - -def get_tardinesses_simple(self, week: Optional[CalendarWeek] = None) -> QuerySet: - """Get all personal notes of late persons for this event/extra lesson.""" - return self.personal_notes.filter(tardiness__gt=0) - - -Event.method(get_tardinesses_simple, "get_tardinesses") -ExtraLesson.method(get_tardinesses_simple, "get_tardinesses") - - -@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 - - -def get_extra_marks_simple(self, week: Optional[CalendarWeek] = None) -> Dict[ExtraMark, QuerySet]: - """Get all statistics on extra marks for this event/extra lesson.""" - stats = {} - for extra_mark in ExtraMark.objects.all(): - qs = self.personal_notes.filter(extra_marks=extra_mark) - if qs: - stats[extra_mark] = qs - - return stats - - -Event.method(get_extra_marks_simple, "get_extra_marks") -ExtraLesson.method(get_extra_marks_simple, "get_extra_marks") - - -@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, as well as all - child groups of the groups owned by the person with related lessons if the - inherit_privileges_from_parent_group preference is turned on. - """ - if get_site_preferences()["alsijil__inherit_privileges_from_parent_group"]: - return ( - Group.get_groups_with_lessons() - .filter(Q(owners=self) | Q(parent_groups__owners=self)) - .distinct() - ) - 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() - - lesson_periods = LessonPeriod.objects.filter( - lesson__validity__school_term=self.school_term - ).filter(Q(lesson__groups=self) | Q(lesson__groups__parent_groups=self)) - extra_lessons = ExtraLesson.objects.filter(school_term=self.school_term).filter( - Q(groups=self) | Q(groups__parent_groups=self) - ) - events = Event.objects.filter(school_term=self.school_term).filter( - Q(groups=self) | Q(groups__parent_groups=self) - ) - - persons = persons.select_related("primary_group", "primary_group__school_term").order_by( - "last_name", "first_name" - ) - persons = persons.annotate( - filtered_personal_notes=FilteredRelation( - "personal_notes", - condition=( - Q(personal_notes__event__in=events) - | Q(personal_notes__lesson_period__in=lesson_periods) - | Q(personal_notes__extra_lesson__in=extra_lessons) - ), - ) - ).annotate( - absences_count=Count( - "filtered_personal_notes", - filter=Q(filtered_personal_notes__absent=True) - & ~Q(filtered_personal_notes__excuse_type__count_as_absent=False), - distinct=True, - ), - excused=Count( - "filtered_personal_notes", - filter=Q( - filtered_personal_notes__absent=True, - filtered_personal_notes__excused=True, - ) - & ~Q(filtered_personal_notes__excuse_type__count_as_absent=False), - distinct=True, - ), - excused_without_excuse_type=Count( - "filtered_personal_notes", - filter=Q( - filtered_personal_notes__absent=True, - filtered_personal_notes__excused=True, - filtered_personal_notes__excuse_type__isnull=True, - ), - distinct=True, - ), - unexcused=Count( - "filtered_personal_notes", - filter=Q(filtered_personal_notes__absent=True, filtered_personal_notes__excused=False), - distinct=True, - ), - tardiness=Sum("filtered_personal_notes__tardiness"), - tardiness_count=Count( - "filtered_personal_notes", - filter=Q(filtered_personal_notes__tardiness__gt=0), - distinct=True, - ), - ) - - for extra_mark in ExtraMark.objects.all(): - persons = persons.annotate( - **{ - extra_mark.count_label: Count( - "filtered_personal_notes", - filter=Q(filtered_personal_notes__extra_marks=extra_mark), - distinct=True, - ) - } - ) - - for excuse_type in ExcuseType.objects.all(): - persons = persons.annotate( - **{ - excuse_type.count_label: Count( - "filtered_personal_notes", - filter=Q( - filtered_personal_notes__absent=True, - filtered_personal_notes__excuse_type=excuse_type, - ), - distinct=True, - ) - } - ) - - return persons diff --git a/aleksis/apps/alsijil/models.py b/aleksis/apps/alsijil/models.py index 5303a40e57e3bf7ac6bbeba98da2f6e206be6007..8f0be3f18661dab3dfffea268c3498345ad435a2 100644 --- a/aleksis/apps/alsijil/models.py +++ b/aleksis/apps/alsijil/models.py @@ -1,12 +1,10 @@ -from datetime import date, datetime -from typing import Optional, Union -from urllib.parse import urlparse +from datetime import datetime +from typing import Optional from django.contrib.auth.models import User from django.core.exceptions import PermissionDenied from django.db import models from django.db.models import QuerySet -from django.db.models.constraints import CheckConstraint from django.db.models.query_utils import Q from django.http import HttpRequest from django.urls import reverse @@ -15,420 +13,29 @@ from django.utils.formats import date_format from django.utils.timezone import localdate, localtime, now from django.utils.translation import gettext_lazy as _ -from calendarweek import CalendarWeek from colorfield.fields import ColorField -from aleksis.apps.alsijil.data_checks import ( - ExcusesWithoutAbsences, - LessonDocumentationOnHolidaysDataCheck, - NoGroupsOfPersonsSetInPersonalNotesDataCheck, - NoPersonalNotesInCancelledLessonsDataCheck, - PersonalNoteOnHolidaysDataCheck, -) from aleksis.apps.alsijil.managers import ( DocumentationManager, GroupRoleAssignmentManager, GroupRoleAssignmentQuerySet, GroupRoleManager, GroupRoleQuerySet, - LessonDocumentationManager, - LessonDocumentationQuerySet, ParticipationStatusManager, - PersonalNoteManager, - PersonalNoteQuerySet, ) from aleksis.apps.chronos.managers import GroupPropertiesMixin -from aleksis.apps.chronos.mixins import WeekRelatedMixin -from aleksis.apps.chronos.models import Event, ExtraLesson, LessonEvent, LessonPeriod, TimePeriod +from aleksis.apps.chronos.models import LessonEvent from aleksis.apps.chronos.util.format import format_m2m from aleksis.apps.cursus.models import Course, Subject from aleksis.apps.kolego.models import Absence as KolegoAbsence from aleksis.apps.kolego.models import AbsenceReason from aleksis.core.data_checks import field_validation_data_check_factory from aleksis.core.mixins import ExtensibleModel, GlobalPermissionModel -from aleksis.core.models import CalendarEvent, Group, Person, SchoolTerm +from aleksis.core.models import CalendarEvent, Group, Person from aleksis.core.util.core_helpers import get_site_preferences from aleksis.core.util.model_helpers import ICONS -def isidentifier(value: str) -> bool: - return value.isidentifier() - - -class ExcuseType(ExtensibleModel): - """An type of excuse. - - Can be used to count different types of absences separately. - """ - - short_name = models.CharField(max_length=255, unique=True, verbose_name=_("Short name")) - name = models.CharField(max_length=255, unique=True, verbose_name=_("Name")) - - count_as_absent = models.BooleanField( - default=True, - verbose_name=_("Count as absent"), - help_text=_( - "If checked, this excuse type will be counted as a missed lesson. If not checked," - "it won't show up in the absence report." - ), - ) - - def __str__(self): - return f"{self.name} ({self.short_name})" - - @property - def count_label(self): - return f"excuse_type_{self.id}_count" - - class Meta: - ordering = ["name"] - verbose_name = _("Excuse type") - verbose_name_plural = _("Excuse types") - - -lesson_related_constraint_q = ( - Q( - lesson_period__isnull=False, - event__isnull=True, - extra_lesson__isnull=True, - week__isnull=False, - year__isnull=False, - ) - | Q( - lesson_period__isnull=True, - event__isnull=False, - extra_lesson__isnull=True, - week__isnull=True, - year__isnull=True, - ) - | Q( - lesson_period__isnull=True, - event__isnull=True, - extra_lesson__isnull=False, - week__isnull=True, - year__isnull=True, - ) -) - - -class RegisterObjectRelatedMixin(WeekRelatedMixin): - """Mixin with common API for lesson documentations and personal notes.""" - - @property - def register_object( - self: Union["LessonDocumentation", "PersonalNote"], - ) -> Union[LessonPeriod, Event, ExtraLesson]: - """Get the object related to this lesson documentation or personal note.""" - if self.lesson_period: - return self.lesson_period - elif self.event: - return self.event - else: - return self.extra_lesson - - @property - def register_object_key(self: Union["LessonDocumentation", "PersonalNote"]) -> str: - """Get a unique reference to the related object related.""" - if self.week and self.year: - return f"{self.register_object.pk}_{self.week}_{self.year}" - else: - return self.register_object.pk - - @property - def calendar_week(self: Union["LessonDocumentation", "PersonalNote"]) -> CalendarWeek: - """Get the calendar week of this lesson documentation or personal note. - - .. note:: - - As events can be longer than one week, - this will return the week of the start date for events. - """ - if self.lesson_period: - return CalendarWeek(week=self.week, year=self.year) - elif self.extra_lesson: - return self.extra_lesson.calendar_week - else: - return CalendarWeek.from_date(self.register_object.date_start) - - @property - def school_term(self: Union["LessonDocumentation", "PersonalNote"]) -> SchoolTerm: - """Get the school term of the related register object.""" - if self.lesson_period: - return self.lesson_period.lesson.validity.school_term - else: - return self.register_object.school_term - - @property - def date(self: Union["LessonDocumentation", "PersonalNote"]) -> Optional[date]: - """Get the date of this lesson documentation or personal note. - - :: warning:: - - As events can be longer than one day, - this will return `None` for events. - """ - if self.lesson_period: - return super().date - elif self.extra_lesson: - return self.extra_lesson.date - return None - - @property - def date_formatted(self: Union["LessonDocumentation", "PersonalNote"]) -> str: - """Get a formatted version of the date of this object. - - Lesson periods, extra lessons: formatted date - Events: formatted date range - """ - return ( - date_format(self.date) - if self.date - else f"{date_format(self.event.date_start)}–{date_format(self.event.date_end)}" - ) - - @property - def period(self: Union["LessonDocumentation", "PersonalNote"]) -> TimePeriod: - """Get the date of this lesson documentation or personal note. - - :: warning:: - - As events can be longer than one day, - this will return `None` for events. - """ - if self.event: - return self.event.period_from - else: - return self.register_object.period - - @property - def period_formatted(self: Union["LessonDocumentation", "PersonalNote"]) -> str: - """Get a formatted version of the period of this object. - - Lesson periods, extra lessons: formatted period - Events: formatted period range - """ - return ( - f"{self.period.period}." - if not self.event - else f"{self.event.period_from.period}.–{self.event.period_to.period}." - ) - - def get_absolute_url(self: Union["LessonDocumentation", "PersonalNote"]) -> str: - """Get the absolute url of the detail view for the related register object.""" - return self.register_object.get_alsijil_url(self.calendar_week) - - -class PersonalNote(RegisterObjectRelatedMixin, ExtensibleModel): - """A personal note about a single person. - - Used in the class register to note absences, excuses - and remarks about a student in a single lesson period. - """ - - data_checks = [ - NoPersonalNotesInCancelledLessonsDataCheck, - NoGroupsOfPersonsSetInPersonalNotesDataCheck, - PersonalNoteOnHolidaysDataCheck, - ExcusesWithoutAbsences, - ] - - objects = PersonalNoteManager.from_queryset(PersonalNoteQuerySet)() - - person = models.ForeignKey("core.Person", models.CASCADE, related_name="personal_notes") - groups_of_person = models.ManyToManyField("core.Group", related_name="+") - - week = models.IntegerField(blank=True, null=True) - year = models.IntegerField(verbose_name=_("Year"), blank=True, null=True) - - lesson_period = models.ForeignKey( - "chronos.LessonPeriod", models.CASCADE, related_name="personal_notes", blank=True, null=True - ) - event = models.ForeignKey( - "chronos.Event", models.CASCADE, related_name="personal_notes", blank=True, null=True - ) - extra_lesson = models.ForeignKey( - "chronos.ExtraLesson", models.CASCADE, related_name="personal_notes", blank=True, null=True - ) - - absent = models.BooleanField(default=False) - tardiness = models.PositiveSmallIntegerField(default=0) - excused = models.BooleanField(default=False) - excuse_type = models.ForeignKey( - ExcuseType, - on_delete=models.SET_NULL, - null=True, - blank=True, - verbose_name=_("Excuse type"), - ) - - remarks = models.CharField(max_length=200, blank=True) - - extra_marks = models.ManyToManyField("ExtraMark", blank=True, verbose_name=_("Extra marks")) - - def save(self, *args, **kwargs): - if self.excuse_type: - self.excused = True - if not self.absent: - self.excused = False - self.excuse_type = None - super().save(*args, **kwargs) - - def reset_values(self): - """Reset all saved data to default values. - - .. warning :: - - This won't save the data, please execute ``save`` extra. - """ - defaults = PersonalNote() - - self.absent = defaults.absent - self.tardiness = defaults.tardiness - self.excused = defaults.excused - self.excuse_type = defaults.excuse_type - self.remarks = defaults.remarks - self.extra_marks.clear() - - def __str__(self) -> str: - return f"{self.date_formatted}, {self.lesson_period}, {self.person}" - - def get_absolute_url(self) -> str: - """Get the absolute url of the detail view for the related register object.""" - return urlparse(super().get_absolute_url())._replace(fragment="personal-notes").geturl() - - class Meta: - verbose_name = _("Personal note") - verbose_name_plural = _("Personal notes") - ordering = [ - "year", - "week", - "lesson_period__period__weekday", - "lesson_period__period__period", - "person__last_name", - "person__first_name", - ] - constraints = [ - CheckConstraint( - check=lesson_related_constraint_q, name="one_relation_only_personal_note" - ), - models.UniqueConstraint( - fields=("week", "year", "lesson_period", "person"), - name="unique_note_per_lp", - ), - models.UniqueConstraint( - fields=("week", "year", "event", "person"), - name="unique_note_per_ev", - ), - models.UniqueConstraint( - fields=("week", "year", "extra_lesson", "person"), - name="unique_note_per_el", - ), - ] - - -class LessonDocumentation(RegisterObjectRelatedMixin, ExtensibleModel): - """A documentation on a single lesson period. - - Non-personal, includes the topic and homework of the lesson. - """ - - objects = LessonDocumentationManager.from_queryset(LessonDocumentationQuerySet)() - - data_checks = [LessonDocumentationOnHolidaysDataCheck] - - week = models.IntegerField(blank=True, null=True) - year = models.IntegerField(verbose_name=_("Year"), blank=True, null=True) - - lesson_period = models.ForeignKey( - "chronos.LessonPeriod", models.CASCADE, related_name="documentations", blank=True, null=True - ) - event = models.ForeignKey( - "chronos.Event", models.CASCADE, related_name="documentations", blank=True, null=True - ) - extra_lesson = models.ForeignKey( - "chronos.ExtraLesson", models.CASCADE, related_name="documentations", blank=True, null=True - ) - - topic = models.CharField(verbose_name=_("Lesson topic"), max_length=200, blank=True) - homework = models.CharField(verbose_name=_("Homework"), max_length=200, blank=True) - group_note = models.CharField(verbose_name=_("Group note"), max_length=200, blank=True) - - def carry_over_data(self, all_periods_of_lesson: LessonPeriod): - """Carry over data to given periods in this lesson if data is not already set. - - Both forms of carrying over data can be deactivated using site preferences - ``alsijil__carry_over_next_periods`` and ``alsijil__allow_carry_over_same_week`` - respectively. - """ - for period in all_periods_of_lesson: - lesson_documentation = period.get_or_create_lesson_documentation( - CalendarWeek(week=self.week, year=self.year) - ) - - changed = False - - if not lesson_documentation.topic: - lesson_documentation.topic = self.topic - changed = True - - if not lesson_documentation.homework: - lesson_documentation.homework = self.homework - changed = True - - if not lesson_documentation.group_note: - lesson_documentation.group_note = self.group_note - changed = True - - if changed: - lesson_documentation.save(carry_over_next_periods=False) - - def __str__(self) -> str: - return f"{self.lesson_period}, {self.date_formatted}" - - def save(self, carry_over_next_periods=True, *args, **kwargs): - if ( - get_site_preferences()["alsijil__carry_over_next_periods"] - and (self.topic or self.homework or self.group_note) - and self.lesson_period - and carry_over_next_periods - ): - self.carry_over_data( - LessonPeriod.objects.filter( - lesson=self.lesson_period.lesson, - period__weekday=self.lesson_period.period.weekday, - ) - ) - super().save(*args, **kwargs) - - class Meta: - verbose_name = _("Lesson documentation") - verbose_name_plural = _("Lesson documentations") - ordering = [ - "year", - "week", - "lesson_period__period__weekday", - "lesson_period__period__period", - ] - constraints = [ - CheckConstraint( - check=lesson_related_constraint_q, - name="one_relation_only_lesson_documentation", - ), - models.UniqueConstraint( - fields=("week", "year", "lesson_period"), - name="unique_documentation_per_lp", - ), - models.UniqueConstraint( - fields=("week", "year", "event"), - name="unique_documentation_per_ev", - ), - models.UniqueConstraint( - fields=("week", "year", "extra_lesson"), - name="unique_documentation_per_el", - ), - ] - - class ExtraMark(ExtensibleModel): """A model for extra marks. diff --git a/aleksis/apps/alsijil/tables.py b/aleksis/apps/alsijil/tables.py index f17a214930f2d7f2e40a1662b491eab587bda0ad..acc3bcbed6f8da596bec2f40dbcdf238f5046a95 100644 --- a/aleksis/apps/alsijil/tables.py +++ b/aleksis/apps/alsijil/tables.py @@ -1,45 +1,9 @@ from django.template.loader import render_to_string -from django.utils.safestring import mark_safe from django.utils.translation import gettext_lazy as _ import django_tables2 as tables from django_tables2.utils import A -from aleksis.apps.chronos.models import Event, LessonPeriod -from aleksis.core.util.tables import SelectColumn - -from .models import PersonalNote - - -class ExcuseTypeTable(tables.Table): - class Meta: - attrs = {"class": "highlight"} - - name = tables.LinkColumn("edit_excuse_type", args=[A("id")]) - short_name = tables.Column() - count_as_absent = tables.BooleanColumn( - verbose_name=_("Count as absent"), - accessor="count_as_absent", - ) - edit = tables.LinkColumn( - "edit_excuse_type", - args=[A("id")], - text=_("Edit"), - attrs={"a": {"class": "btn-flat waves-effect waves-orange orange-text"}}, - ) - delete = tables.LinkColumn( - "delete_excuse_type", - args=[A("id")], - text=_("Delete"), - attrs={"a": {"class": "btn-flat waves-effect waves-red red-text"}}, - ) - - def before_render(self, request): - if not request.user.has_perm("alsijil.edit_excusetype_rule"): - self.columns.hide("edit") - if not request.user.has_perm("alsijil.delete_excusetype_rule"): - self.columns.hide("delete") - class GroupRoleTable(tables.Table): class Meta: @@ -68,137 +32,3 @@ class GroupRoleTable(tables.Table): self.columns.hide("edit") if not request.user.has_perm("alsijil.delete_grouprole_rule"): self.columns.hide("delete") - - -class PersonalNoteTable(tables.Table): - selected = SelectColumn(attrs={"input": {"name": "selected_objects"}}, accessor=A("pk")) - date = tables.Column( - verbose_name=_("Date"), accessor=A("date_formatted"), order_by=A("day_start"), linkify=True - ) - period = tables.Column( - verbose_name=_("Period"), - accessor=A("period_formatted"), - order_by=A("order_period"), - linkify=True, - ) - groups = tables.Column( - verbose_name=_("Groups"), - accessor=A("register_object__group_names"), - order_by=A("order_groups"), - linkify=True, - ) - teachers = tables.Column( - verbose_name=_("Teachers"), - accessor=A("register_object__teacher_names"), - order_by=A("order_teachers"), - linkify=True, - ) - subject = tables.Column(verbose_name=_("Subject"), accessor=A("subject"), linkify=True) - absent = tables.Column(verbose_name=_("Absent")) - tardiness = tables.Column(verbose_name=_("Tardiness")) - excused = tables.Column(verbose_name=_("Excuse")) - extra_marks = tables.Column(verbose_name=_("Extra marks"), accessor=A("extra_marks__all")) - - def render_groups(self, value, record): - if isinstance(record.register_object, LessonPeriod): - return record.register_object.lesson.group_names - else: - return value - - def render_subject(self, value, record): - if isinstance(record.register_object, Event): - return _("Event") - else: - return value - - def render_absent(self, value): - return ( - render_to_string( - "components/materialize-chips.html", - dict(content=_("Absent"), classes="red white-text"), - ) - if value - else "–" - ) - - def render_excused(self, value, record): - if record.absent and value: - context = dict(content=_("Excused"), classes="green white-text") - badge = render_to_string("components/materialize-chips.html", context) - if record.excuse_type: - context = dict(content=record.excuse_type.name, classes="green white-text") - badge = render_to_string("components/materialize-chips.html", context) - return badge - return "–" - - def render_tardiness(self, value): - if value: - content = _(f"{value}' tardiness") - context = dict(content=content, classes="orange white-text") - return render_to_string("components/materialize-chips.html", context) - else: - return "–" - - def render_extra_marks(self, value): - if value: - badges = "" - for extra_mark in value: - content = extra_mark.name - badges += render_to_string( - "components/materialize-chips.html", context=dict(content=content) - ) - return mark_safe(badges) # noqa - else: - return "–" - - class Meta: - model = PersonalNote - fields = () - - -def _get_link(value, record): - return record["register_object"].get_alsijil_url(record.get("week")) - - -class RegisterObjectTable(tables.Table): - """Table to show all register objects in an overview. - - .. warning:: - Works only with ``generate_list_of_all_register_objects``. - """ - - class Meta: - attrs = {"class": "highlight responsive-table"} - - status = tables.Column(accessor="register_object") - date = tables.Column(order_by="date_sort", linkify=_get_link) - period = tables.Column(order_by="period_sort", linkify=_get_link) - groups = tables.Column(linkify=_get_link) - teachers = tables.Column(linkify=_get_link) - subject = tables.Column(linkify=_get_link) - topic = tables.Column(linkify=_get_link) - homework = tables.Column(linkify=_get_link) - group_note = tables.Column(linkify=_get_link) - - def render_status(self, value, record): - context = { - "has_documentation": record.get("has_documentation", False), - "register_object": value, - } - if record.get("week"): - context["week"] = record["week"] - if record.get("substitution"): - context["substitution"] = record["substitution"] - return render_to_string("alsijil/partials/lesson_status.html", context) - - -class RegisterObjectSelectTable(RegisterObjectTable): - """Table to show all register objects with multi-select support. - - More information at ``RegisterObjectTable`` - """ - - selected = SelectColumn() - - class Meta(RegisterObjectTable.Meta): - sequence = ("selected", "...") diff --git a/aleksis/apps/alsijil/tasks.py b/aleksis/apps/alsijil/tasks.py index 7eaf8c01a5186ef08c12a5e4b19e9fc1e853d4d2..fc4b79fd24871e6092ff7a1eb4376c0710adc662 100644 --- a/aleksis/apps/alsijil/tasks.py +++ b/aleksis/apps/alsijil/tasks.py @@ -13,7 +13,7 @@ from aleksis.core.models import Group, PDFFile from aleksis.core.util.celery_progress import ProgressRecorder, recorded_task from aleksis.core.util.pdf import generate_pdf_from_template -from .models import ExcuseType, ExtraMark, LessonDocumentation, PersonalNote +from .models import ExtraMark @recorded_task diff --git a/aleksis/apps/alsijil/util/alsijil_helpers.py b/aleksis/apps/alsijil/util/alsijil_helpers.py index 118b70af8e1a71f9d5e56a8d251c5a319cae0e02..4c93eb1fc46e9fda08afa58a2dbb8d8dbf71e150 100644 --- a/aleksis/apps/alsijil/util/alsijil_helpers.py +++ b/aleksis/apps/alsijil/util/alsijil_helpers.py @@ -1,407 +1,4 @@ -from datetime import date -from operator import itemgetter -from typing import Any, Dict, Iterable, List, Optional, Sequence, Union - -from django.db.models.expressions import Exists, OuterRef -from django.db.models.query import Prefetch, QuerySet -from django.db.models.query_utils import Q -from django.http import HttpRequest -from django.utils.formats import date_format -from django.utils.translation import gettext as _ - -from calendarweek import CalendarWeek - -from aleksis.apps.alsijil.forms import FilterRegisterObjectForm -from aleksis.apps.alsijil.models import LessonDocumentation -from aleksis.apps.chronos.models import Event, ExtraLesson, Holiday, LessonPeriod -from aleksis.apps.chronos.util.chronos_helpers import get_el_by_pk from aleksis.apps.kolego.models import AbsenceReasonTag -from aleksis.core.models import Group -from aleksis.core.util.core_helpers import get_site_preferences - - -def get_register_object_by_pk( - request: HttpRequest, - model: Optional[str] = None, - year: Optional[int] = None, - week: Optional[int] = None, - id_: Optional[int] = None, -) -> Optional[Union[LessonPeriod, Event, ExtraLesson]]: - """Get register object either by given object_id or by time and current person.""" - wanted_week = CalendarWeek(year=year, week=week) - if id_ and model == "lesson": - register_object = LessonPeriod.objects.annotate_week(wanted_week).get(pk=id_) - elif id_ and model == "event": - register_object = Event.objects.get(pk=id_) - elif id_ and model == "extra_lesson": - register_object = ExtraLesson.objects.get(pk=id_) - elif hasattr(request, "user") and hasattr(request.user, "person"): - if request.user.person.lessons_as_teacher.exists(): - register_object = ( - LessonPeriod.objects.at_time().filter_teacher(request.user.person).first() - ) - else: - register_object = ( - LessonPeriod.objects.at_time().filter_participant(request.user.person).first() - ) - else: - register_object = None - return register_object - - -def get_timetable_instance_by_pk( - request: HttpRequest, - year: Optional[int] = None, - week: Optional[int] = None, - type_: Optional[str] = None, - id_: Optional[int] = None, -): - """Get timetable object (teacher, room or group) by given type and id or the current person.""" - if type_ and id_: - return get_el_by_pk(request, type_, id_) - elif hasattr(request, "user") and hasattr(request.user, "person"): - return request.user.person - - -def annotate_documentations( - klass: Union[Event, LessonPeriod, ExtraLesson], wanted_week: CalendarWeek, pks: List[int] -) -> QuerySet: - """Return an annotated queryset of all provided register objects.""" - if isinstance(klass, LessonPeriod): - prefetch = Prefetch( - "documentations", - queryset=LessonDocumentation.objects.filter( - week=wanted_week.week, year=wanted_week.year - ), - ) - else: - prefetch = Prefetch("documentations") - instances = klass.objects.prefetch_related(prefetch).filter(pk__in=pks) - - if klass == LessonPeriod: - instances = instances.annotate_week(wanted_week) - elif klass in (LessonPeriod, ExtraLesson): - instances = instances.order_by("period__weekday", "period__period") - else: - instances = instances.order_by("period_from__weekday", "period_from__period") - - instances = instances.annotate( - has_documentation=Exists( - LessonDocumentation.objects.filter( - ~Q(topic__exact=""), - Q(week=wanted_week.week, year=wanted_week.year) | Q(week=None, year=None), - ).filter(**{klass.label_: OuterRef("pk")}) - ) - ) - - return instances - - -def register_objects_sorter(register_object: Union[LessonPeriod, Event, ExtraLesson]) -> int: - """Sort key for sorted/sort for sorting a list of class register objects. - - This will sort the objects by the start period. - """ - if hasattr(register_object, "period"): - return register_object.period.period - elif isinstance(register_object, Event): - return register_object.period_from_on_day - else: - return 0 - - -def _filter_register_objects_by_dict( - filter_dict: Dict[str, Any], - register_objects: QuerySet[Union[LessonPeriod, Event, ExtraLesson]], - label_: str, -) -> QuerySet[Union[LessonPeriod, Event, ExtraLesson]]: - """Filter register objects by a dictionary generated through ``FilterRegisterObjectForm``.""" - if label_ == LessonPeriod.label_: - register_objects = register_objects.filter( - lesson__validity__school_term=filter_dict.get("school_term") - ) - else: - register_objects = register_objects.filter(school_term=filter_dict.get("school_term")) - register_objects = register_objects.distinct() - - if ( - filter_dict.get("date_start") - and filter_dict.get("date_end") - and label_ != LessonPeriod.label_ - ): - register_objects = register_objects.within_dates( - filter_dict.get("date_start"), filter_dict.get("date_end") - ) - - if filter_dict.get("person"): - if label_ == LessonPeriod.label_: - register_objects = register_objects.filter( - Q(lesson__teachers=filter_dict.get("person")) - | Q(substitutions__teachers=filter_dict.get("person")) - ) - else: - register_objects = register_objects.filter_teacher(filter_dict.get("person")) - - if filter_dict.get("group"): - register_objects = register_objects.filter_group(filter_dict.get("group")) - - if filter_dict.get("groups"): - register_objects = register_objects.filter_groups(filter_dict.get("groups")) - - if filter_dict.get("subject"): - if label_ == LessonPeriod.label_: - register_objects = register_objects.filter( - Q(lesson__subject=filter_dict.get("subject")) - | Q(substitutions__subject=filter_dict.get("subject")) - ) - elif label_ == Event.label_: - # As events have no subject, we exclude them at all - register_objects = register_objects.none() - else: - register_objects = register_objects.filter(subject=filter_dict.get("subject")) - - return register_objects - - -def _generate_dicts_for_lesson_periods( - filter_dict: Dict[str, Any], - lesson_periods: QuerySet[LessonPeriod], - documentations: Optional[Iterable[LessonDocumentation]] = None, - holiday_days: Optional[Sequence[date]] = None, -) -> List[Dict[str, Any]]: - """Generate a list of dicts for use with ``RegisterObjectTable``.""" - if not holiday_days: - holiday_days = [] - lesson_periods = list(lesson_periods) - date_start = lesson_periods[0].lesson.validity.date_start - date_end = lesson_periods[-1].lesson.validity.date_end - if ( - filter_dict["filter_date"] - and filter_dict.get("date_start") > date_start - and filter_dict.get("date_start") < date_end - ): - date_start = filter_dict.get("date_start") - if ( - filter_dict["filter_date"] - and filter_dict.get("date_end") < date_end - and filter_dict.get("date_end") > date_start - ): - date_end = filter_dict.get("date_end") - weeks = CalendarWeek.weeks_within(date_start, date_end) - - register_objects = [] - inherit_privileges_preference = get_site_preferences()[ - "alsijil__inherit_privileges_from_parent_group" - ] - for lesson_period in lesson_periods: - parent_group_owned_by_person = inherit_privileges_preference and ( - Group.objects.filter( - child_groups__in=Group.objects.filter(lessons__lesson_periods=lesson_period), - owners=filter_dict.get("person"), - ).exists() - ) - for week in weeks: - day = week[lesson_period.period.weekday] - - # Skip all lesson periods in holidays - if day in holiday_days: - continue - # Ensure that the lesson period is in filter range and validity range - if ( - lesson_period.lesson.validity.date_start - <= day - <= lesson_period.lesson.validity.date_end - ) and ( - not filter_dict.get("filter_date") - or (filter_dict.get("date_start") <= day <= filter_dict.get("date_end")) - ): - sub = lesson_period.get_substitution() - - # Skip lesson period if the person isn't a teacher, - # substitution teacher or, when the corresponding - # preference is switched on, owner of a parent group - # of this lesson period - if filter_dict.get("person") and ( - filter_dict.get("person") not in lesson_period.lesson.teachers.all() - and not sub - and not parent_group_owned_by_person - ): - continue - - teachers = lesson_period.teacher_names - if ( - filter_dict.get("subject") - and filter_dict.get("subject") != lesson_period.get_subject() - ): - continue - - # Filter matching documentations and annotate if they exist - filtered_documentations = list( - filter( - lambda d: d.week == week.week - and d.year == week.year - and d.lesson_period_id == lesson_period.pk, - documentations - if documentations is not None - else lesson_period.documentations.all(), - ) - ) - has_documentation = bool(filtered_documentations) - - if filter_dict.get( - "has_documentation" - ) is not None and has_documentation != filter_dict.get("has_documentation"): - continue - - # Build table entry - entry = { - "pk": f"lesson_period_{lesson_period.pk}_{week.year}_{week.week}", - "week": week, - "has_documentation": has_documentation, - "substitution": sub, - "register_object": lesson_period, - "date": date_format(day), - "date_sort": day, - "period": f"{lesson_period.period.period}.", - "period_sort": lesson_period.period.period, - "groups": lesson_period.lesson.group_names, - "teachers": teachers, - "subject": lesson_period.get_subject().name, - } - if has_documentation: - doc = filtered_documentations[0] - entry["topic"] = doc.topic - entry["homework"] = doc.homework - entry["group_note"] = doc.group_note - register_objects.append(entry) - return register_objects - - -def _generate_dicts_for_events_and_extra_lessons( - filter_dict: Dict[str, Any], - register_objects_start: Sequence[Union[Event, ExtraLesson]], - documentations: Optional[Iterable[LessonDocumentation]] = None, -) -> List[Dict[str, Any]]: - """Generate a list of dicts for use with ``RegisterObjectTable``.""" - register_objects = [] - for register_object in register_objects_start: - filtered_documentations = list( - filter( - lambda d: getattr(d, f"{register_object.label_}_id") == register_object.pk, - documentations - if documentations is not None - else register_object.documentations.all(), - ) - ) - has_documentation = bool(filtered_documentations) - - if filter_dict.get( - "has_documentation" - ) is not None and has_documentation != filter_dict.get("has_documentation"): - continue - - if isinstance(register_object, ExtraLesson): - day = date_format(register_object.date) - day_sort = register_object.date - period = f"{register_object.period.period}." - period_sort = register_object.period.period - else: - register_object.annotate_day(register_object.date_end) - day = ( - f"{date_format(register_object.date_start)}" - f"–{date_format(register_object.date_end)}" - ) - day_sort = register_object.date_start - period = f"{register_object.period_from.period}.–{register_object.period_to.period}." - period_sort = register_object.period_from.period - - # Build table entry - entry = { - "pk": f"{register_object.label_}_{register_object.pk}", - "has_documentation": has_documentation, - "register_object": register_object, - "date": day, - "date_sort": day_sort, - "period": period, - "period_sort": period_sort, - "groups": register_object.group_names, - "teachers": register_object.teacher_names, - "subject": register_object.subject.name - if isinstance(register_object, ExtraLesson) - else _("Event"), - } - if has_documentation: - doc = filtered_documentations[0] - entry["topic"] = doc.topic - entry["homework"] = doc.homework - entry["group_note"] = doc.group_note - register_objects.append(entry) - - return register_objects - - -def generate_list_of_all_register_objects(filter_dict: Dict[str, Any]) -> List[Dict[str, Any]]: - """Generate a list of all register objects. - - This list can be filtered using ``filter_dict``. The following keys are supported: - - ``school_term`` (defaults to the current school term) - - ``date_start`` and ``date_end`` (defaults to the last thirty days) - - ``groups`` and/or ``groups`` - - ``person`` - - ``subject`` - """ - # Always force a value for school term, start and end date so that queries won't get too big - initial_filter_data = FilterRegisterObjectForm.get_initial() - filter_dict["school_term"] = filter_dict.get("school_term", initial_filter_data["school_term"]) - - # If there is not school year at all, there are definitely no data. - if not filter_dict["school_term"]: - return [] - - filter_dict["date_start"] = filter_dict.get("date_start", initial_filter_data["date_start"]) - filter_dict["date_end"] = filter_dict.get("date_end", initial_filter_data["date_end"]) - filter_dict["filter_date"] = bool(filter_dict.get("date_start")) and bool( - filter_dict.get("date_end") - ) - - # Get all holidays in the selected school term to sort all data in holidays out - holidays = Holiday.objects.within_dates( - filter_dict["school_term"].date_start, filter_dict["school_term"].date_end - ) - holiday_days = holidays.get_all_days() - - lesson_periods = _filter_register_objects_by_dict( - filter_dict, - LessonPeriod.objects.order_by("lesson__validity__date_start"), - LessonPeriod.label_, - ) - events = _filter_register_objects_by_dict( - filter_dict, Event.objects.exclude_holidays(holidays), Event.label_ - ) - extra_lessons = _filter_register_objects_by_dict( - filter_dict, ExtraLesson.objects.exclude_holidays(holidays), ExtraLesson.label_ - ) - - # Prefetch documentations for all register objects and substitutions for all lesson periods - # in order to prevent extra queries - documentations = LessonDocumentation.objects.not_empty().filter( - Q(event__in=events) - | Q(extra_lesson__in=extra_lessons) - | Q(lesson_period__in=lesson_periods) - ) - - if lesson_periods: - register_objects = _generate_dicts_for_lesson_periods( - filter_dict, lesson_periods, documentations, holiday_days - ) - register_objects += _generate_dicts_for_events_and_extra_lessons( - filter_dict, list(events) + list(extra_lessons), documentations - ) - - # Sort table entries by date and period and configure table - register_objects = sorted(register_objects, key=itemgetter("date_sort", "period_sort")) - return register_objects - return [] def get_absence_reason_tag(): diff --git a/aleksis/apps/alsijil/util/predicates.py b/aleksis/apps/alsijil/util/predicates.py index 5966a7459bc8a32f7e7468f20de2e7da0044441e..fd49ca4a2f044064e6739d8f53ed27e8eb0e2539 100644 --- a/aleksis/apps/alsijil/util/predicates.py +++ b/aleksis/apps/alsijil/util/predicates.py @@ -12,7 +12,7 @@ from aleksis.core.models import Group, Person from aleksis.core.util.core_helpers import get_site_preferences from aleksis.core.util.predicates import check_object_permission -from ..models import Documentation, NewPersonalNote, PersonalNote +from ..models import Documentation, NewPersonalNote @predicate @@ -198,7 +198,7 @@ def has_personal_note_group_perm(perm: str): name = f"has_personal_note_person_or_group_perm:{perm}" @predicate(name) - def fn(user: User, obj: PersonalNote) -> bool: + def fn(user: User, obj) -> bool: if hasattr(obj, "person"): groups = obj.person.member_of.all() for group in groups: @@ -210,7 +210,7 @@ def has_personal_note_group_perm(perm: str): @predicate -def is_own_personal_note(user: User, obj: PersonalNote) -> bool: +def is_own_personal_note(user: User, obj) -> bool: """Predicate for users referred to in a personal note. Checks whether the user referred to in a PersonalNote is the active user. @@ -231,7 +231,7 @@ def is_parent_group_owner(user: User, obj: Group) -> bool: @predicate -def is_personal_note_lesson_teacher(user: User, obj: PersonalNote) -> bool: +def is_personal_note_lesson_teacher(user: User, obj) -> bool: """Predicate for teachers of a register object linked to a personal note. Checks whether the person linked to the user is a teacher @@ -245,7 +245,7 @@ def is_personal_note_lesson_teacher(user: User, obj: PersonalNote) -> bool: @predicate -def is_personal_note_lesson_original_teacher(user: User, obj: PersonalNote) -> bool: +def is_personal_note_lesson_original_teacher(user: User, ob) -> bool: """Predicate for teachers of a register object linked to a personal note. Checks whether the person linked to the user is a teacher @@ -265,7 +265,7 @@ def is_personal_note_lesson_original_teacher(user: User, obj: PersonalNote) -> b @predicate -def is_personal_note_lesson_parent_group_owner(user: User, obj: PersonalNote) -> bool: +def is_personal_note_lesson_parent_group_owner(user: User, obj) -> bool: """ Predicate for parent group owners of a lesson referred to in the lesson of a personal note. diff --git a/aleksis/apps/alsijil/views.py b/aleksis/apps/alsijil/views.py index 0395a8bfbc54fcab81dfea092f431045d15ae134..75f0e1b06d6dfe14380047fe8a6d1221109bcf21 100644 --- a/aleksis/apps/alsijil/views.py +++ b/aleksis/apps/alsijil/views.py @@ -1,36 +1,20 @@ -from contextlib import nullcontext -from copy import deepcopy -from datetime import datetime, timedelta -from typing import Any, Dict, Optional +from typing import Any, Dict -from django.apps import apps -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 -from django.http import Http404, HttpRequest, HttpResponse, HttpResponseNotFound -from django.shortcuts import get_object_or_404, redirect, render +from django.db.models import Q +from django.http import HttpRequest, HttpResponse +from django.shortcuts import get_object_or_404, redirect from django.urls import reverse, reverse_lazy from django.utils import timezone from django.utils.decorators import method_decorator from django.utils.http import url_has_allowed_host_and_scheme from django.utils.translation import gettext as _ -from django.views import View from django.views.decorators.cache import never_cache from django.views.generic import DetailView -import reversion -from calendarweek import CalendarWeek -from django_tables2 import RequestConfig, SingleTableView -from guardian.core import ObjectPermissionChecker -from guardian.shortcuts import get_objects_for_user +from django_tables2 import SingleTableView from reversion.views import RevisionMixin from rules.contrib.views import PermissionRequiredMixin, permission_required -from aleksis.apps.chronos.managers import TimetableType -from aleksis.apps.chronos.models import Event, ExtraLesson, Holiday, LessonPeriod, TimePeriod -from aleksis.apps.chronos.util.build import build_weekdays -from aleksis.apps.chronos.util.date import get_weeks_for_year, week_weekday_to_date from aleksis.core.decorators import pwa_cache from aleksis.core.mixins import ( AdvancedCreateView, @@ -38,594 +22,23 @@ from aleksis.core.mixins import ( AdvancedEditView, SuccessNextMixin, ) -from aleksis.core.models import Group, PDFFile, Person, SchoolTerm +from aleksis.core.models import Group, PDFFile from aleksis.core.util import messages from aleksis.core.util.celery_progress import render_progress_page -from aleksis.core.util.core_helpers import get_site_preferences, has_person, objectgetter_optional -from aleksis.core.util.predicates import check_global_permission +from aleksis.core.util.core_helpers import has_person, objectgetter_optional -from .filters import PersonalNoteFilter from .forms import ( AssignGroupRoleForm, - ExcuseTypeForm, - FilterRegisterObjectForm, GroupRoleAssignmentEditForm, GroupRoleForm, - LessonDocumentationForm, - PersonalNoteFormSet, - PersonOverviewForm, - RegisterAbsenceForm, - RegisterObjectActionForm, - SelectForm, ) -from .models import ExcuseType, ExtraMark, GroupRole, GroupRoleAssignment, PersonalNote +from .models import GroupRole, GroupRoleAssignment from .tables import ( - ExcuseTypeTable, GroupRoleTable, - PersonalNoteTable, - RegisterObjectSelectTable, - RegisterObjectTable, ) from .tasks import generate_full_register_printout -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, -) - - -@pwa_cache -@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: - context = {} - - 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": - return redirect( - "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 - ): - return HttpResponseNotFound() - - if ( - 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" - ] and request.user.has_perm( - "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 - if not request.user.has_perm( - "alsijil.view_register_object_personalnote_rule", register_object - ): - persons = Person.objects.filter( - Q(pk=request.user.person.pk) | Q(member_of__in=request.user.person.owner_of.all()) - ).distinct() - else: - persons = Person.objects.all() - - persons_qs = register_object.get_personal_notes(persons, wanted_week).distinct() - - # 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( - "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( - "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) - - -@pwa_cache -@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: - context = {} - - wanted_week = CalendarWeek(year=year, week=week) if year and week else CalendarWeek() - instance = get_timetable_instance_by_pk(request, year, week, type_, id_) - 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) - - query_exists = True - 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) - extra_lessons = extra_lessons.filter_from_type(type_, instance) - - elif hasattr(request, "user") and hasattr(request.user, "person"): - 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) - ) - - 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) - - else: - query_exists = False - lesson_periods = None - events = None - extra_lessons = None - - # 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] - ) - 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) - - if request.method == "POST" and 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, - ) - - group = instance if type_ == TimetableType.GROUP else None - - # Group roles - show_group_roles = ( - group - and request.user.person.preferences["alsijil__group_roles_in_week_view"] - 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) - - extra_marks = ExtraMark.objects.all() - - if query_exists: - 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) - ) - else: - lesson_periods_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 - persons_qs = Person.objects.all() - - if not request.user.has_perm("alsijil.view_week_personalnote_rule", instance): - persons_qs = persons_qs.filter(pk=request.user.person.pk) - elif group: - persons_qs = ( - persons_qs.filter(member_of=group) - .filter(member_of__in=request.user.person.owner_of.all()) - .distinct() - ) - else: - persons_qs = ( - persons_qs.filter(member_of__in=groups) - .filter(member_of__in=request.user.person.owner_of.all()) - .distinct() - ) - - # 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 = ( - 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__tardiness"), - tardiness_count=Count( - "filtered_personal_notes", - filter=Q(filtered_personal_notes__tardiness__gt=0), - ), - ) - - for extra_mark in extra_marks: - persons_qs = persons_qs.annotate( - **{ - extra_mark.count_label: Count( - "filtered_personal_notes", - filter=Q(filtered_personal_notes__extra_marks=extra_mark), - ) - } - ) - - persons = [] - for person in persons_qs: - personal_notes = [] - 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) - else: - persons = None - - context["extra_marks"] = extra_marks - 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 - 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) - 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: - 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) - - -@pwa_cache @permission_required( "alsijil.view_full_register_rule", fn=objectgetter_optional(Group, None, False) ) @@ -665,436 +78,6 @@ def full_register_group(request: HttpRequest, id_: int) -> HttpResponse: ) -@pwa_cache -@permission_required("alsijil.view_my_students_rule") -def my_students(request: HttpRequest) -> HttpResponse: - context = {} - relevant_groups = ( - request.user.person.get_owner_groups_with_lessons() - .annotate(has_parents=Exists(Group.objects.filter(child_groups=OuterRef("pk")))) - .filter(members__isnull=False) - .order_by("has_parents", "name") - .prefetch_related("members") - .distinct() - ) - - # 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"), - ) - ).distinct() - 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["groups"] = new_groups - 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) - - -@pwa_cache -@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) - - -@method_decorator(pwa_cache, "dispatch") -class StudentsList(PermissionRequiredMixin, DetailView): - model = Group - template_name = "alsijil/class_register/students_list.html" - 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()) - .distinct() - ) - 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) - return context - - -@pwa_cache -@permission_required( - "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_) - 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) - - if request.user.has_perm("alsijil.view_person_overview_personalnote_rule", person): - allowed_personal_notes = person_personal_notes.all() - else: - 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) - context["unexcused_absences"] = unexcused_absences - - personal_notes = ( - allowed_personal_notes.not_empty() - .filter(Q(absent=True) | Q(tardiness__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"), - ), - 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"), - ), - ) - .order_by( - "-school_term_start", - "-order_year", - "-order_week", - "-order_weekday", - "order_period", - ) - .annotate_date_range() - .annotate_subject() - ) - - 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 - - used_filters = list(personal_note_filter_object.data.values()) - 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) - - 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) - and form.is_valid() - ): - with reversion.create_revision(): - reversion.set_user(request.user) - form.execute() - person.refresh_from_db() - context["action_form"] = form - - 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() - 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 - - stat.update( - personal_notes.filter(absent=True) - .exclude(excuse_type__count_as_absent=False) - .aggregate(absences_count=Count("absent")) - ) - stat.update( - personal_notes.filter(absent=True, excused=True) - .exclude(excuse_type__count_as_absent=False) - .aggregate(excused=Count("absent")) - ) - stat.update( - personal_notes.filter(absent=True, excused=True, excuse_type__isnull=True) - .exclude(excuse_type__count_as_absent=False) - .aggregate(excused_no_excuse_type=Count("absent")) - ) - stat.update( - personal_notes.filter(absent=True, excused=False).aggregate( - unexcused=Count("absent") - ) - ) - stat.update(personal_notes.aggregate(tardiness=Sum("tardiness"))) - stat.update( - personal_notes.filter(~Q(tardiness=0)).aggregate(tardiness_count=Count("tardiness")) - ) - - for extra_mark in extra_marks: - stat.update( - personal_notes.filter(extra_marks=extra_mark).aggregate( - **{extra_mark.count_label: Count("pk")} - ) - ) - - for excuse_type in excuse_types: - stat.update( - personal_notes.filter(absent=True, excuse_type=excuse_type).aggregate( - **{excuse_type.count_label: Count("absent")} - ) - ) - - stats.append((school_term, stat)) - context["stats"] = stats - - context["extra_marks"] = extra_marks - - # Build filter with own form and logic as django-filter can't work with different models - if request.user.person.preferences["alsijil__default_lesson_documentation_filter"]: - default_documentation = False - else: - default_documentation = None - - filter_form = FilterRegisterObjectForm( - request, request.GET or None, for_person=True, default_documentation=default_documentation - ) - filter_dict = ( - filter_form.cleaned_data - if filter_form.is_valid() - else {"has_documentation": default_documentation} - ) - filter_dict["person"] = person - context["filter_form"] = filter_form - - if person.is_teacher: - register_objects = generate_list_of_all_register_objects(filter_dict) - table = RegisterObjectTable(register_objects) - items_per_page = request.user.person.preferences[ - "alsijil__register_objects_table_items_per_page" - ] - RequestConfig(request, paginate={"per_page": items_per_page}).configure(table) - context["register_object_table"] = table - return render(request, "alsijil/class_register/person.html", context) - - -@never_cache -@permission_required("alsijil.register_absence_rule", fn=objectgetter_optional(Person)) -def register_absence(request: HttpRequest, id_: int = None) -> HttpResponse: - context = {} - - person = get_object_or_404(Person, pk=id_) if id_ else None - - register_absence_form = RegisterAbsenceForm( - request, request.POST or None, initial={"person": person} - ) - - if ( - request.method == "POST" - and register_absence_form.is_valid() - and request.user.has_perm("alsijil.register_absence_rule", person) - ): - confirmed = request.POST.get("confirmed", "0") == "1" - - # Get data from form - person = register_absence_form.cleaned_data["person"] - start_date = register_absence_form.cleaned_data["date_start"] - end_date = register_absence_form.cleaned_data["date_end"] - from_period = register_absence_form.cleaned_data["from_period"] - to_period = register_absence_form.cleaned_data["to_period"] - absent = register_absence_form.cleaned_data["absent"] - excused = register_absence_form.cleaned_data["excused"] - excuse_type = register_absence_form.cleaned_data["excuse_type"] - remarks = register_absence_form.cleaned_data["remarks"] - - # Mark person as absent - affected_count = 0 - delta = end_date - start_date - for i in range(delta.days + 1): - from_period_on_day = from_period if i == 0 else TimePeriod.period_min - to_period_on_day = to_period if i == delta.days else TimePeriod.period_max - day = start_date + timedelta(days=i) - - # Skip holidays if activated - if not get_site_preferences()["alsijil__allow_entries_in_holidays"]: - holiday = Holiday.on_day(day) - if holiday: - continue - - with reversion.create_revision() if confirmed else nullcontext(): - affected_count += person.mark_absent( - day, - from_period_on_day, - absent, - excused, - excuse_type, - remarks, - to_period_on_day, - dry_run=not confirmed, - ) - - if not confirmed: - # Show confirmation page - context = {} - context["affected_lessons"] = affected_count - context["person"] = person - context["form_data"] = register_absence_form.cleaned_data - context["form"] = register_absence_form - return render(request, "alsijil/absences/register_confirm.html", context) - else: - messages.success(request, _("The absence has been saved.")) - return redirect("overview_person", person.pk) - - context["person"] = person - context["register_absence_form"] = register_absence_form - - return render(request, "alsijil/absences/register.html", context) - - -@method_decorator(never_cache, name="dispatch") -class DeletePersonalNoteView(PermissionRequiredMixin, DetailView): - model = PersonalNote - template_name = "core/pages/delete.html" - permission_required = "alsijil.edit_personalnote_rule" - - def post(self, request, *args, **kwargs): - note = self.get_object() - with reversion.create_revision(): - reversion.set_user(request.user) - note.reset_values() - note.save() - messages.success(request, _("The personal note has been deleted.")) - return redirect("overview_person", note.person.pk) - - -@method_decorator(pwa_cache, "dispatch") -class ExcuseTypeListView(PermissionRequiredMixin, SingleTableView): - """Table of all excuse types.""" - - model = ExcuseType - table_class = ExcuseTypeTable - permission_required = "alsijil.view_excusetypes_rule" - template_name = "alsijil/excuse_type/list.html" - - -@method_decorator(never_cache, name="dispatch") -class ExcuseTypeCreateView(PermissionRequiredMixin, AdvancedCreateView): - """Create view for excuse types.""" - - model = ExcuseType - form_class = ExcuseTypeForm - permission_required = "alsijil.add_excusetype_rule" - template_name = "alsijil/excuse_type/create.html" - success_url = reverse_lazy("excuse_types") - success_message = _("The excuse type has been created.") - - -@method_decorator(never_cache, name="dispatch") -class ExcuseTypeEditView(PermissionRequiredMixin, AdvancedEditView): - """Edit view for excuse types.""" - - model = ExcuseType - form_class = ExcuseTypeForm - permission_required = "alsijil.edit_excusetype_rule" - template_name = "alsijil/excuse_type/edit.html" - success_url = reverse_lazy("excuse_types") - success_message = _("The excuse type has been saved.") - - -@method_decorator(never_cache, "dispatch") -class ExcuseTypeDeleteView(PermissionRequiredMixin, RevisionMixin, AdvancedDeleteView): - """Delete view for excuse types.""" - - model = ExcuseType - permission_required = "alsijil.delete_excusetype_rule" - template_name = "core/pages/delete.html" - success_url = reverse_lazy("excuse_types") - success_message = _("The excuse type has been deleted.") - - @method_decorator(pwa_cache, "dispatch") class GroupRoleListView(PermissionRequiredMixin, SingleTableView): """Table of all group roles.""" @@ -1261,52 +244,3 @@ class GroupRoleAssignmentDeleteView( def get_success_url(self) -> str: pk = self.object.groups.first().pk return reverse("assigned_group_roles", args=[pk]) - - -@method_decorator(pwa_cache, "dispatch") -class AllRegisterObjectsView(PermissionRequiredMixin, View): - """Provide overview of all register objects for coordinators.""" - - permission_required = "alsijil.view_register_objects_list_rule" - - def get_context_data(self, request): - context = {} - # Filter selectable groups by permissions - groups = Group.objects.all() - if not check_global_permission(request.user, "alsijil.view_full_register"): - allowed_groups = get_objects_for_user( - self.request.user, "core.view_full_register_group", Group - ).values_list("pk", flat=True) - groups = groups.filter(Q(parent_groups__in=allowed_groups) | Q(pk__in=allowed_groups)) - - # Build filter with own form and logic as django-filter can't work with different models - filter_form = FilterRegisterObjectForm( - request, request.GET or None, for_person=False, groups=groups - ) - filter_dict = filter_form.cleaned_data if filter_form.is_valid() else {} - filter_dict["groups"] = groups - context["filter_form"] = filter_form - - register_objects = generate_list_of_all_register_objects(filter_dict) - - self.action_form = RegisterObjectActionForm(request, register_objects, request.POST or None) - context["action_form"] = self.action_form - - if register_objects: - self.table = RegisterObjectSelectTable(register_objects) - items_per_page = request.user.person.preferences[ - "alsijil__register_objects_table_items_per_page" - ] - RequestConfig(request, paginate={"per_page": items_per_page}).configure(self.table) - context["table"] = self.table - return context - - def get(self, request: HttpRequest) -> HttpResponse: - context = self.get_context_data(request) - return render(request, "alsijil/class_register/all_objects.html", context) - - def post(self, request: HttpRequest): - context = self.get_context_data(request) - if self.action_form.is_valid(): - self.action_form.execute() - return render(request, "alsijil/class_register/all_objects.html", context)