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/checks.py b/aleksis/apps/alsijil/checks.py new file mode 100644 index 0000000000000000000000000000000000000000..310e5d2dbc17cbe95d89f81a4aa83fb0ef7f25b0 --- /dev/null +++ b/aleksis/apps/alsijil/checks.py @@ -0,0 +1,157 @@ +import logging +from datetime import datetime, time +from typing import TYPE_CHECKING + +from django.db.models.query_utils import Q +from django.utils.translation import gettext as _ + +from aleksis.apps.chronos.models import LessonEvent +from aleksis.core.data_checks import DataCheck, IgnoreSolveOption, SolveOption + +if TYPE_CHECKING: + from aleksis.core.models import DataCheckResult + + +class DeleteRelatedObjectSolveOption(SolveOption): + name = "delete" + verbose_name = _("Delete object") + + @classmethod + def solve(cls, check_result: "DataCheckResult"): + check_result.related_object.delete() + check_result.delete() + + +class SetGroupsWithCurrentGroupsSolveOption(SolveOption): + name = "set_groups_of_person" + verbose_name = _("Set current groups") + + @classmethod + def solve(cls, check_result: "DataCheckResult"): + person = check_result.related_object.person + check_result.related_object.groups_of_person.set(person.member_of.all()) + check_result.delete() + + +class NoParticipationStatusesPersonalNotesInCancelledLessonsDataCheck(DataCheck): + name = "no_personal_notes_participation_statuses_in_cancelled_lessons" + verbose_name = _( + "Ensure that there are no participation statuses and personal notes in cancelled lessons" + ) + problem_name = _("The participation status or personal note is related to a cancelled lesson.") + solve_options = { + DeleteRelatedObjectSolveOption.name: DeleteRelatedObjectSolveOption, + IgnoreSolveOption.name: IgnoreSolveOption, + } + + @classmethod + def check_data(cls): + from .models import NewPersonalNote, ParticipationStatus + + participation_statuses = ParticipationStatus.objects.filter( + related_documentation__amends__in=LessonEvent.objects.filter(cancelled=True) + ) + personal_notes = NewPersonalNote.objects.filter( + documentation__amends__in=LessonEvent.objects.filter(cancelled=True) + ) + + for status in participation_statuses: + logging.info(f"Check participation status {status}") + cls.register_result(status) + + for note in personal_notes: + logging.info(f"Check personal note {note}") + cls.register_result(note) + + +class NoGroupsOfPersonsSetInParticipationStatusesDataCheck(DataCheck): + name = "no_groups_of_persons_set_in_participation_statuses" + verbose_name = _("Ensure that 'groups_of_person' is set for every participation status") + problem_name = _("The participation status has no group in 'groups_of_person'.") + solve_options = { + SetGroupsWithCurrentGroupsSolveOption.name: SetGroupsWithCurrentGroupsSolveOption, + DeleteRelatedObjectSolveOption.name: DeleteRelatedObjectSolveOption, + IgnoreSolveOption.name: IgnoreSolveOption, + } + + @classmethod + def check_data(cls): + from .models import ParticipationStatus + + participation_statuses = ParticipationStatus.objects.filter(groups_of_person__isnull=True) + + for status in participation_statuses: + logging.info(f"Check participation status {status}") + cls.register_result(status) + + +class DocumentationOnHolidaysDataCheck(DataCheck): + """Checks for documentation objects on holidays.""" + + name = "documentation_on_holidays" + verbose_name = _("Ensure that there are no documentations on holidays") + problem_name = _("The documentation is on holidays.") + solve_options = { + DeleteRelatedObjectSolveOption.name: DeleteRelatedObjectSolveOption, + IgnoreSolveOption.name: IgnoreSolveOption, + } + + @classmethod + def check_data(cls): + from aleksis.apps.chronos.models import Holiday + + from .models import Documentation + + holidays = Holiday.objects.all() + + q = Q(pk__in=[]) + for holiday in holidays: + q = q | Q( + datetime_start__gte=datetime.combine(holiday.date_start, time.min), + datetime_end__lte=datetime.combine(holiday.date_end, time.max), + ) + documentations = Documentation.objects.filter(q) + + for doc in documentations: + logging.info(f"Documentation {doc} is on holidays") + cls.register_result(doc) + + +class ParticipationStatusPersonalNoteOnHolidaysDataCheck(DataCheck): + """Checks for participation status and personal note objects on holidays.""" + + name = "participation_status_personal_note_on_holidays" + verbose_name = _( + "Ensure that there are no participation statuses or personal notes on holidays" + ) + problem_name = _("The participation status or personal note is on holidays.") + solve_options = { + DeleteRelatedObjectSolveOption.name: DeleteRelatedObjectSolveOption, + IgnoreSolveOption.name: IgnoreSolveOption, + } + + @classmethod + def check_data(cls): + from aleksis.apps.chronos.models import Holiday + + from .models import NewPersonalNote, ParticipationStatus + + holidays = Holiday.objects.all() + + q = Q(pk__in=[]) + for holiday in holidays: + q = q | Q( + datetime_start__gte=datetime.combine(holiday.date_start, time.min), + datetime_end__lte=datetime.combine(holiday.date_end, time.max), + ) + + participation_statuses = ParticipationStatus.objects.filter(q) + personal_notes = NewPersonalNote.objects.filter(q) + + for status in participation_statuses: + logging.info(f"Participation status {status} is on holidays") + cls.register_result(status) + + for note in personal_notes: + logging.info(f"Personal note {note} is on holidays") + cls.register_result(note) diff --git a/aleksis/apps/alsijil/data_checks.py b/aleksis/apps/alsijil/data_checks.py deleted file mode 100644 index 87e703a0cb4fb17cff6be360cbea52895a6fa775..0000000000000000000000000000000000000000 --- a/aleksis/apps/alsijil/data_checks.py +++ /dev/null @@ -1,181 +0,0 @@ -import logging -from typing import TYPE_CHECKING - -from django.db.models import F -from django.db.models.query_utils import Q -from django.utils.translation import gettext as _ - -from aleksis.core.data_checks import DataCheck, IgnoreSolveOption, SolveOption - -if TYPE_CHECKING: - from aleksis.core.models import DataCheckResult - - -class DeleteRelatedObjectSolveOption(SolveOption): - name = "delete" - verbose_name = _("Delete object") - - @classmethod - def solve(cls, check_result: "DataCheckResult"): - check_result.related_object.delete() - check_result.delete() - - -class SetGroupsWithCurrentGroupsSolveOption(SolveOption): - name = "set_groups_of_person" - verbose_name = _("Set current groups") - - @classmethod - def solve(cls, check_result: "DataCheckResult"): - person = check_result.related_object.person - check_result.related_object.groups_of_person.set(person.member_of.all()) - check_result.delete() - - -class ResetPersonalNoteSolveOption(SolveOption): - name = "reset_personal_note" - verbose_name = _("Reset personal note to defaults") - - @classmethod - def solve(cls, check_result: "DataCheckResult"): - note = check_result.related_object - note.reset_values() - note.save() - check_result.delete() - - -class NoPersonalNotesInCancelledLessonsDataCheck(DataCheck): - name = "no_personal_notes_in_cancelled_lessons" - verbose_name = _("Ensure that there are no personal notes in cancelled lessons") - problem_name = _("The personal note is related to a cancelled lesson.") - solve_options = { - DeleteRelatedObjectSolveOption.name: DeleteRelatedObjectSolveOption, - IgnoreSolveOption.name: IgnoreSolveOption, - } - - @classmethod - def check_data(cls): - from .models import PersonalNote - - personal_notes = ( - PersonalNote.objects.not_empty() - .filter( - lesson_period__substitutions__cancelled=True, - lesson_period__substitutions__week=F("week"), - lesson_period__substitutions__year=F("year"), - ) - .prefetch_related("lesson_period", "lesson_period__substitutions") - ) - - for note in personal_notes: - logging.info(f"Check personal note {note}") - cls.register_result(note) - - -class NoGroupsOfPersonsSetInPersonalNotesDataCheck(DataCheck): - name = "no_groups_of_persons_set_in_personal_notes" - verbose_name = _("Ensure that 'groups_of_person' is set for every personal note") - problem_name = _("The personal note has no group in 'groups_of_person'.") - solve_options = { - SetGroupsWithCurrentGroupsSolveOption.name: SetGroupsWithCurrentGroupsSolveOption, - DeleteRelatedObjectSolveOption.name: DeleteRelatedObjectSolveOption, - IgnoreSolveOption.name: IgnoreSolveOption, - } - - @classmethod - def check_data(cls): - from .models import PersonalNote - - personal_notes = PersonalNote.objects.filter(groups_of_person__isnull=True) - - for note in personal_notes: - logging.info(f"Check personal note {note}") - cls.register_result(note) - - -class LessonDocumentationOnHolidaysDataCheck(DataCheck): - """Checks for lesson documentation objects on holidays. - - This ignores empty lesson documentation as they are created by default. - """ - - name = "lesson_documentation_on_holidays" - verbose_name = _("Ensure that there are no filled out lesson documentations on holidays") - problem_name = _("The lesson documentation is on holidays.") - solve_options = { - DeleteRelatedObjectSolveOption.name: DeleteRelatedObjectSolveOption, - IgnoreSolveOption.name: IgnoreSolveOption, - } - - @classmethod - def check_data(cls): - from aleksis.apps.chronos.models import Holiday - - from .models import LessonDocumentation - - holidays = Holiday.objects.all() - - documentations = LessonDocumentation.objects.not_empty().annotate_date_range() - - q = Q(pk__in=[]) - for holiday in holidays: - q = q | Q(day_end__gte=holiday.date_start, day_start__lte=holiday.date_end) - documentations = documentations.filter(q) - - for doc in documentations: - logging.info(f"Lesson documentation {doc} is on holidays") - cls.register_result(doc) - - -class PersonalNoteOnHolidaysDataCheck(DataCheck): - """Checks for personal note objects on holidays. - - This ignores empty personal notes as they are created by default. - """ - - name = "personal_note_on_holidays" - verbose_name = _("Ensure that there are no filled out personal notes on holidays") - problem_name = _("The personal note is on holidays.") - solve_options = { - DeleteRelatedObjectSolveOption.name: DeleteRelatedObjectSolveOption, - IgnoreSolveOption.name: IgnoreSolveOption, - } - - @classmethod - def check_data(cls): - from aleksis.apps.chronos.models import Holiday - - from .models import PersonalNote - - holidays = Holiday.objects.all() - - personal_notes = PersonalNote.objects.not_empty().annotate_date_range() - - q = Q(pk__in=[]) - for holiday in holidays: - q = q | Q(day_end__gte=holiday.date_start, day_start__lte=holiday.date_end) - personal_notes = personal_notes.filter(q) - - 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..983a29b8d73196278afc3de7b1e25f484e62f02f 100644 --- a/aleksis/apps/alsijil/managers.py +++ b/aleksis/apps/alsijil/managers.py @@ -1,141 +1,21 @@ 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 -from aleksis.apps.chronos.managers import DateRangeQuerySetMixin -from aleksis.core.managers import AlekSISBaseManagerWithoutMigrations, RecurrencePolymorphicManager +from aleksis.core.managers import ( + AlekSISBaseManagerWithoutMigrations, + RecurrencePolymorphicManager, +) 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 @@ -164,7 +44,7 @@ class GroupRoleAssignmentManager(AlekSISBaseManagerWithoutMigrations): pass -class GroupRoleAssignmentQuerySet(DateRangeQuerySetMixin, QuerySet): +class GroupRoleAssignmentQuerySet(QuerySet): def within_dates(self, start: date, end: date): """Filter for all role assignments within a date range.""" return self.filter( diff --git a/aleksis/apps/alsijil/migrations/0001_initial.py b/aleksis/apps/alsijil/migrations/0001_initial.py index 344efdac0df75135647a3093ee20b8d228570b22..faa5eab2c749d234ccadaf31eeff97bb448dbc8e 100644 --- a/aleksis/apps/alsijil/migrations/0001_initial.py +++ b/aleksis/apps/alsijil/migrations/0001_initial.py @@ -41,7 +41,6 @@ class Migration(migrations.Migration): models.CharField( max_length=30, unique=True, - validators=[aleksis.apps.alsijil.models.isidentifier], verbose_name="Identifier", ), ), diff --git a/aleksis/apps/alsijil/migrations/0007_personal_note_lesson_documentation_year.py b/aleksis/apps/alsijil/migrations/0007_personal_note_lesson_documentation_year.py index 4cf8743b0ba5a2e16e0e1dcb491fe49b72dbb9c4..133cff82fb04e5a04e31dfe16f3efa58cc7f2133 100644 --- a/aleksis/apps/alsijil/migrations/0007_personal_note_lesson_documentation_year.py +++ b/aleksis/apps/alsijil/migrations/0007_personal_note_lesson_documentation_year.py @@ -1,8 +1,7 @@ # Generated by Django 3.0.9 on 2020-08-15 09:39 from django.db import migrations, models - -import aleksis.apps.chronos.util.date +from django.utils import timezone def migrate_data(apps, schema_editor): @@ -39,7 +38,7 @@ class Migration(migrations.Migration): model_name="lessondocumentation", name="year", field=models.IntegerField( - default=aleksis.apps.chronos.util.date.get_current_year, + default=lambda: timezone.now().year, verbose_name="Year", ), ), @@ -47,7 +46,7 @@ class Migration(migrations.Migration): model_name="personalnote", name="year", field=models.IntegerField( - default=aleksis.apps.chronos.util.date.get_current_year, + default=lambda: timezone.now().year, verbose_name="Year", ), ), diff --git a/aleksis/apps/alsijil/migrations/0009_group_roles.py b/aleksis/apps/alsijil/migrations/0009_group_roles.py index 78f6f3666cb385a10e6d7101738e66b987fc68f2..ce978b3833b2c9b961c237ef24a4873710b5b0fd 100644 --- a/aleksis/apps/alsijil/migrations/0009_group_roles.py +++ b/aleksis/apps/alsijil/migrations/0009_group_roles.py @@ -44,6 +44,6 @@ class Migration(migrations.Migration): 'verbose_name': 'Group role assignment', 'verbose_name_plural': 'Group role assignments', }, - bases=(aleksis.apps.chronos.managers.GroupPropertiesMixin, models.Model), + bases=(models.Model,), ), ] diff --git a/aleksis/apps/alsijil/migrations/0010_events_extra_lessons.py b/aleksis/apps/alsijil/migrations/0010_events_extra_lessons.py index 1c3bf9ec00c3d3242fd17cbc2ef60d1b097a4458..39878398d3aae13a0c0f870f4fb8f34d88c1450e 100644 --- a/aleksis/apps/alsijil/migrations/0010_events_extra_lessons.py +++ b/aleksis/apps/alsijil/migrations/0010_events_extra_lessons.py @@ -1,6 +1,5 @@ # Generated by Django 3.1.5 on 2021-01-10 15:48 -import aleksis.apps.chronos.util.date from django.db import migrations, models import django.db.models.deletion diff --git a/aleksis/apps/alsijil/migrations/0024_check_new_models.py b/aleksis/apps/alsijil/migrations/0024_check_new_models.py new file mode 100644 index 0000000000000000000000000000000000000000..da52a87226e6591939b5888c37667ea79ab7f47f --- /dev/null +++ b/aleksis/apps/alsijil/migrations/0024_check_new_models.py @@ -0,0 +1,27 @@ +from django.db import migrations, models + +from django.apps import apps as global_apps + +def check_for_migration(apps, schema_editor): + if global_apps.is_installed('aleksis.apps.lesrooster'): + return + + ExcuseType = apps.get_model('alsijil', 'ExcuseType') + PersonalNote = apps.get_model('alsijil', 'PersonalNote') + LessonDocumentation = apps.get_model('alsijil', 'LessonDocumentation') + + model_types = [ExcuseType, PersonalNote, LessonDocumentation] + + for model in model_types: + if model.objects.exists(): + raise RuntimeError("You have legacy data. Please install AlekSIS-App-Lesrooster to migrate them.") + +class Migration(migrations.Migration): + + dependencies = [ + ('alsijil', '0023_add_tardiness_and_rework_constraints'), + ] + + operations = [ + migrations.RunPython(check_for_migration), + ] diff --git a/aleksis/apps/alsijil/migrations/0025_remove_old_models.py b/aleksis/apps/alsijil/migrations/0025_remove_old_models.py new file mode 100644 index 0000000000000000000000000000000000000000..3093767e6a32330540ea6e90ea398bca03eeb7c6 --- /dev/null +++ b/aleksis/apps/alsijil/migrations/0025_remove_old_models.py @@ -0,0 +1,62 @@ +# Generated by Django 5.0.8 on 2024-08-06 16:18 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('alsijil', '0024_check_new_models'), + ] + + operations = [ + migrations.RemoveField( + model_name='personalnote', + name='excuse_type', + ), + migrations.RemoveField( + model_name='lessondocumentation', + name='event', + ), + migrations.RemoveField( + model_name='lessondocumentation', + name='extra_lesson', + ), + migrations.RemoveField( + model_name='lessondocumentation', + name='lesson_period', + ), + migrations.RemoveField( + model_name='personalnote', + name='event', + ), + migrations.RemoveField( + model_name='personalnote', + name='extra_lesson', + ), + migrations.RemoveField( + model_name='personalnote', + name='extra_marks', + ), + migrations.RemoveField( + model_name='personalnote', + name='groups_of_person', + ), + migrations.RemoveField( + model_name='personalnote', + name='lesson_period', + ), + migrations.RemoveField( + model_name='personalnote', + name='person', + ), + migrations.DeleteModel( + name='ExcuseType', + ), + migrations.DeleteModel( + name='LessonDocumentation', + ), + migrations.DeleteModel( + name='PersonalNote', + ), + ] 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..fb9e57c40fa66ce75b93e11c451cb87a6b21f69c 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,27 @@ 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.util.format import format_m2m +from aleksis.apps.chronos.models import LessonEvent 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. @@ -517,7 +122,7 @@ class Documentation(CalendarEvent): start_datetime = CalendarEvent.value_start_datetime(self) end_datetime = CalendarEvent.value_end_datetime(self) return ( - f"{format_m2m(self.get_groups())} {self.get_subject()}" + f"{','.join([str(g) for g in self.get_groups()])} {self.get_subject()}" + f" {start_datetime} - {end_datetime}" ) @@ -950,7 +555,7 @@ class GroupRole(ExtensibleModel): return reverse("edit_group_role", args=[self.id]) -class GroupRoleAssignment(GroupPropertiesMixin, ExtensibleModel): +class GroupRoleAssignment(ExtensibleModel): objects = GroupRoleAssignmentManager.from_queryset(GroupRoleAssignmentQuerySet)() role = models.ForeignKey( diff --git a/aleksis/apps/alsijil/preferences.py b/aleksis/apps/alsijil/preferences.py index 74ed22802286eaeccc4d92b6c1e255364c3503b2..b0d8fbc42c9227e32c227bf3ac10ef9c1635fba6 100644 --- a/aleksis/apps/alsijil/preferences.py +++ b/aleksis/apps/alsijil/preferences.py @@ -1,122 +1,19 @@ -from django.core.exceptions import ValidationError from django.utils.translation import gettext_lazy as _ from dynamic_preferences.preferences import Section from dynamic_preferences.types import ( BooleanPreference, ChoicePreference, - IntegerPreference, ModelChoicePreference, ModelMultipleChoicePreference, ) from aleksis.core.models import GroupType -from aleksis.core.registries import person_preferences_registry, site_preferences_registry +from aleksis.core.registries import site_preferences_registry alsijil = Section("alsijil", verbose_name=_("Class register")) -@site_preferences_registry.register -class BlockPersonalNotesForCancelled(BooleanPreference): - section = alsijil - name = "block_personal_notes_for_cancelled" - default = True - verbose_name = _("Block adding personal notes for cancelled lessons") - - -@site_preferences_registry.register -class ViewOwnPersonalNotes(BooleanPreference): - section = alsijil - name = "view_own_personal_notes" - default = True - verbose_name = _("Allow users to view their own personal notes") - - -@site_preferences_registry.register -class RegisterAbsenceAsPrimaryGroupOwner(BooleanPreference): - section = alsijil - name = "register_absence_as_primary_group_owner" - default = True - verbose_name = _( - "Allow primary group owners to register future absences for students in their groups" - ) - - -@site_preferences_registry.register -class InheritPrivilegesFromParentGroup(BooleanPreference): - section = alsijil - name = "inherit_privileges_from_parent_group" - default = True - verbose_name = _( - "Grant the owner of a parent group the same privileges " - "as the owners of the respective child groups" - ) - - -@site_preferences_registry.register -class EditLessonDocumentationAsOriginalTeacher(BooleanPreference): - section = alsijil - name = "edit_lesson_documentation_as_original_teacher" - default = True - verbose_name = _("Allow original teachers to edit their lessons although they are substituted") - - -@site_preferences_registry.register -class CarryOverDataToNextPeriods(BooleanPreference): - section = alsijil - name = "carry_over_next_periods" - default = True - verbose_name = _( - "Carry over data from first lesson period to the " - "following lesson periods in lessons over multiple periods" - ) - help_text = _("This will carry over data only if the data in the following periods are empty.") - - -@site_preferences_registry.register -class AllowCarryOverLessonDocumentationToCurrentWeek(BooleanPreference): - section = alsijil - name = "allow_carry_over_same_week" - default = False - verbose_name = _( - "Allow carrying over data from any lesson period to all other lesson \ - periods with the same lesson and in the same week" - ) - help_text = _( - "This will carry over data only if the data in the aforementioned periods are empty." - ) - - -@site_preferences_registry.register -class CarryOverPersonalNotesToNextPeriods(BooleanPreference): - section = alsijil - name = "carry_over_personal_notes" - default = True - verbose_name = _("Carry over personal notes to all following lesson periods on the same day.") - - -@site_preferences_registry.register -class AllowOpenPeriodsOnSameDay(BooleanPreference): - section = alsijil - name = "open_periods_same_day" - default = False - verbose_name = _( - "Allow teachers to open lesson periods on the " - "same day and not just at the beginning of the period" - ) - help_text = _( - "Lessons in the past are not affected by this setting, you can open them whenever you want." - ) - - -@site_preferences_registry.register -class AllowEntriesInHolidays(BooleanPreference): - section = alsijil - name = "allow_entries_in_holidays" - default = False - verbose_name = _("Allow teachers to add data for lessons in holidays") - - @site_preferences_registry.register class GroupOwnersCanAssignRolesToParents(BooleanPreference): section = alsijil @@ -127,45 +24,6 @@ class GroupOwnersCanAssignRolesToParents(BooleanPreference): ) -@person_preferences_registry.register -class ShowGroupRolesInWeekView(BooleanPreference): - section = alsijil - name = "group_roles_in_week_view" - default = True - verbose_name = _("Show assigned group roles in week view") - help_text = _("Only week view of groups") - - -@person_preferences_registry.register -class ShowGroupRolesInLessonView(BooleanPreference): - section = alsijil - name = "group_roles_in_lesson_view" - default = True - verbose_name = _("Show assigned group roles in lesson view") - - -@person_preferences_registry.register -class RegisterObjectsTableItemsPerPage(IntegerPreference): - """Preference how many items are shown per page in ``RegisterObjectTable``.""" - - section = alsijil - name = "register_objects_table_items_per_page" - default = 100 - verbose_name = _("Items per page in lessons table") - - def validate(self, value): - if value < 1: - raise ValidationError(_("Each page must show at least one item.")) - - -@person_preferences_registry.register -class DefaultLessonDocumentationFilter(BooleanPreference): - section = alsijil - name = "default_lesson_documentation_filter" - default = True - verbose_name = _("Filter lessons by existence of their lesson documentation on default") - - @site_preferences_registry.register class AllowEditFutureDocumentations(ChoicePreference): """Time range for which documentations may be edited.""" diff --git a/aleksis/apps/alsijil/rules.py b/aleksis/apps/alsijil/rules.py index 5fe872e73c65d370bf5bc70ce3671899144d5ee7..602663b519a0680ca88de3813ecbc38df64579a8 100644 --- a/aleksis/apps/alsijil/rules.py +++ b/aleksis/apps/alsijil/rules.py @@ -20,9 +20,7 @@ from .util.predicates import ( can_view_documentation, can_view_participation_status, can_view_personal_note, - has_lesson_group_object_perm, has_person_group_object_perm, - has_personal_note_group_perm, is_course_group_owner, is_course_member, is_course_teacher, @@ -33,143 +31,10 @@ from .util.predicates import ( is_in_allowed_time_range_for_participation_status, is_lesson_event_group_owner, is_lesson_event_teacher, - is_lesson_original_teacher, - is_lesson_parent_group_owner, - is_lesson_participant, - is_lesson_teacher, - is_none, - is_own_personal_note, is_owner_of_any_group, is_parent_group_owner, is_person_group_owner, - is_person_primary_group_owner, - is_personal_note_lesson_original_teacher, - is_personal_note_lesson_parent_group_owner, - is_personal_note_lesson_teacher, - is_teacher, -) - -# View lesson -view_register_object_predicate = has_person & ( - is_none # View is opened as "Current lesson" - | is_lesson_teacher - | is_lesson_original_teacher - | is_lesson_participant - | is_lesson_parent_group_owner - | has_global_perm("alsijil.view_lesson") - | has_lesson_group_object_perm("core.view_week_class_register_group") -) -add_perm("alsijil.view_register_object_rule", view_register_object_predicate) - -# View lesson in menu -add_perm("alsijil.view_lesson_menu_rule", has_person) - -# View lesson personal notes -view_lesson_personal_notes_predicate = view_register_object_predicate & ( - ~is_lesson_participant - | is_lesson_teacher - | is_lesson_original_teacher - | ( - is_lesson_parent_group_owner - & is_site_preference_set("alsijil", "inherit_privileges_from_parent_group") - ) - | has_global_perm("alsijil.view_personalnote") - | has_lesson_group_object_perm("core.view_personalnote_group") -) -add_perm("alsijil.view_register_object_personalnote_rule", view_lesson_personal_notes_predicate) - -# Edit personal note -edit_lesson_personal_note_predicate = view_lesson_personal_notes_predicate & ( - is_lesson_teacher - | ( - is_lesson_original_teacher - & is_site_preference_set("alsijil", "edit_lesson_documentation_as_original_teacher") - ) - | ( - is_lesson_parent_group_owner - & is_site_preference_set("alsijil", "inherit_privileges_from_parent_group") - ) - | has_global_perm("alsijil.change_personalnote") - | has_lesson_group_object_perm("core.edit_personalnote_group") ) -add_perm("alsijil.edit_register_object_personalnote_rule", edit_lesson_personal_note_predicate) - -# View personal note -view_personal_note_predicate = has_person & ( - (is_own_personal_note & is_site_preference_set("alsijil", "view_own_personal_notes")) - | is_personal_note_lesson_teacher - | is_personal_note_lesson_original_teacher - | is_personal_note_lesson_parent_group_owner - | has_global_perm("alsijil.view_personalnote") - | has_personal_note_group_perm("core.view_personalnote_group") -) -add_perm("alsijil.view_personalnote_rule", view_personal_note_predicate) - -# Edit personal note -edit_personal_note_predicate = view_personal_note_predicate & ( - ~is_own_personal_note - & ~( - is_personal_note_lesson_original_teacher - | ~is_site_preference_set("alsijil", "edit_lesson_documentation_as_original_teacher") - ) - | ( - is_personal_note_lesson_parent_group_owner - | is_site_preference_set("alsijil", "inherit_privileges_from_parent_group") - ) - | has_global_perm("alsijil.view_personalnote") - | has_personal_note_group_perm("core.edit_personalnote_group") -) -add_perm("alsijil.edit_personalnote_rule", edit_personal_note_predicate) - -# View lesson documentation -view_lesson_documentation_predicate = view_register_object_predicate -add_perm("alsijil.view_lessondocumentation_rule", view_lesson_documentation_predicate) - -# Edit lesson documentation -edit_lesson_documentation_predicate = view_register_object_predicate & ( - is_lesson_teacher - | ( - is_lesson_original_teacher - & is_site_preference_set("alsijil", "edit_lesson_documentation_as_original_teacher") - ) - | ( - is_lesson_parent_group_owner - & is_site_preference_set("alsijil", "inherit_privileges_from_parent_group") - ) - | has_global_perm("alsijil.change_lessondocumentation") - | has_lesson_group_object_perm("core.edit_lessondocumentation_group") -) -add_perm("alsijil.edit_lessondocumentation_rule", edit_lesson_documentation_predicate) - -# View week overview -view_week_predicate = has_person & ( - is_current_person - | is_group_member - | is_group_owner - | ( - is_parent_group_owner - & is_site_preference_set("alsijil", "inherit_privileges_from_parent_group") - ) - | has_global_perm("alsijil.view_week") - | has_object_perm("core.view_week_class_register_group") -) -add_perm("alsijil.view_week_rule", view_week_predicate) - -# View week overview in menu -add_perm("alsijil.view_week_menu_rule", has_person) - -# View week personal notes -view_week_personal_notes_predicate = has_person & ( - (is_current_person & is_teacher) - | is_group_owner - | ( - is_parent_group_owner - & is_site_preference_set("alsijil", "inherit_privileges_from_parent_group") - ) - | has_global_perm("alsijil.view_personalnote") - | has_object_perm("core.view_personalnote_group") -) -add_perm("alsijil.view_week_personalnote_rule", view_week_personal_notes_predicate) # Register absence view_register_absence_predicate = has_person & ( @@ -197,86 +62,6 @@ view_full_register_predicate = has_person & ( ) add_perm("alsijil.view_full_register_rule", view_full_register_predicate) -# View students list -view_my_students_predicate = has_person & is_teacher -add_perm("alsijil.view_my_students_rule", view_my_students_predicate) - -# View groups list -view_my_groups_predicate = has_person & is_teacher -add_perm("alsijil.view_my_groups_rule", view_my_groups_predicate) - -# View students list -view_students_list_predicate = view_my_groups_predicate & ( - is_group_owner - | ( - is_parent_group_owner - & is_site_preference_set("alsijil", "inherit_privileges_from_parent_group") - ) - | has_global_perm("alsijil.view_personalnote") - | has_object_perm("core.view_personalnote_group") -) -add_perm("alsijil.view_students_list_rule", view_students_list_predicate) - -# View person overview -view_person_overview_predicate = has_person & ( - (is_current_person & is_site_preference_set("alsijil", "view_own_personal_notes")) - | is_person_group_owner -) -add_perm("alsijil.view_person_overview_rule", view_person_overview_predicate) - -# View person overview -view_person_overview_menu_predicate = has_person -add_perm("alsijil.view_person_overview_menu_rule", view_person_overview_menu_predicate) - -# View person overview personal notes -view_person_overview_personal_notes_predicate = view_person_overview_predicate & ( - (is_current_person & is_site_preference_set("alsijil", "view_own_personal_notes")) - | is_person_primary_group_owner - | has_global_perm("alsijil.view_personalnote") - | has_person_group_object_perm("core.view_personalnote_group") -) -add_perm( - "alsijil.view_person_overview_personalnote_rule", - view_person_overview_personal_notes_predicate, -) - -# Edit person overview personal notes -edit_person_overview_personal_notes_predicate = view_person_overview_predicate & ( - ~is_current_person - | has_global_perm("alsijil.change_personalnote") - | has_person_group_object_perm("core.edit_personalnote_group") -) -add_perm( - "alsijil.edit_person_overview_personalnote_rule", - edit_person_overview_personal_notes_predicate, -) - -# View person statistics on personal notes -view_person_statistics_personal_notes_predicate = view_person_overview_personal_notes_predicate -add_perm( - "alsijil.view_person_statistics_personalnote_rule", - view_person_statistics_personal_notes_predicate, -) - -# View excuse type list -view_excusetypes_predicate = has_person & has_global_perm("alsijil.view_excusetype") -add_perm("alsijil.view_excusetypes_rule", view_excusetypes_predicate) - -# Add excuse type -add_excusetype_predicate = view_excusetypes_predicate & has_global_perm("alsijil.add_excusetype") -add_perm("alsijil.add_excusetype_rule", add_excusetype_predicate) - -# Edit excuse type -edit_excusetype_predicate = view_excusetypes_predicate & has_global_perm( - "alsijil.change_excusetype" -) -add_perm("alsijil.edit_excusetype_rule", edit_excusetype_predicate) - -# Delete excuse type -delete_excusetype_predicate = view_excusetypes_predicate & has_global_perm( - "alsijil.delete_excusetype" -) -add_perm("alsijil.delete_excusetype_rule", delete_excusetype_predicate) # View extra mark list view_extramarks_predicate = has_person & has_global_perm("alsijil.view_extramark") @@ -327,17 +112,6 @@ view_assigned_group_roles_predicate = has_person & ( ) add_perm("alsijil.view_assigned_grouproles_rule", view_assigned_group_roles_predicate) -view_assigned_group_roles_register_object_predicate = has_person & ( - is_lesson_teacher - | is_lesson_original_teacher - | is_lesson_parent_group_owner - | has_global_perm("alsijil.assign_grouprole") -) -add_perm( - "alsijil.view_assigned_grouproles_for_register_object", - view_assigned_group_roles_register_object_predicate, -) - assign_group_role_person_predicate = has_person & ( is_person_group_owner | has_global_perm("alsijil.assign_grouprole") ) diff --git a/aleksis/apps/alsijil/static/css/alsijil/alsijil.css b/aleksis/apps/alsijil/static/css/alsijil/alsijil.css deleted file mode 100644 index a30fb99bb8b239ca38f7f4d627329594387741c6..0000000000000000000000000000000000000000 --- a/aleksis/apps/alsijil/static/css/alsijil/alsijil.css +++ /dev/null @@ -1,60 +0,0 @@ -table.datatable a { - color: inherit !important; -} - -table a.tr-link { - display: block; - width: inherit; - height: inherit; -} - -.collapsible-icon-right { - align-self: end; - flex-grow: 100; - text-align: right !important; -} - -@media only screen and (min-width: 1201px) { - .hide-on-extra-large-only { - display: none; - } -} - -@media only screen and (max-width: 1200px) { - .show-on-extra-large { - display: none; - } -} - -@media only screen and (max-width: 600px) { - .collection .collection-item.avatar { - padding-left: 20px; - } - .collection .collection-item.avatar:not(.circle-clipper) > .circle { - position: relative; - margin-bottom: 10px; - } -} - -.collapsible li .show-on-active { - display: none; -} - -.collapsible li.active .show-on-active { - display: block; -} - -th.chip-height { - height: 67px; - line-height: 2.2; -} - -.collection-item.chip-height { - height: 52px; - line-height: 2.2; -} - -li.collection-item.button-height { - height: 58px; - line-height: 2.5; -} diff --git a/aleksis/apps/alsijil/static/css/alsijil/lesson.css b/aleksis/apps/alsijil/static/css/alsijil/lesson.css deleted file mode 100644 index 3ca0427a81bba0793b78d41d884a5810f0080f74..0000000000000000000000000000000000000000 --- a/aleksis/apps/alsijil/static/css/alsijil/lesson.css +++ /dev/null @@ -1,140 +0,0 @@ -.alsijil-check-box { - margin-right: 10px; -} - -.alsijil-check-box [type="checkbox"] { - padding-left: 30px; -} - -.alsijil-lesson-cancelled { - text-decoration: line-through; -} - -.alsijil-tardiness-text { - vertical-align: super; -} - -@media only screen and (max-width: 992px) { - .no-mobile-card { - border: unset; - padding: unset; - margin: unset; - box-shadow: unset; - } - .no-mobile-card .card-content { - padding: unset; - } - table.alsijil-table.horizontal-on-small { - display: block; - max-width: calc(100vw - 40px); - } - table.alsijil-table.horizontal-on-small thead { - display: none; - } - table.alsijil-table.horizontal-on-small tbody { - overflow-x: scroll; - display: flex; - column-gap: 1rem; - flex-wrap: nowrap; - align-items: stretch; - scroll-snap-type: x proximity; - } - - table.alsijil-table.horizontal-on-small tr { - flex-basis: min(75vw, 400px); - flex-shrink: 0; - flex-grow: 1; - border-radius: 8px; - display: flex; - flex-direction: column; - justify-content: space-between; - scroll-snap-align: center; - transition: all 0.5s; - margin: 0.5rem 0 1rem 0; - background-color: #fff !important; - box-shadow: - 0 2px 2px 0 rgba(0, 0, 0, 0.14), - 0 3px 1px -2px rgba(0, 0, 0, 0.12), - 0 1px 5px 0 rgba(0, 0, 0, 0.2); - padding: 24px; - } - table.alsijil-table.horizontal-on-small tr:first-of-type { - margin-inline-start: 0.4rem; - -moz-margin-start: 0.4rem; - -webkit-margin-start: 0.4rem; - } - - table.alsijil-table.horizontal-on-small tr:last-of-type { - margin-inline-end: 0.4rem; - -moz-margin-end: 0.4rem; - -webkit-margin-end: 0.4rem; - } - table.alsijil-table.horizontal-on-small td.center-align { - text-align: left; - } - table.alsijil-table.horizontal-on-small .person-name { - font-size: 24px; - font-weight: 300; - display: block; - line-height: 32px; - margin-bottom: 8px; - } -} - -.alsijil-time-head, -.alsijil-object-head { - display: block; -} - -.alsijil-time-head { - font-size: 2rem; - line-height: 1.1; -} - -.alsijil-object-head { - font-size: 3rem; -} - -@media only screen and (max-width: 600px) { - .alsijil-time-head { - font-size: 1.5rem; - } - - .alsijil-object-head { - font-size: 2.2rem; - line-height: 1.4; - } -} - -.alsijil-nav { - line-height: 36px; -} - -.alsijil-header-nav-button { - height: 66px; - padding: 0; -} - -.alsijil-header-nav-button.left { - margin-right: 5px; -} - -.alsijil-header-nav-button.right { - margin-left: 5px; -} - -.alsijil-header-nav-button i.material-icons { - line-height: 60px; - height: 60px; - font-size: 40px; -} - -.alsijil-nav-header { - width: calc(100% + 40px); - padding: 10px 20px; - margin: -10px -20px 0; -} - -.tabs-icons .tab svg.iconify { - display: block; -} diff --git a/aleksis/apps/alsijil/static/css/alsijil/person.css b/aleksis/apps/alsijil/static/css/alsijil/person.css deleted file mode 100644 index d385d7b69e031fafd4c21d037b70b8f973e99b74..0000000000000000000000000000000000000000 --- a/aleksis/apps/alsijil/static/css/alsijil/person.css +++ /dev/null @@ -1,100 +0,0 @@ -span.input-field.inline > .select-wrapper > input { - color: red; - padding: 14px 0 0 0; - line-height: 2px; - height: 36px; - vertical-align: middle; -} - -span.input-field.inline > .select-wrapper .caret { - top: 12px !important; -} - -@media screen and (min-width: 1400px) { - li.collection-item form { - margin: -30px 0 -30px 0; - } - - li.collection-item#title #select_all_span { - margin-top: 5px; - } -} - -.collection { - overflow: visible; - overflow-x: hidden; -} - -#select_all_container { - display: none; -} - -#select_all_box:indeterminate + span:not(.lever):before { - top: -4px; - left: -6px; - width: 10px; - height: 12px; - border-top: none; - border-left: none; - border-right: white 2px solid; - border-bottom: none; - transform: rotate(90deg); - backface-visibility: hidden; - transform-origin: 100% 100%; -} - -#select_all_box:indeterminate + span:not(.lever):after { - top: 0; - width: 20px; - height: 20px; - border: 2px solid currentColor; - background-color: currentColor; - z-index: 0; -} - -#select_all_box_text { - color: #9e9e9e !important; -} - -td.material-icons { - display: table-cell; -} - -.medium-high { - position: relative; - top: 50%; - left: 50%; - transform: translate(-50%, 50%); -} - -@media screen and (min-width: 600px) { - /* On medium and up devices */ - .medium-high-right { - float: right; - transform: translate(0%, 50%); - } -} - -@media screen and (max-width: 600px) { - /* Only on small devices */ - .full-width-s { - width: 100%; - } - - #heading { - display: block; - } - #heading + a { - float: none !important; - } -} - -.overflow-x-scroll { - overflow-x: scroll; -} - -figure.modal-content figcaption { - font-weight: 300; - font-size: 2.28rem; - line-height: 110%; -} diff --git a/aleksis/apps/alsijil/static/css/alsijil/week_view.css b/aleksis/apps/alsijil/static/css/alsijil/week_view.css deleted file mode 100644 index a42111f55e31d3b3f518873218c76fd246106afe..0000000000000000000000000000000000000000 --- a/aleksis/apps/alsijil/static/css/alsijil/week_view.css +++ /dev/null @@ -1,122 +0,0 @@ -@media screen and (max-width: 600px) { - #toggle-row button[type="submit"] { - width: 100%; - margin-bottom: 1em; - } -} - -.horizontal-scroll-container { - overflow-x: scroll; - display: flex; - column-gap: 1rem; - flex-wrap: nowrap; - align-items: stretch; - scroll-snap-type: x proximity; -} - -.horizontal-scroll-container.vertical { - flex-wrap: wrap; - overflow-x: inherit; -} - -.horizontal-scroll-container.vertical .horizontal-scroll-card { - margin-inline: 0; -} - -dl { - margin: 0; - padding: 0; -} - -dt { - font-weight: bold; -} - -dd { - margin: 0; - padding: unset; -} - -.horizontal-scroll-card { - flex-basis: min(75vw, 400px); - flex-shrink: 0; - flex-grow: 1; - border-radius: 8px; - display: flex; - flex-direction: column; - justify-content: space-between; - scroll-snap-align: center; - transition: all 0.5s; -} - -.horizontal-scroll-card:first-of-type { - margin-inline-start: 0.4rem; - -moz-margin-start: 0.4rem; - -webkit-margin-start: 0.4rem; -} - -.horizontal-scroll-card:last-of-type { - margin-inline-end: 0.4rem; - -moz-margin-end: 0.4rem; - -webkit-margin-end: 0.4rem; -} - -.horizontal-scroll-card .card-action { - margin-bottom: 5px; -} - -.horizontal-scroll-card .card-content .card-title { - display: flex; - justify-content: space-between; -} - -.horizontal-scroll-card .card-content .card-title .subject { - flex-grow: 5; -} - -.horizontal-scroll-card .one-line { - display: grid; - grid-auto-flow: column; - grid-template-rows: 1fr 1fr; -} - -p.subtitle { - display: flex; - justify-content: space-between; - align-items: flex-end; -} - -.btn-superflat ~ span { - line-height: 24px; -} - -.btn-superflat, -.btn-superflat:focus, -.btn-superflat:active { - border: none; - line-height: 1; - height: 24px; - background: none; - font-weight: normal; -} - -.btn-superflat i.material-icons { - vertical-align: middle; -} - -.btn-superflat:hover { - cursor: pointer; -} - -.unfold-trigger i.material-icons { - transition: transform 0.5s 0s ease-in-out; - transform: rotate(-90deg); -} - -.unfold-trigger.vertical i.material-icons { - transform: rotate(-180deg); -} - -.tabs-icons .tab svg.iconify { - display: block; -} diff --git a/aleksis/apps/alsijil/static/js/alsijil/week_view.js b/aleksis/apps/alsijil/static/js/alsijil/week_view.js deleted file mode 100644 index 69124b9c41e656948bbced89b06b8d4edaf3b3c2..0000000000000000000000000000000000000000 --- a/aleksis/apps/alsijil/static/js/alsijil/week_view.js +++ /dev/null @@ -1,20 +0,0 @@ -$(document).ready(function () { - $("#id_group").change(function () { - $("#id_teacher").val("").formSelect(); - }); - $("#id_teacher").change(function () { - $("#id_group").val("").formSelect(); - }); - $("#toggle-row.pre-hidden").hide(); -}); -$("#toggle-button").click(function () { - $("#toggle-row").toggle(); -}); -$(".unfold-trigger").click(function (event) { - let target = event.target; - target.classList.toggle("vertical"); - let next_container = $(target).parent().next(".horizontal-scroll-container"); - if (next_container.length >= 1) { - next_container[0].classList.toggle("vertical"); - } -}); 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..01e5ff016db1eb53d7fc78b0a82ed33f02774f04 100644 --- a/aleksis/apps/alsijil/tasks.py +++ b/aleksis/apps/alsijil/tasks.py @@ -1,187 +1,186 @@ -from copy import deepcopy -from datetime import date, timedelta - -from django.db.models import Q -from django.utils.translation import gettext as _ - -from calendarweek import CalendarWeek -from celery.result import allow_join_result -from celery.states import SUCCESS - -from aleksis.apps.chronos.models import Event, ExtraLesson, LessonPeriod -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 - - -@recorded_task -def generate_full_register_printout(group: int, file_object: int, recorder: ProgressRecorder): - """Generate a full register printout as PDF for a group.""" - context = {} - - _number_of_steps = 8 - - recorder.set_progress(1, _number_of_steps, _("Load data ...")) - - group = Group.objects.get(pk=group) - file_object = PDFFile.objects.get(pk=file_object) - - groups_q = ( - Q(lesson_period__lesson__groups=group) - | Q(lesson_period__lesson__groups__parent_groups=group) - | Q(extra_lesson__groups=group) - | Q(extra_lesson__groups__parent_groups=group) - | Q(event__groups=group) - | Q(event__groups__parent_groups=group) - ) - personal_notes = ( - PersonalNote.objects.prefetch_related( - "lesson_period__substitutions", "lesson_period__lesson__teachers" - ) - .not_empty() - .filter(groups_q) - .filter(groups_of_person=group) - ) - documentations = LessonDocumentation.objects.not_empty().filter(groups_q) - - recorder.set_progress(2, _number_of_steps, _("Sort data ...")) - - sorted_documentations = {"extra_lesson": {}, "event": {}, "lesson_period": {}} - sorted_personal_notes = {"extra_lesson": {}, "event": {}, "lesson_period": {}, "person": {}} - for documentation in documentations: - key = documentation.register_object.label_ - sorted_documentations[key][documentation.register_object_key] = documentation - - for note in personal_notes: - key = note.register_object.label_ - sorted_personal_notes[key].setdefault(note.register_object_key, []) - sorted_personal_notes[key][note.register_object_key].append(note) - sorted_personal_notes["person"].setdefault(note.person.pk, []) - sorted_personal_notes["person"][note.person.pk].append(note) - - recorder.set_progress(3, _number_of_steps, _("Load lesson data ...")) - - # Get all lesson periods for the selected group - lesson_periods = LessonPeriod.objects.filter_group(group).distinct() - events = Event.objects.filter_group(group).distinct() - extra_lessons = ExtraLesson.objects.filter_group(group).distinct() - weeks = CalendarWeek.weeks_within(group.school_term.date_start, group.school_term.date_end) - - register_objects_by_day = {} - for extra_lesson in extra_lessons: - day = extra_lesson.date - register_objects_by_day.setdefault(day, []).append( - ( - extra_lesson, - sorted_documentations["extra_lesson"].get(extra_lesson.pk), - sorted_personal_notes["extra_lesson"].get(extra_lesson.pk, []), - None, - ) - ) - - for event in events: - day_number = (event.date_end - event.date_start).days + 1 - for i in range(day_number): - day = event.date_start + timedelta(days=i) - event_copy = deepcopy(event) - event_copy.annotate_day(day) - - # Skip event days if it isn't inside the timetable schema - if not (event_copy.raw_period_from_on_day and event_copy.raw_period_to_on_day): - continue - - register_objects_by_day.setdefault(day, []).append( - ( - event_copy, - sorted_documentations["event"].get(event.pk), - sorted_personal_notes["event"].get(event.pk, []), - None, - ) - ) - - recorder.set_progress(4, _number_of_steps, _("Sort lesson data ...")) - - weeks = CalendarWeek.weeks_within( - group.school_term.date_start, - group.school_term.date_end, - ) - - for lesson_period in lesson_periods: - for week in weeks: - day = week[lesson_period.period.weekday] - - if ( - lesson_period.lesson.validity.date_start - <= day - <= lesson_period.lesson.validity.date_end - ): - filtered_documentation = sorted_documentations["lesson_period"].get( - f"{lesson_period.pk}_{week.week}_{week.year}" - ) - filtered_personal_notes = sorted_personal_notes["lesson_period"].get( - f"{lesson_period.pk}_{week.week}_{week.year}", [] - ) - - substitution = lesson_period.get_substitution(week) - - register_objects_by_day.setdefault(day, []).append( - (lesson_period, filtered_documentation, filtered_personal_notes, substitution) - ) - - recorder.set_progress(5, _number_of_steps, _("Load statistics ...")) - - persons = group.members.prefetch_related(None).select_related(None) - persons = group.generate_person_list_with_class_register_statistics(persons) - - prefetched_persons = [] - for person in persons: - person.filtered_notes = sorted_personal_notes["person"].get(person.pk, []) - prefetched_persons.append(person) - - context["school_term"] = group.school_term - context["persons"] = prefetched_persons - context["excuse_types"] = ExcuseType.objects.filter(count_as_absent=True) - context["excuse_types_not_absent"] = ExcuseType.objects.filter(count_as_absent=False) - context["extra_marks"] = ExtraMark.objects.all() - context["group"] = group - context["weeks"] = weeks - context["register_objects_by_day"] = register_objects_by_day - context["register_objects"] = list(lesson_periods) + list(events) + list(extra_lessons) - context["today"] = date.today() - context["lessons"] = ( - group.lessons.all() - .select_related(None) - .prefetch_related(None) - .select_related("validity", "subject") - .prefetch_related("teachers", "lesson_periods") - ) - context["child_groups"] = ( - group.child_groups.all() - .select_related(None) - .prefetch_related(None) - .prefetch_related( - "lessons", - "lessons__validity", - "lessons__subject", - "lessons__teachers", - "lessons__lesson_periods", - ) - ) - - recorder.set_progress(6, _number_of_steps, _("Generate template ...")) - - file_object, result = generate_pdf_from_template( - "alsijil/print/full_register.html", context, file_object=file_object - ) - - recorder.set_progress(7, _number_of_steps, _("Generate PDF ...")) - - with allow_join_result(): - result.wait() - file_object.refresh_from_db() - if not result.status == SUCCESS and file_object.file: - raise Exception(_("PDF generation failed")) - - recorder.set_progress(8, _number_of_steps) +# from copy import deepcopy +# from datetime import date, timedelta + +# from django.db.models import Q +# from django.utils.translation import gettext as _ + +# from calendarweek import CalendarWeek +# from celery.result import allow_join_result +# from celery.states import SUCCESS + +# 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 ExtraMark + + +# @recorded_task +# def generate_full_register_printout(group: int, file_object: int, recorder: ProgressRecorder): +# """Generate a full register printout as PDF for a group.""" +# context = {} + +# _number_of_steps = 8 + +# recorder.set_progress(1, _number_of_steps, _("Load data ...")) + +# group = Group.objects.get(pk=group) +# file_object = PDFFile.objects.get(pk=file_object) + +# groups_q = ( +# Q(lesson_period__lesson__groups=group) +# | Q(lesson_period__lesson__groups__parent_groups=group) +# | Q(extra_lesson__groups=group) +# | Q(extra_lesson__groups__parent_groups=group) +# | Q(event__groups=group) +# | Q(event__groups__parent_groups=group) +# ) +# personal_notes = ( +# PersonalNote.objects.prefetch_related( +# "lesson_period__substitutions", "lesson_period__lesson__teachers" +# ) +# .not_empty() +# .filter(groups_q) +# .filter(groups_of_person=group) +# ) +# documentations = LessonDocumentation.objects.not_empty().filter(groups_q) + +# recorder.set_progress(2, _number_of_steps, _("Sort data ...")) + +# sorted_documentations = {"extra_lesson": {}, "event": {}, "lesson_period": {}} +# sorted_personal_notes = {"extra_lesson": {}, "event": {}, "lesson_period": {}, "person": {}} +# for documentation in documentations: +# key = documentation.register_object.label_ +# sorted_documentations[key][documentation.register_object_key] = documentation + +# for note in personal_notes: +# key = note.register_object.label_ +# sorted_personal_notes[key].setdefault(note.register_object_key, []) +# sorted_personal_notes[key][note.register_object_key].append(note) +# sorted_personal_notes["person"].setdefault(note.person.pk, []) +# sorted_personal_notes["person"][note.person.pk].append(note) + +# recorder.set_progress(3, _number_of_steps, _("Load lesson data ...")) + +# # Get all lesson periods for the selected group +# lesson_periods = LessonPeriod.objects.filter_group(group).distinct() +# events = Event.objects.filter_group(group).distinct() +# extra_lessons = ExtraLesson.objects.filter_group(group).distinct() +# weeks = CalendarWeek.weeks_within(group.school_term.date_start, group.school_term.date_end) + +# register_objects_by_day = {} +# for extra_lesson in extra_lessons: +# day = extra_lesson.date +# register_objects_by_day.setdefault(day, []).append( +# ( +# extra_lesson, +# sorted_documentations["extra_lesson"].get(extra_lesson.pk), +# sorted_personal_notes["extra_lesson"].get(extra_lesson.pk, []), +# None, +# ) +# ) + +# for event in events: +# day_number = (event.date_end - event.date_start).days + 1 +# for i in range(day_number): +# day = event.date_start + timedelta(days=i) +# event_copy = deepcopy(event) +# event_copy.annotate_day(day) + +# # Skip event days if it isn't inside the timetable schema +# if not (event_copy.raw_period_from_on_day and event_copy.raw_period_to_on_day): +# continue + +# register_objects_by_day.setdefault(day, []).append( +# ( +# event_copy, +# sorted_documentations["event"].get(event.pk), +# sorted_personal_notes["event"].get(event.pk, []), +# None, +# ) +# ) + +# recorder.set_progress(4, _number_of_steps, _("Sort lesson data ...")) + +# weeks = CalendarWeek.weeks_within( +# group.school_term.date_start, +# group.school_term.date_end, +# ) + +# for lesson_period in lesson_periods: +# for week in weeks: +# day = week[lesson_period.period.weekday] + +# if ( +# lesson_period.lesson.validity.date_start +# <= day +# <= lesson_period.lesson.validity.date_end +# ): +# filtered_documentation = sorted_documentations["lesson_period"].get( +# f"{lesson_period.pk}_{week.week}_{week.year}" +# ) +# filtered_personal_notes = sorted_personal_notes["lesson_period"].get( +# f"{lesson_period.pk}_{week.week}_{week.year}", [] +# ) + +# substitution = lesson_period.get_substitution(week) + +# register_objects_by_day.setdefault(day, []).append( +# (lesson_period, filtered_documentation, filtered_personal_notes, substitution) +# ) + +# recorder.set_progress(5, _number_of_steps, _("Load statistics ...")) + +# persons = group.members.prefetch_related(None).select_related(None) +# persons = group.generate_person_list_with_class_register_statistics(persons) + +# prefetched_persons = [] +# for person in persons: +# person.filtered_notes = sorted_personal_notes["person"].get(person.pk, []) +# prefetched_persons.append(person) + +# context["school_term"] = group.school_term +# context["persons"] = prefetched_persons +# context["excuse_types"] = ExcuseType.objects.filter(count_as_absent=True) +# context["excuse_types_not_absent"] = ExcuseType.objects.filter(count_as_absent=False) +# context["extra_marks"] = ExtraMark.objects.all() +# context["group"] = group +# context["weeks"] = weeks +# context["register_objects_by_day"] = register_objects_by_day +# context["register_objects"] = list(lesson_periods) + list(events) + list(extra_lessons) +# context["today"] = date.today() +# context["lessons"] = ( +# group.lessons.all() +# .select_related(None) +# .prefetch_related(None) +# .select_related("validity", "subject") +# .prefetch_related("teachers", "lesson_periods") +# ) +# context["child_groups"] = ( +# group.child_groups.all() +# .select_related(None) +# .prefetch_related(None) +# .prefetch_related( +# "lessons", +# "lessons__validity", +# "lessons__subject", +# "lessons__teachers", +# "lessons__lesson_periods", +# ) +# ) + +# recorder.set_progress(6, _number_of_steps, _("Generate template ...")) + +# file_object, result = generate_pdf_from_template( +# "alsijil/print/full_register.html", context, file_object=file_object +# ) + +# recorder.set_progress(7, _number_of_steps, _("Generate PDF ...")) + +# with allow_join_result(): +# result.wait() +# file_object.refresh_from_db() +# if not result.status == SUCCESS and file_object.file: +# raise Exception(_("PDF generation failed")) + +# recorder.set_progress(8, _number_of_steps) diff --git a/aleksis/apps/alsijil/templates/alsijil/absences/register.html b/aleksis/apps/alsijil/templates/alsijil/absences/register.html deleted file mode 100644 index 590ad1e1f61d5ed09210101012aadaae9dda6f25..0000000000000000000000000000000000000000 --- a/aleksis/apps/alsijil/templates/alsijil/absences/register.html +++ /dev/null @@ -1,31 +0,0 @@ -{# -*- engine:django -*- #} -{% extends "core/base.html" %} -{% load material_form i18n static any_js %} - -{% block browser_title %}{% blocktrans %}Register absence{% endblocktrans %}{% endblock %} -{% block page_title %}{% blocktrans %}Register absence{% endblocktrans %}{% endblock %} - -{% block extra_head %} - {{ form.media.css }} - {% include_css "select2-materialize" %} -{% endblock %} - -{% block content %} - <form method="post"> - {% csrf_token %} - {% form form=register_absence_form %}{% endform %} - {% include "core/partials/save_button.html" %} - </form> - - <script> - $(document).ready(function () { - $("#id_date_start").change(function () { - $("#id_date_end").val($("#id_date_start").val()); - initDatePicker("#id_date_end"); - }); - }); - </script> - - {% include_js "select2-materialize" %} - {{ form.media.js }} -{% endblock %} diff --git a/aleksis/apps/alsijil/templates/alsijil/absences/register_confirm.html b/aleksis/apps/alsijil/templates/alsijil/absences/register_confirm.html deleted file mode 100644 index 2c427ef2fa701a4e1beb36b40fc5c1de09ca8e44..0000000000000000000000000000000000000000 --- a/aleksis/apps/alsijil/templates/alsijil/absences/register_confirm.html +++ /dev/null @@ -1,82 +0,0 @@ -{# -*- engine:django -*- #} -{% extends "core/base.html" %} -{% load material_form i18n static %} - -{% block browser_title %}{% blocktrans %}Confirm: Register absence{% endblocktrans %}{% endblock %} -{% block page_title %}{% blocktrans %}Confirm: Register absence{% endblocktrans %}{% endblock %} - -{% block content %} - <p class="flow-text"> - {% blocktrans %} - Do you really want to register the following absence? - {% endblocktrans %} - </p> - <div class="card"> - <div class="card-content"> - <div class="card-title"> - {{ person }} - </div> - <div class="collection"> - <div class="collection-item"> - <i class="material-icons iconify left" data-icon="mdi:calendar-range"></i> - {{ form_data.date_start }}, {{ form_data.from_period }}. – {{ form_data.date_end }}, {{ form_data.to_period }}. - {% if form_data.date_start != form_data.date_end %} - <figure class="alert warning"> - <i class="material-icons iconify left" data-icon="mdi:alert-outline"></i> - {% blocktrans %} - As the length of this absence is longer than one day, - please double check the correctness of your entry. - {% endblocktrans %} - </figure> - {% endif %} - </div> - <div class="collection-item"> - <i class="material-icons iconify left" data-icon="mdi:format-list-bulleted"></i> - {% blocktrans with count=affected_lessons %} {{ count }} affected lessons {% endblocktrans %} - {% if affected_lessons == 0 %} - <div class="alert error"> - <div> - <i class="material-icons iconify left" data-icon="mdi:alert-octagon-outline"></i> - {% blocktrans %} - There are no affected lessons. Registering this absence won't have any effect. - {% endblocktrans %} - </div> - </div> - {% endif %} - </div> - <div class="collection-item"> - <i class="material-icons iconify left" data-icon="mdi:label-outline"></i> - {% if form_data.absent %} - <span class="chip red white-text">{% trans "Absent" %}</span> - {% if form_data.excused and form_data.excuse_type %} - <span class="chip green white-text">{{ form_data.excuse_type.name }}</span> - {% elif form_data.excused %} - <span class="chip green white-text">{% trans "Excused" %}</span> - {% endif %} - {% else %} - {% trans "Reset status to 'not absent'" %} - {% endif %} - </div> - {% if form_data.remarks %} - <div class="collection-item"> - <i class="material-icons iconify left" data-icon="mdi:pencil-outline"></i> - {{ form_data.remarks }} - </div> - {% endif %} - </div> - </div> - </div> - - <form method="post"> - {% csrf_token %} - <div class="hide"> - {% form form=form %}{% endform %} - </div> - <input type="hidden" name="confirmed" value="1"> - {% include "core/partials/save_button.html" %} - <a class="btn red waves-effect waves-light" href="{% url "register_absence" person.pk %}"> - <i class="material-icons iconify left" data-icon="mdi:close"></i> - {% trans "Cancel" %} - </a> - </form> -{% endblock %} diff --git a/aleksis/apps/alsijil/templates/alsijil/class_register/all_objects.html b/aleksis/apps/alsijil/templates/alsijil/class_register/all_objects.html deleted file mode 100644 index b5e57e0ee276ab395ebd824327c7584a063dba63..0000000000000000000000000000000000000000 --- a/aleksis/apps/alsijil/templates/alsijil/class_register/all_objects.html +++ /dev/null @@ -1,19 +0,0 @@ -{# -*- engine:django -*- #} -{% extends "core/base.html" %} -{% load i18n rules static django_tables2 material_form %} - -{% block browser_title %}{% blocktrans %}All lessons{% endblocktrans %}{% endblock %} - -{% block page_title %} - {% blocktrans %}All lessons{% endblocktrans %} -{% endblock %} - -{% block extra_head %} - {{ block.super }} - <link rel="stylesheet" href="{% static 'css/alsijil/alsijil.css' %}"/> -{% endblock %} - -{% block content %} - {% include "alsijil/partials/objects_table.html" %} - <script src="{% static "js/multi_select.js" %}"></script> -{% endblock %} diff --git a/aleksis/apps/alsijil/templates/alsijil/class_register/groups.html b/aleksis/apps/alsijil/templates/alsijil/class_register/groups.html deleted file mode 100644 index 43a6eeeb9349969b58c61111e6017b7d24e74f60..0000000000000000000000000000000000000000 --- a/aleksis/apps/alsijil/templates/alsijil/class_register/groups.html +++ /dev/null @@ -1,109 +0,0 @@ -{# -*- engine:django -*- #} -{% extends "core/base.html" %} -{% load i18n static rules %} - -{% block browser_title %}{% blocktrans %}My groups{% endblocktrans %}{% endblock %} - -{% block page_title %} - {% blocktrans %}My groups{% endblocktrans %} -{% endblock %} - -{% block extra_head %} - {{ block.super }} - <link rel="stylesheet" href="{% static 'css/alsijil/alsijil.css' %}"/> -{% endblock %} - -{% block content %} - <table class="highlight responsive-table hide-on-med-and-down"> - <thead> - <tr> - <th>{% trans "Name" %}</th> - <th>{% trans "Students" %}</th> - <th></th> - </tr> - </thead> - {% for group in groups %} - <tr> - <td> - {{ group }} - </td> - <td>{{ group.students_count }}</td> - <td> - <div class="right"> - <a class="btn primary-color waves-effect waves-light" href="{% url "students_list" group.pk %}"> - <i class="material-icons iconify left" data-icon="mdi:account-multiple-outline"></i> - {% trans "Students list" %} - </a> - <a class="btn secondary-color waves-effect waves-light" href="{% url "week_view" "group" group.pk %}"> - <i class="material-icons iconify left" data-icon="mdi:view-week-outline"></i> - {% trans "Week view" %} - </a> - {% has_perm "alsijil.view_assigned_grouproles_rule" user group as can_view_assigned_group_roles %} - {% if can_view_assigned_group_roles %} - <a class="btn primary waves-effect waves-light" href="{% url 'assigned_group_roles' group.pk %}"> - <i class="material-icons iconify left" data-icon="mdi:clipboard-account-outline"></i> - {% trans "Roles" %} - </a> - {% endif %} - <a class="btn primary waves-effect waves-light" href="{% url "full_register_group" group.pk %}" - target="_blank"> - <i class="material-icons iconify left" data-icon="mdi:printer-outline"></i> - {% trans "Generate printout" %} - </a> - </div> - </td> - </tr> - {% empty %} - <tr> - <td class="flow-text" colspan="3"> - {% blocktrans %}No groups available.{% endblocktrans %} - </td> - </tr> - {% endfor %} - </table> - - <div class="hide-on-large-only"> - <ul class="collection"> - {% for group in groups %} - <li class="collection-item"> - <span class="title">{{ group }}</span> - <p> - {{ group.students_count }} {% trans "students" %} - </p> - <p> - <a class="btn primary-color waves-effect waves-light" href="{% url "week_view" "group" group.pk %}"> - <i class="material-icons iconify left" data-icon="mdi:account-multiple-outline"></i> - {% trans "Students list" %} - </a> - </p> - <p> - <a class="btn secondary-color waves-effect waves-light" href="{% url "week_view" "group" group.pk %}"> - <i class="material-icons iconify left" data-icon="mdi:view-week-outline"></i> - {% trans "Week view" %} - </a> - </p> - {% has_perm "alsijil.view_assigned_grouproles_rule" user group as can_view_assigned_group_roles %} - {% if can_view_assigned_group_roles %} - <p> - <a class="btn primary waves-effect waves-light" href="{% url 'assigned_group_roles' group.pk %}"> - <i class="material-icons iconify left" data-icon="mdi:clipboard-account-outline"></i> - {% trans "Roles" %} - </a> - </p> - {% endif %} - <p> - <a class="btn primary waves-effect waves-light" href="{% url "full_register_group" group.pk %}" - target="_blank"> - <i class="material-icons iconify left" data-icon="mdi:printer-outline"></i> - {% trans "Generate printout" %} - </a> - </p> - </li> - {% empty %} - <li class="collection-item flow-text"> - {% blocktrans %}No groups available.{% endblocktrans %} - </li> - {% endfor %} - </ul> - </div> -{% endblock %} diff --git a/aleksis/apps/alsijil/templates/alsijil/class_register/lesson.html b/aleksis/apps/alsijil/templates/alsijil/class_register/lesson.html deleted file mode 100644 index e4e3960c8a1325acdca09ddcff95d958657d0f62..0000000000000000000000000000000000000000 --- a/aleksis/apps/alsijil/templates/alsijil/class_register/lesson.html +++ /dev/null @@ -1,125 +0,0 @@ -{# -*- engine:django -*- #} -{% extends "core/base.html" %} -{% load week_helpers material_form_internal material_form i18n static rules time_helpers %} - -{% block browser_title %}{% blocktrans %}Lesson{% endblocktrans %}{% endblock %} -{% block extra_head %} - {{ block.super }} - <link rel="stylesheet" href="{% static 'css/alsijil/lesson.css' %}"/> - - {% if with_seating_plan %} - <link rel="stylesheet" href="{% static "css/stoelindeling/seating_plan.css" %}"> - {% endif %} -{% endblock %} - -{% block page_title %} - {% include "alsijil/partials/lesson/heading.html" %} -{% endblock %} - -{% block content %} - {% has_perm "alsijil.view_lessondocumentation_rule" user register_object as can_view_lesson_documentation %} - {% has_perm "alsijil.edit_lessondocumentation_rule" user register_object as can_edit_lesson_documentation %} - {% has_perm "alsijil.edit_register_object_personalnote_rule" user register_object as can_edit_register_object_personalnote %} - - <!-- Tab Buttons --> - <div class="col s12 margin-bottom"> - <ul class="tabs tabs-icons tabs-fixed-width"> - <li class="tab col"> - <a href="#lesson-documentation"> - <i class="material-icons iconify" data-icon="mdi:message-bulleted"></i> - {% trans "Period" %} - </a> - </li> - {% if register_object.label_ != "lesson_period" or not register_object.get_substitution.cancelled or not SITE_PREFERENCES.alsijil__block_personal_notes_for_cancelled %} - <li class="tab col"> - <a href="#personal-notes"> - <i class="material-icons iconify" data-icon="mdi:account-multiple-outline"></i> - {% trans "Persons" %} - </a> - </li> - {% endif %} - {% if with_seating_plan %} - <li class="tab col"> - <a href="#seating-plan"> - <i class="material-icons iconify" data-icon="mdi:seat-outline"></i> - {% trans "Seating plan" %} - </a> - </li> - {% endif %} - {% if prev_lesson %} - {% has_perm "alsijil.view_lessondocumentation_rule" user prev_lesson as can_view_prev_lesson_documentation %} - {% if prev_lesson.get_lesson_documentation and can_view_prev_lesson_documentation %} - <li class="tab col"> - <a href="#previous-lesson"> - <i class="material-icons iconify" data-icon="mdi:history"></i> - {% trans "Previous" %} - </a> - </li> - {% endif %} - {% endif %} - <li class="tab col"> - <a href="#more"> - <i class="material-icons iconify" data-icon="mdi:dots-horizontal"></i> - {% trans "More" %} - </a> - </li> - </ul> - </div> - - <form method="post" class="row"> - {% csrf_token %} - - {% if not blocked_because_holidays %} - <div class="row"> - <div class="col s12 no-padding" id="lesson-documentation"> - {% include "alsijil/partials/lesson/tabs/documentation.html" %} - </div> - - {% with prev_doc=prev_lesson.get_lesson_documentation %} - {% with absences=prev_lesson.get_absences tardinesses=prev_lesson.get_tardinesses extra_marks=prev_lesson.get_extra_marks %} - {% has_perm "alsijil.view_lessondocumentation_rule" user prev_lesson as can_view_prev_lesson_documentation %} - {% if prev_doc and can_view_prev_lesson_documentation %} - <div class="col s12 no-padding" id="previous-lesson"> - {% include "alsijil/partials/lesson/tabs/previous_lesson.html" %} - </div> - {% endif %} - {% endwith %} - {% endwith %} - - {% if register_object.label_ != "lesson_period" or not register_object.get_substitution.cancelled or not SITE_PREFERENCES.alsijil__block_personal_notes_for_cancelled %} - <div class="col s12 no-padding" id="personal-notes"> - {% include "alsijil/partials/lesson/tabs/notes.html" %} - </div> - {% endif %} - - {% if with_seating_plan %} - <div class="col s12 no-padding" id="seating-plan"> - {% include "alsijil/partials/lesson/tabs/seating_plan.html" %} - </div> - {% endif %} - - <div class="col s12 no-padding" id="more"> - {% include "alsijil/partials/lesson/tabs/more.html" %} - </div> - </div> - {% else %} - <div class="row no-margin"> - <div class="container"> - <div class="card"> - <div class="card-content center-align"> - <p> - <i class="material-icons iconify medium orange-text" data-icon="mdi:alert-outline"></i> - </p> - <p class="card-title"> - {% blocktrans %} - This lesson overlaps with holidays and can't be edited. - {% endblocktrans %} - </p> - <span class="badge new blue no-float no-margin">{{ holiday }}</span> - </div> - </div> - </div> - </div> - {% endif %} - </form> -{% endblock %} diff --git a/aleksis/apps/alsijil/templates/alsijil/class_register/person.html b/aleksis/apps/alsijil/templates/alsijil/class_register/person.html deleted file mode 100644 index 78792a793cce1b93577d7bf6ca55da771a43a7df..0000000000000000000000000000000000000000 --- a/aleksis/apps/alsijil/templates/alsijil/class_register/person.html +++ /dev/null @@ -1,185 +0,0 @@ -{# -*- engine:django -*- #} -{% extends "core/base.html" %} -{% load rules data_helpers week_helpers i18n material_form static django_tables2 %} - -{% block extra_head %} - <link rel="stylesheet" href="{% static "css/alsijil/person.css" %}"> - <script src="{% static "js/multi_select.js" %}" type="text/javascript"></script> -{% endblock %} - -{% block browser_title %}{% blocktrans %}Class register: person{% endblocktrans %}{% endblock %} - - -{% block page_title %} - {% has_perm "alsijil.view_my_students_rule" user as has_students %} - {% if has_students %} - <a href="{% url "my_students" %}" - class="btn-flat primary-color-text waves-light waves-effect"> - <i class="material-icons iconify left" data-icon="mdi:chevron-left"></i> {% trans "Back" %} - </a> - {% endif %} - <span id="heading"> - {% blocktrans with person=person %} - Class register overview for {{ person }} - {% endblocktrans %} - </span> - {% has_perm "alsijil.register_absence_rule" user person as can_register_absence %} - {% if can_register_absence %} - <a class="btn primary-color waves-effect waves-light right" href="{% url "register_absence" person.pk %}"> - <i class="material-icons iconify left" data-icon="mdi:message-draw"></i> - {% trans "Register absence" %} - </a> - {% endif %} -{% endblock %} - -{% block content %} - <div class="row"> - - <!-- Tab Buttons --> - <div class="col s12"> - <ul class="tabs"> - {% if register_object_table %} - <li class="tab"> - <a href="#lesson-documentations">{% trans "Lesson documentations" %}</a> - </li> - {% endif %} - <li class="tab"> - <a href="#personal-notes">{% trans "Personal notes" %}</a> - </li> - {% if stats %} - <li class="tab"><a href="#statistics">{% trans "Statistics" %}</a></li> - {% endif %} - </ul> - </div> - - <!-- Lesson Documentation Tab --> - {% if register_object_table %} - <div class="col s12" id="lesson-documentations"> - {% include "alsijil/partials/objects_table.html" with table=register_object_table filter_form=filter_form %} - </div> - {% endif %} - - <!-- Personal Note Tab --> - <div class="col s12" id="personal-notes"> - <div class="col s12" id="overview"> - <h2>{% trans "Relevant personal notes" %}</h2> - <form class="modal" id="filter-modal"> - <figure class="modal-content"> - <figcaption>{% trans "Filter personal notes" %}</figcaption> - {% form form=personal_note_filter_form %}{% endform %} - </figure> - <div class="modal-footer"> - <button type="button" class="btn-flat secondary-color-text waves-effect waves-ripple" id="remove-filters"> - <i class="material-icons iconify left" data-icon="mdi:close"></i>{% trans "Clear all filters" %} - </button> - <button type="button" class="modal-close btn-flat red-text waves-effect waves-ripple waves-red"> - <i class="material-icons iconify left" data-icon="mdi:close-circle-outline"></i>{% trans "Close" %} - </button> - <button type="submit" class="modal-close btn-flat primary-color-text waves-effect waves-ripple waves-light"> - <i class="material-icons iconify left" data-icon="mdi:filter-outline"></i>{% trans "Filter" %} - </button> - </div> - </form> - {% has_perm "alsijil.edit_person_overview_personalnote_rule" user person as can_mark_all_as_excused %} - <div class="row"> - <div class="col s12 m3 l5 push-m9 push-l7"> - <button - class="modal-trigger btn primary-color waves-effect waves-light - {% if can_mark_all_as_excused %} medium-high-right {% endif %}" - data-target="filter-modal" - type="button"> - {% trans "Filter results" %} ({{ num_filters }}) - <i class="material-icons iconify right" data-icon="mdi:filter-outline"></i> - </button> - </div> - <form action="" method="post" class=""> - {% csrf_token %} - <div class="col s12 m9 l7 pull-m3 pull-l5 row"> - {% if can_mark_all_as_excused %} - <div class="col s12 m9"> - {% form form=action_form %}{% endform %} - </div> - <div class="col s12 m3"> - <button type="submit" class="btn waves-effect waves-light medium-high full-width-s"> - Run <i class="material-icons iconify right" data-icon="mdi:send-outline"></i> - </button> - </div> - {% endif %} - </div> - <div class="col s12 overflow-x-scroll"> - {% render_table personal_notes_table %} - </div> - </form> - </div> - </div> - </div> - - <!-- Statistics Tab --> - {% if stats %} - <div class="col s12" id="statistics"> - <h2>{% trans "Statistics on absences, tardiness and remarks" %}</h2> - <ul class="collapsible"> - {% for school_term, stat in stats %} - <li {% if forloop.first %}class="active"{% endif %}> - <div class="collapsible-header"> - <i class="material-icons iconify" data-icon="mdi:calendar-range"></i>{{ school_term }}</div> - <div class="collapsible-body"> - <table> - <tr> - <th colspan="3">{% trans 'Absences' %}</th> - <td>{{ stat.absences_count }}</td> - </tr> - <tr> - <td rowspan="{{ excuse_types.count|add:3 }}" class="hide-on-small-only">{% trans "thereof" %}</td> - <td rowspan="{{ excuse_types.count|add:3 }}" class="hide-on-med-and-up"></td> - <th colspan="2">{% trans 'Excused' %}</th> - <td>{{ stat.excused }}</td> - </tr> - <tr> - <td rowspan="{{ excuse_types.count|add:1 }}" class="hide-on-small-only">{% trans "thereof" %}</td> - <td rowspan="{{ excuse_types.count|add:1 }}" class="hide-on-med-and-up"></td> - <th colspan="2" class="truncate">{% trans 'Without Excuse Type' %}</th> - <td>{{ stat.excused_no_excuse_type }}</td> - </tr> - {% for excuse_type in excuse_types %} - <tr> - <th>{{ excuse_type.name }}</th> - <td>{{ stat|get_dict:excuse_type.count_label }}</td> - </tr> - {% endfor %} - <tr> - <th colspan="2">{% trans 'Unexcused' %}</th> - <td>{{ stat.unexcused }}</td> - </tr> - {% for excuse_type in excuse_types_not_absent %} - <tr> - <th colspan="3">{{ excuse_type.name }}</th> - <td>{{ stat|get_dict:excuse_type.count_label }}</td> - </tr> - {% endfor %} - <tr> - <th colspan="3">{% trans 'Tardiness' %}</th> - <td>{{ stat.tardiness }}'/{{ stat.tardiness_count }} ×</td> - </tr> - {% for extra_mark in extra_marks %} - <tr> - <th colspan="3">{{ extra_mark.name }}</th> - <td>{{ stat|get_dict:extra_mark.count_label }}</td> - </tr> - {% endfor %} - </table> - </div> - </li> - {% endfor %} - </ul> - </div> - {% endif %} - <script type="text/javascript"> - $("#remove-filters").click(function () { - $("#filter-modal").trigger("reset"); - $("#filter-modal input, #filter-modal select").each(function () { - $(this).val(""); - }) - }) - </script> -{% endblock %} diff --git a/aleksis/apps/alsijil/templates/alsijil/class_register/persons.html b/aleksis/apps/alsijil/templates/alsijil/class_register/persons.html deleted file mode 100644 index 6873ddc84a1cdbad62ff43b09d1d50ece163a01a..0000000000000000000000000000000000000000 --- a/aleksis/apps/alsijil/templates/alsijil/class_register/persons.html +++ /dev/null @@ -1,64 +0,0 @@ -{# -*- engine:django -*- #} -{% extends "core/base.html" %} -{% load i18n week_helpers data_helpers static time_helpers %} - -{% block browser_title %}{% blocktrans %}My students{% endblocktrans %}{% endblock %} - - -{% block page_title %} - {% blocktrans %}My students{% endblocktrans %} -{% endblock %} - -{% block extra_head %} - {{ block.super }} - <link rel="stylesheet" href="{% static 'css/alsijil/alsijil.css' %}"/> -{% endblock %} - - -{% block content %} - <ul class="collapsible"> - {% for group, persons in groups %} - <li {% if forloop.first %}class="active"{% endif %}> - <div class="collapsible-header"> - <div class="hundred-percent"> - <span class="right show-on-active hide-on-small-and-down"> - <a class="btn primary-color waves-effect waves-light" href="{% url "week_view" "group" group.pk %}"> - <i class="material-icons iconify left" data-icon="mdi:view-week-outline"></i> - {% trans "Week view" %} - </a> - <a class="btn waves-effect waves-light" href="{% url "full_register_group" group.pk %}" target="_blank"> - <i class="material-icons iconify left" data-icon="mdi:printer-outline"></i> - {% trans "Generate printout" %} - </a> - </span> - - <h2>{{ group.name }} - <span class="chip">{{ group.school_term }}</span> - </h2> - - <p class="show-on-active hide-on-med-and-up"> - <a class="btn primary-color waves-effect waves-light hundred-percent" - href="{% url "week_view" "group" group.pk %}"> - <i class="material-icons iconify left" data-icon="mdi:view-week-outline"></i> - {% trans "Week view" %} - </a> - </p> - <p class="show-on-active hide-on-med-and-up"> - <a class="btn waves-effect waves-light hundred-percent" href="{% url "full_register_group" group.pk %}" - target="_blank"> - <i class="material-icons iconify left" data-icon="mdi:printer-outline"></i> - {% trans "Generate printout" %} - </a> - </p> - </div> - </div> - - <div class="collapsible-body"> - {% include "alsijil/partials/persons_with_stats.html" with persons=persons %} - </div> - </li> - {% endfor %} - </ul> - - {% include "alsijil/partials/legend.html" %} -{% endblock %} diff --git a/aleksis/apps/alsijil/templates/alsijil/class_register/students_list.html b/aleksis/apps/alsijil/templates/alsijil/class_register/students_list.html deleted file mode 100644 index 72bb8071f1b05f7e911bf75776a1740a9a5d5ae9..0000000000000000000000000000000000000000 --- a/aleksis/apps/alsijil/templates/alsijil/class_register/students_list.html +++ /dev/null @@ -1,49 +0,0 @@ -{# -*- engine:django -*- #} -{% extends "core/base.html" %} -{% load static time_helpers data_helpers week_helpers i18n %} - -{% block browser_title %}{% blocktrans with group=group %}Students list: {{ group }}{% endblocktrans %}{% endblock %} - -{% block page_title %} - <a href="{% url "my_groups" %}" - class="btn-flat primary-color-text waves-light waves-effect"> - <i class="material-icons iconify left" data-icon="mdi:chevron-left"></i> {% trans "Back" %} - </a> - {% blocktrans with group=group %}Students list: {{ group }}{% endblocktrans %} - <span class="right show-on-active hide-on-small-and-down"> - <a class="btn primary-color waves-effect waves-light" href="{% url "week_view" "group" group.pk %}"> - <i class="material-icons iconify left" data-icon="mdi:view-week-outline"></i> - {% trans "Week view" %} - </a> - <a class="btn waves-effect waves-light" href="{% url "full_register_group" group.pk %}" target="_blank"> - <i class="material-icons iconify left" data-icon="mdi:printer-outline"></i> - {% trans "Generate printout" %} - </a> - </span> -{% endblock %} - -{% block extra_head %} - {{ block.super }} - <link rel="stylesheet" href="{% static 'css/alsijil/alsijil.css' %}"/> -{% endblock %} - -{% block content %} - <p class="show-on-active hide-on-med-and-up"> - <a class="btn primary-color waves-effect waves-light hundred-percent" - href="{% url "week_view" "group" group.pk %}"> - <i class="material-icons iconify left" data-icon="mdi:view-week-outline"></i> - {% trans "Week view" %} - </a> - </p> - <p class="show-on-active hide-on-med-and-up"> - <a class="btn waves-effect waves-light hundred-percent" href="{% url "full_register_group" group.pk %}" - target="_blank"> - <i class="material-icons iconify left" data-icon="mdi:printer-outline"></i> - {% trans "Generate printout" %} - </a> - </p> - - {% include "alsijil/partials/persons_with_stats.html" with persons=persons %} - - {% include "alsijil/partials/legend.html" %} -{% endblock %} diff --git a/aleksis/apps/alsijil/templates/alsijil/class_register/week_view.html b/aleksis/apps/alsijil/templates/alsijil/class_register/week_view.html deleted file mode 100644 index 081f38c5321359ca92241c21ba1f9b1fc71aa926..0000000000000000000000000000000000000000 --- a/aleksis/apps/alsijil/templates/alsijil/class_register/week_view.html +++ /dev/null @@ -1,443 +0,0 @@ -{# -*- engine:django -*- #} - -{% extends "core/base.html" %} -{% load material_form i18n week_helpers static data_helpers rules time_helpers %} - -{% block browser_title %}{% blocktrans %}Week view{% endblocktrans %}{% endblock %} - -{% block extra_head %} - {{ block.super }} - <link rel="stylesheet" href="{% static 'css/alsijil/alsijil.css' %}"/> - <link rel="stylesheet" href="{% static 'css/alsijil/week_view.css' %}"/> -{% endblock %} - -{% block content %} - <script type="text/javascript" src="{% static "js/helper.js" %}"></script> - {{ week_select|json_script:"week_select" }} - <script type="text/javascript" src="{% static "js/chronos/week_select.js" %}"></script> - <div class="row"> - <div id="toggle-row" class="col s12 m8 l10 {% if lesson_periods %}pre-hidden{% endif %}"> - <form method="post" action=""> - {% csrf_token %} - {% form form=select_form %}{% endform %} - <button type="submit" class="btn waves-effect waves-light primary-color"> - <i class="material-icons iconify left" data-icon="mdi:check"></i> - {% blocktrans %}Select{% endblocktrans %} - </button> - </form> - </div> - <div class="col s12 m4 l2 right"> - <button type="button" class="btn waves-effect waves-light hundred-percent" id="toggle-button"> - <i class="material-icons iconify left" data-icon="mdi:filter-outline"></i> {% trans "Toggle filters" %} - </button> - </div> - </div> - - - <div class="row no-margin"> - <h4 class="col s12 m6">{% blocktrans with el=el week=week.week %}CW {{ week }}: - {{ instance }}{% endblocktrans %} </h4> - {% include "chronos/partials/week_select.html" with wanted_week=week %} - </div> - - {% if group %} - <p class="hide-on-med-and-down"> - <a class="btn primary-color waves-effect waves-light" href="{% url "students_list" group.pk %}"> - <i class="material-icons iconify left" data-icon="mdi:account-multiple-outline"></i> - {% trans "Students list" %} - </a> - <a class="btn waves-effect waves-light" href="{% url "full_register_group" group.pk %}" target="_blank"> - <i class="material-icons iconify left" data-icon="mdi:printer-outline"></i> - {% trans "Generate printout" %} - </a> - </p> - - <p class="hide-on-med-and-up"> - <a class="btn primary-color waves-effect waves-light hundred-percent" - href="{% url "students_list" group.pk %}"> - <i class="material-icons iconify left" data-icon="mdi:account-multiple-outline"></i> - {% trans "Students list" %} - </a> - </p> - <p class="hide-on-med-and-up"> - <a class="btn waves-effect waves-light hundred-percent" href="{% url "full_register_group" group.pk %}" - target="_blank"> - <i class="material-icons iconify left" data-icon="mdi:printer-outline"></i> - {% trans "Generate printout" %} - </a> - </p> - {% endif %} - - {% if lesson_periods %} - <div class="col s12 margin-bottom"> - <ul class="tabs tabs-icons tabs-fixed-width"> - <li class="tab col"> - <a class="active" href="#week-overview"> - <i class="material-icons iconify" data-icon="mdi:message-bulleted"></i> - {% trans "Lesson documentations" %} - </a> - </li> - <li class="tab col"> - <a href="#personal-notes"> - <i class="material-icons iconify" data-icon="mdi:account-multiple-outline"></i> - {% trans "Persons" %} - </a> - </li> - {% if group_roles %} - <li class="tab col"> - <a href="#group-roles"> - <i class="material-icons iconify" data-icon="mdi:clipboard-account-outline"></i> - {% trans "Group roles" %} - </a> - </li> - {% endif %} - </ul> - </div> - {% endif %} - - {% if lesson_periods %} - <div class="row"> - <div class="col s12" id="week-overview"> - {% for weekday, objects in regrouped_objects.items %} - {% with weekdays|get_dict:objects.0.weekday as advanced_weekday %} - {% if advanced_weekday.holiday and not SITE_PREFERENCES.alsijil__allow_entries_in_holidays %} - <div class="card"> - <div class="card-content"> - <span class="card-title"> - {{ advanced_weekday.name }}, {{ advanced_weekday.date }} <span - class="badge new blue no-float">{{ advanced_weekday.holiday }}</span> - </span> - </div> - </div> - {% else %} - <div class="card show-on-extra-large"> - <div class="card-content"> - <span class="card-title"> - {{ advanced_weekday.name }}, {{ advanced_weekday.date }} - </span> - <table class="striped datatable"> - <thead> - <tr> - <th></th> - <th>{% blocktrans %}Period{% endblocktrans %}</th> - {% if not group %} - <th>{% blocktrans %}Groups{% endblocktrans %}</th> - {% endif %} - <th>{% blocktrans %}Subject{% endblocktrans %}</th> - <th>{% blocktrans %}Teachers{% endblocktrans %}</th> - <th>{% blocktrans %}Lesson topic{% endblocktrans %}</th> - <th>{% blocktrans %}Homework{% endblocktrans %}</th> - <th>{% blocktrans %}Group note{% endblocktrans %}</th> - </tr> - </thead> - <tbody> - {% for register_object in objects %} - {% has_perm "alsijil.view_lessondocumentation_rule" user register_object as can_view_lesson_documentation %} - {% if can_view_lesson_documentation %} - <tr> - <td class="center-align"> - {% include "alsijil/partials/lesson_status.html" with register_object=register_object %} - </td> - <td class="tr-link"> - <a class="tr-link" - href="{{ register_object.alsijil_url }}?back={{ back_url }}"> - {% if register_object.period %} - {{ register_object.period.period }}. - {% else %} - {{ register_object.period_from_on_day }}.– - {{ register_object.period_to_on_day }}. - {% endif %} - </a> - </td> - {% if not group %} - <td> - <a class="tr-link" - href="{{ register_object.alsijil_url }}?back={{ back_url }}"> - {% if register_object.lesson %} - {{ register_object.lesson.group_names }} - {% else %} - {{ register_object.group_names }} - {% endif %} - </a> - </td> - {% endif %} - <td> - <a class="tr-link" - href="{{ register_object.alsijil_url }}?back={{ back_url }}"> - {% if register_object.get_subject %} - {{ register_object.get_subject.name }} - {% elif register_object.subject %} - {{ register_object.subject }} - {% else %} - {% trans "Event" %} ({{ register_object.title }}) - {% endif %} - </a> - </td> - <td> - <a class="tr-link" - href="{{ register_object.alsijil_url }}?back={{ back_url }}"> - {{ register_object.teacher_names }} - </a> - </td> - <td> - <a class="tr-link" - href="{{ register_object.alsijil_url }}?back={{ back_url }}"> - {% firstof register_object.get_lesson_documentation.topic "–" %} - </a> - </td> - <td> - <a class="tr-link" - href="{{ register_object.alsijil_url }}?back={{ back_url }}"> - {% firstof register_object.get_lesson_documentation.homework "–" %} - </a> - </td> - <td> - <a class="tr-link" - href="{{ register_object.alsijil_url }}?back={{ back_url }}"> - {% firstof register_object.get_lesson_documentation.group_note "–" %} - </a> - </td> - </tr> - {% endif %} - {% endfor %} - </tbody> - </table> - </div> - </div> - <ul class="collapsible hide-on-extra-large-only hide-on-small-only"> - <li class=""> - <div class="collapsible-header flow-text"> - {{ advanced_weekday.name }}, {{ advanced_weekday.date }} <i - class="material-icons iconify collapsible-icon-right" data-icon="mdi:unfold-more-horizontal"></i> - </div> - <div class="collapsible-body"> - <div class="collection"> - {% for register_object in objects %} - {% has_perm "alsijil.view_lessondocumentation_rule" user register_object as can_view_lesson_documentation %} - {% if can_view_lesson_documentation %} - <a class="collection-item avatar" - href="{{ register_object.alsijil_url }}?back={{ back_url }}"> - {% include "alsijil/partials/lesson_status.html" with register_object=register_object css_class="materialize-circle" color_suffix=" " %} - <table> - <tr> - <th>{% trans "Subject" %}</th> - <td> - {% if register_object.period %} - {{ register_object.period.period }}. - {% else %} - {{ register_object.period_from_on_day }}.– - {{ register_object.period_to_on_day }}. - {% endif %} - {% if register_object.get_subject %} - {{ register_object.get_subject.name }} - {% elif register_object.subject %} - {{ register_object.subject }} - {% else %} - {% trans "Event" %} - {% endif %} - </td> - </tr> - {% if not group %} - <tr> - <th>{% trans "Groups" %}</th> - <td> - {% if register_object.lesson %} - {{ register_object.lesson.group_names }} - {% else %} - {{ register_object.group_names }} - {% endif %} - </td> - </tr> - {% endif %} - <tr> - <th>{% trans "Teachers" %}</th> - <td> - {{ register_object.teacher_names }} - </td> - </tr> - <tr> - <th>{% trans "Lesson topic" %}</th> - <td>{% firstof register_object.get_lesson_documentation.topic "–" %}</td> - </tr> - {% with register_object.get_lesson_documentation as lesson_documentation %} - {% if lesson_documentation.homework %} - <tr> - <th>{% trans "Homework" %}</th> - <td>{% firstof register_object.get_lesson_documentation.homework "–" %}</td> - </tr> - {% endif %} - {% if lesson_documentation.group_note %} - <tr> - <th>{% trans "Group note" %}</th> - <td>{% firstof register_object.get_lesson_documentation.group_note "–" %}</td> - </tr> - {% endif %} - {% endwith %} - </table> - </a> - {% endif %} - {% endfor %} - </div> - </div> - </li> - </ul> - <div class="hide-on-med-and-up"> - <h3>{{ advanced_weekday.name }}</h3> - <p class="subtitle"> - <span>{{ advanced_weekday.date }}</span> - <button class="btn-superflat right waves-effect unfold-trigger"> - {% trans "Unfold" %} - <i class="material-icons iconify" data-icon="mdi:unfold-less-horizontal"></i> - </button> - </p> - <div class="horizontal-scroll-container"> - {% for register_object in objects %} - <div class="card horizontal-scroll-card"> - <div class="card-content"> - <span class="card-title"> - <span class="period"> - {% if register_object.period %} - {{ register_object.period.period }}. - {% else %} - {{ register_object.period_from_on_day }}.–{{ register_object.period_to_on_day }}. - {% endif %} - </span> - <span class="subject"> - {% if register_object.get_subject %} - {{ register_object.get_subject.name }} - {% elif register_object.subject %} - {{ register_object.subject }} - {% else %} - {% trans "Event" %} - {% endif %} - </span> - <span class="lesson-icon"> - {% include "alsijil/partials/lesson_status.html" with register_object=register_object %} - </span> - </span> - <dl> - <div class="one-line"> - {% if not group %} - <dt>{% trans "Groups" %}</dt> - <dd> - {% if register_object.lesson %} - {{ register_object.lesson.group_names }} - {% else %} - {{ register_object.group_names }} - {% endif %} - </dd> - {% endif %} - - <dt>{% trans "Teachers" %}</dt> - <dd> - {{ register_object.teacher_names }} - </dd> - </div> - - <dt>{% trans "Lesson topic" %}</dt> - <dd>{% firstof register_object.get_lesson_documentation.topic "–" %}</dd> - - {% with register_object.get_lesson_documentation as lesson_documentation %} - {% if lesson_documentation.homework %} - <dt>{% trans "Homework" %}</dt> - <dd>{% firstof register_object.get_lesson_documentation.homework "–" %}</dd> - {% endif %} - {% if lesson_documentation.group_note %} - <dt>{% trans "Group note" %}</dt> - <dd>{% firstof register_object.get_lesson_documentation.group_note "–" %}</dd> - {% endif %} - {% endwith %} - </dl> - </div> - <div class="card-action"> - <a href="{{ register_object.alsijil_url }}?back={{ back_url }}" - class=""> - {% trans "Visit lesson overview" %} - </a> - </div> - </div> - {% endfor %} - </div> - </div> - {% endif %} - {% endwith %} - {% endfor %} - </div> - <div class="col s12" id="personal-notes"> - <div class="card"> - <div class="card-content"> - <span class="card-title"> - {% blocktrans %}Personal notes{% endblocktrans %} - </span> - {% for person in persons %} - <h5 class="card-title"> - <a href="{% url "overview_person" person.person.pk %}">{{ person.person.full_name }}</a> - {% has_perm "alsijil.register_absence_rule" user person.person as can_register_absence %} - {% if can_register_absence %} - <a class="btn primary-color waves-effect waves-light right" - href="{% url "register_absence" person.person.pk %}"> - <i class="material-icons iconify left" data-icon="mdi:message-draw"></i> - {% trans "Register absence" %} - </a> - {% endif %} - </h5> - {% if group_roles %} - <p> - {% for assignment in person.group_roles %} - {% include "alsijil/group_role/chip.html" with role=assignment.role small=assignment.date_range %} - {% endfor %} - </p> - {% endif %} - <p class="card-text"> - {% trans "Absent" %}: {{ person.person.absences_count }} - ({{ person.person.unexcused_count }} {% trans "unexcused" %}) - </p> - <p class="card-text"> - {% trans "Summed up tardiness" %}: {% firstof person.person.tardiness_sum|to_time|time:"H\h i\m" "–" %} - </p> - <p class="card-text"> - {% trans "Count of tardiness" %}: {{ person.person.tardiness_count }} × - </p> - {% for extra_mark in extra_marks %} - <p class="card-text"> - {{ extra_mark.name }}: {{ person.person|get_dict:extra_mark.count_label }} - </p> - {% endfor %} - {% for note in person.personal_notes %} - <blockquote> - {{ note.remarks }} - <em class="right"> - <a href="{{ note.register_object.alsijil_url }}"> - {{ note.date_formatted }}, {{ note.register_object.get_subject.name }} - </a> - </em> - </blockquote> - {% endfor %} - {% endfor %} - </div> - </div> - </div> - {% if group_roles %} - <div class="col s12" id="group-roles"> - {% include "alsijil/group_role/partials/assigned_roles.html" with roles=group_roles group=group back_url=back_url %} - </div> - {% endif %} - </div> - {% else %} - <div class="card"> - <div class="card-content"> - <span class="card-title"> - <i class="material-icons iconify red-text left" data-icon="mdi:alert-outline"></i> - {% blocktrans %}No lessons available{% endblocktrans %} - </span> - <p> - {% blocktrans %} - There are no lessons for the selected group or teacher in this week. - {% endblocktrans %} - </p> - </div> - </div> - {% endif %} - - <script src="{% static 'js/alsijil/week_view.js' %}" type="text/javascript"></script> -{% endblock %} diff --git a/aleksis/apps/alsijil/templates/alsijil/excuse_type/create.html b/aleksis/apps/alsijil/templates/alsijil/excuse_type/create.html deleted file mode 100644 index 6fc6faefb2543cecf32b02e7dcb7ea2a40f3d73b..0000000000000000000000000000000000000000 --- a/aleksis/apps/alsijil/templates/alsijil/excuse_type/create.html +++ /dev/null @@ -1,18 +0,0 @@ -{# -*- engine:django -*- #} - -{% extends "core/base.html" %} -{% load material_form i18n %} - -{% block browser_title %}{% blocktrans %}Create excuse type{% endblocktrans %}{% endblock %} -{% block page_title %}{% blocktrans %}Create excuse type{% endblocktrans %}{% endblock %} - -{% block content %} - {% include "alsijil/excuse_type/warning.html" %} - - <form method="post"> - {% csrf_token %} - {% form form=form %}{% endform %} - {% include "core/partials/save_button.html" %} - </form> - -{% endblock %} diff --git a/aleksis/apps/alsijil/templates/alsijil/excuse_type/edit.html b/aleksis/apps/alsijil/templates/alsijil/excuse_type/edit.html deleted file mode 100644 index 78396ed66264cc19abdac2085d1cc89ff931bb38..0000000000000000000000000000000000000000 --- a/aleksis/apps/alsijil/templates/alsijil/excuse_type/edit.html +++ /dev/null @@ -1,17 +0,0 @@ -{# -*- engine:django -*- #} - -{% extends "core/base.html" %} -{% load material_form i18n %} - -{% block browser_title %}{% blocktrans %}Edit excuse type{% endblocktrans %}{% endblock %} -{% block page_title %}{% blocktrans %}Edit excuse type{% endblocktrans %}{% endblock %} - -{% block content %} - - <form method="post"> - {% csrf_token %} - {% form form=form %}{% endform %} - {% include "core/partials/save_button.html" %} - </form> - -{% endblock %} diff --git a/aleksis/apps/alsijil/templates/alsijil/excuse_type/list.html b/aleksis/apps/alsijil/templates/alsijil/excuse_type/list.html deleted file mode 100644 index e6235a32f382aa9cf80ae3d8ad0ad1704e25e2e9..0000000000000000000000000000000000000000 --- a/aleksis/apps/alsijil/templates/alsijil/excuse_type/list.html +++ /dev/null @@ -1,23 +0,0 @@ -{# -*- engine:django -*- #} - -{% extends "core/base.html" %} - -{% load i18n rules %} -{% load render_table from django_tables2 %} - -{% block browser_title %}{% blocktrans %}Excuse types{% endblocktrans %}{% endblock %} -{% block page_title %}{% blocktrans %}Excuse types{% endblocktrans %}{% endblock %} - -{% block content %} - {% include "alsijil/excuse_type/warning.html" %} - - {% has_perm "alsijil.add_excusetype_rule" user as add_excusetype %} - {% if add_excusetype %} - <a class="btn green waves-effect waves-light" href="{% url 'create_excuse_type' %}"> - <i class="material-icons iconify left"data-icon="mdi:plus"></i> - {% trans "Create excuse type" %} - </a> - {% endif %} - - {% render_table table %} -{% endblock %} diff --git a/aleksis/apps/alsijil/templates/alsijil/excuse_type/warning.html b/aleksis/apps/alsijil/templates/alsijil/excuse_type/warning.html deleted file mode 100644 index 811b90b33381c578469552154092333b2b38624d..0000000000000000000000000000000000000000 --- a/aleksis/apps/alsijil/templates/alsijil/excuse_type/warning.html +++ /dev/null @@ -1,8 +0,0 @@ -{% load i18n %} -<figure class="alert warning"> - <i class="material-icons iconify left" data-icon="mdi:alert-outline"></i> - {% blocktrans %} - This function should only be used to define alternatives to the default excuse which also will be counted extra. - Don't use this to create a default excuse or if you don't divide between different types of excuse. - {% endblocktrans %} -</figure> diff --git a/aleksis/apps/alsijil/templates/alsijil/notifications/check.html b/aleksis/apps/alsijil/templates/alsijil/notifications/check.html deleted file mode 100644 index d76a1a0a5abfc6fb8b7722d5a4cf16ff927f069a..0000000000000000000000000000000000000000 --- a/aleksis/apps/alsijil/templates/alsijil/notifications/check.html +++ /dev/null @@ -1,4 +0,0 @@ -{% load i18n %}{% trans "Please check if the following class register entries are complete and correct:" %} -{% for entry in items %} -- {{ entry.register_object }} ({{ entry.date }}) -{% endfor %} \ No newline at end of file diff --git a/aleksis/apps/alsijil/templates/alsijil/partials/lesson/heading.html b/aleksis/apps/alsijil/templates/alsijil/partials/lesson/heading.html deleted file mode 100644 index 132e97f05acd0216d59f89a12cab08b96e123cc5..0000000000000000000000000000000000000000 --- a/aleksis/apps/alsijil/templates/alsijil/partials/lesson/heading.html +++ /dev/null @@ -1,110 +0,0 @@ -{% load i18n %} - -{% if next_lesson_person or prev_lesson_person or back_to_week_url %} - <div class="row margin-bottom alsijil-nav-header"> - <div class="col s12 no-padding"> - {# Back to week view #} - {% if back_to_week_url %} - <a href="{{ back_to_week_url }}" - class="btn secondary-color waves-light waves-effect margin-bottom {% if prev_lesson_person or next_lesson_person %}hide-on-extra-large-only{% endif %}"> - <i class="material-icons iconify left" data-icon="mdi:chevron-left"></i> {% trans "Week view" %} - </a> - {% endif %} - - {% if prev_lesson_person or next_lesson_person %} - <div class="col s12 no-padding center alsijil-nav"> - {% if back_to_week_url %} - <a href="{{ back_to_week_url }}" - class="btn-flat secondary-color-text waves-light waves-effect left hide-on-med-and-down hide-on-large-only show-on-extra-large"> - <i class="material-icons iconify left" data-icon="mdi:chevron-left"></i> {% trans "Week view" %} - </a> - {% endif %} - - {# Previous lesson #} - <a class="btn-flat waves-effect waves-light left primary-color-text {% if not prev_lesson_person %}disabled{% endif %}" - title="{% trans "My previous lesson" %}" - {% if prev_lesson_person %} - href="{% url "lesson_period" prev_lesson_person.week.year prev_lesson_person.week.week prev_lesson_person.id %}" - {% endif %} - > - <i class="material-icons iconify left" data-icon="mdi:chevron-left"></i> - <span class="hide-on-small-only">{% trans "My previous lesson" %}</span> - <span class="hide-on-med-and-up">{% trans "Previous" %}</span> - </a> - {# Next lesson #} - <a class="btn-flat waves-effect waves-light right primary-color-text {% if not next_lesson_person %}disabled{% endif %}" - title="{% trans "My next lesson" %}" - {% if next_lesson_person %} - href="{% url "lesson_period" next_lesson_person.week.year next_lesson_person.week.week next_lesson_person.id %}" - {% endif %} - > - <i class="material-icons iconify right" data-icon="mdi:chevron-right"></i> - <span class="hide-on-small-only">{% trans "My next lesson" %}</span> - <span class="hide-on-med-and-up">{% trans "Next" %}</span> - </a> - <span class="truncate">{{ request.user.person }}</span> - </div> - {% endif %} - </div> - </div> -{% endif %} - -<h1> - <span class="right hide-on-small-only"> - {% include "alsijil/partials/lesson_status.html" with register_object=register_object css_class="medium" %} - </span> - - <a class="btn-flat waves-effect waves-light primary-color-text left alsijil-header-nav-button hide-on-med-and-up {% if not prev_lesson %}disabled{% endif %}" - {% if prev_lesson %} - href="{% url "lesson_period" prev_lesson.week.year prev_lesson.week.week prev_lesson.id %}" - {% endif %} - > - <i class="material-icons iconify center" data-icon="mdi:chevron-left"></i> - </a> - <a class="btn-flat waves-effect waves-light primary-color-text right alsijil-header-nav-button hide-on-med-and-up {% if not next_lesson %}disabled{% endif %}" - {% if next_lesson %} - href="{% url "lesson_period" next_lesson.week.year next_lesson.week.week next_lesson.id %}" - {% endif %} - > - <i class="material-icons iconify center" data-icon="mdi:chevron-right"></i> - </a> - - <span class="alsijil-time-head"> - {% if register_object.label_ == "event" %} - {% if register_object.date_start == register_object.date_end %} - {% if register_object.period_from.period == register_object.period_to.period %} - {{ register_object.date_start|date:"SHORT_DATE_FORMAT" }}, - {% blocktrans with period=register_object.period_from.period %}{{ period }}. period{% endblocktrans %} - {% else %} - {{ register_object.date_start|date:"SHORT_DATE_FORMAT" }}, - {% blocktrans with period_from=register_object.period_from.period period_to=register_object.period_to.period %} - {{ period_from }}.–{{ period_to }}. period - {% endblocktrans %} - {% endif %} - {% else %} - {{ register_object.date_start|date:"SHORT_DATE_FORMAT" }}, - {{ register_object.period_from.period }}.–{{ register_object.date_end|date:"SHORT_DATE_FORMAT" }}, - {{ register_object.period_to.period }}. - {% endif %} - {% else %} - {{ day|date:"SHORT_DATE_FORMAT" }}, - {% blocktrans with period=register_object.period.period %}{{ period }}. period{% endblocktrans %} - {% endif %} - </span> - - <span class="alsijil-object-head"> - {{ register_object.group_names }}, - - {% if register_object.label_ == "event" %} - {% trans "Event" %} ({{ register_object.title }}), - {% else %} - {{ register_object.get_subject.short_name }}, - {% endif %} - - {{ register_object.teacher_short_names }} - </span> -</h1> - -<div class="hide-on-med-and-up margin-bottom"> - {% include "alsijil/partials/lesson_status.html" with register_object=register_object chip=1 css_class="hundred-percent center" %} -</div> diff --git a/aleksis/apps/alsijil/templates/alsijil/partials/lesson/prev_next.html b/aleksis/apps/alsijil/templates/alsijil/partials/lesson/prev_next.html deleted file mode 100644 index a12cf71e843ada4d1caeec54a82c8ca1876cb7ec..0000000000000000000000000000000000000000 --- a/aleksis/apps/alsijil/templates/alsijil/partials/lesson/prev_next.html +++ /dev/null @@ -1,38 +0,0 @@ -{% load i18n %} - -<div class="row no-margin hide-on-small-only"> - <div class="col s12 no-padding"> - {% if not blocked_because_holidays and with_save %} - {% if can_edit_lesson_documentation or can_edit_register_object_personalnote %} - <button type="submit" class="btn waves-effect waves-light green margin-bottom"> - <i class="material-icons iconify left" data-icon="mdi:content-save-outline"></i> - {% trans "Save" %} - </button> - {% endif %} - {% endif %} - - <a class="btn waves-effect waves-light primary margin-bottom {% if not prev_lesson %}disabled{% endif %}" - {% if prev_lesson %} - href="{% url "lesson_period" prev_lesson.week.year prev_lesson.week.week prev_lesson.id %}" - {% endif %} - > - <i class="material-icons iconify left" data-icon="mdi:arrow-left"></i> - {% blocktrans with subject=register_object.get_subject.short_name %} - Previous {{ subject }} lesson - {% endblocktrans %} - </a> - - <a class="btn right waves-effect waves-light primary margin-bottom {% if not next_lesson %}disabled{% endif %}" - {% if next_lesson %} - href="{% url "lesson_period" next_lesson.week.year next_lesson.week.week next_lesson.id %}" - {% endif %} - > - <i class="material-icons iconify right" data-icon="mdi:arrow-right"></i> - {% blocktrans with subject=register_object.get_subject.short_name %} - Next {{ subject }} lesson - {% endblocktrans %} - </a> - </div> -</div> - - diff --git a/aleksis/apps/alsijil/templates/alsijil/partials/lesson/tabs/documentation.html b/aleksis/apps/alsijil/templates/alsijil/partials/lesson/tabs/documentation.html deleted file mode 100644 index 22b396f457a13d4d04824836745fd49fdf7eac72..0000000000000000000000000000000000000000 --- a/aleksis/apps/alsijil/templates/alsijil/partials/lesson/tabs/documentation.html +++ /dev/null @@ -1,65 +0,0 @@ -{% load i18n material_form_internal material_form %} - -{% include "alsijil/partials/lesson/prev_next.html" with with_save=0 %} - -<div class="hide-on-med-and-up margin-bottom"> - {% if not blocked_because_holidays %} - {% if can_edit_lesson_documentation or can_edit_register_object_personalnote %} - {% include "core/partials/save_button.html" %} - {% endif %} - {% endif %} -</div> - -<div class="card"> - <div class="card-content"> - <span class="card-title"> - {% blocktrans %}Lesson documentation{% endblocktrans %} - </span> - - {% if can_edit_lesson_documentation %} - {% form form=lesson_documentation_form %}{% endform %} - {% elif can_view_lesson_documentation %} - <table> - <tr> - <th> - {% trans "Lesson topic" %} - </th> - <td> - {{ lesson_documentation.topic }} - </td> - </tr> - <tr> - <th> - {% trans "Homework" %} - </th> - <td> - {{ lesson_documentation.homework }} - </td> - </tr> - <tr> - <th> - {% trans "Group note" %} - </th> - <td> - {{ lesson_documentation.group_note }} - </td> - </tr> - </table> - {% endif %} - </div> - <div class="card-action-light hide-on-small-only"> - {% if not blocked_because_holidays %} - {% if can_edit_lesson_documentation or can_edit_register_object_personalnote %} - {% include "core/partials/save_button.html" %} - {% endif %} - {% endif %} - </div> -</div> - -<div class="hide-on-med-and-up"> - {% if not blocked_because_holidays %} - {% if can_edit_lesson_documentation or can_edit_register_object_personalnote %} - {% include "core/partials/save_button.html" %} - {% endif %} - {% endif %} -</div> \ No newline at end of file diff --git a/aleksis/apps/alsijil/templates/alsijil/partials/lesson/tabs/more.html b/aleksis/apps/alsijil/templates/alsijil/partials/lesson/tabs/more.html deleted file mode 100644 index ffc7706488757871e43de4e71da9e73802273a3f..0000000000000000000000000000000000000000 --- a/aleksis/apps/alsijil/templates/alsijil/partials/lesson/tabs/more.html +++ /dev/null @@ -1,16 +0,0 @@ -{% load i18n %} - -{% if group_roles %} - {% include "alsijil/group_role/partials/assigned_roles.html" with roles=group_roles group=register_object.get_groups.first back_url=back_url %} -{% endif %} - -{% if can_view_lesson_documentation %} - <div class="card"> - <div class="card-content"> - <span class="card-title"> - {% blocktrans %}Change history{% endblocktrans %} - </span> - {% include 'core/partials/crud_events.html' with obj=lesson_documentation %} - </div> - </div> -{% endif %} \ No newline at end of file diff --git a/aleksis/apps/alsijil/templates/alsijil/partials/lesson/tabs/notes.html b/aleksis/apps/alsijil/templates/alsijil/partials/lesson/tabs/notes.html deleted file mode 100644 index 1b013252a8f316979cdf5bdcc87ef1c7463b6e36..0000000000000000000000000000000000000000 --- a/aleksis/apps/alsijil/templates/alsijil/partials/lesson/tabs/notes.html +++ /dev/null @@ -1,142 +0,0 @@ -{% load i18n material_form_internal material_form time_helpers %} - -{% include "alsijil/partials/lesson/prev_next.html" with with_save=1 %} - -{% if not blocked_because_holidays %} - {% if can_edit_lesson_documentation or can_edit_register_object_personalnote %} - <button type="submit" - class="btn waves-effect waves-light green margin-bottom hundred-percent hide-on-med-and-up"> - <i class="material-icons iconify left" data-icon="mdi:content-save-outline"></i> {% trans "Save" %} - </button> - {% endif %} -{% endif %} - -{% if can_edit_register_object_personalnote %} - {% form form=personal_note_formset.management_form %}{% endform %} -{% endif %} - -<div class="card no-mobile-card"> - <div class="card-content"> - <span class="card-title"> - {% blocktrans %}Personal notes{% endblocktrans %} - </span> - - <table class="striped alsijil-table horizontal-on-small"> - <thead> - <tr> - <th>{% blocktrans %}Person{% endblocktrans %}</th> - <th>{% blocktrans %}Absent{% endblocktrans %}</th> - <th>{% blocktrans %}Tardiness{% endblocktrans %}</th> - <th>{% blocktrans %}Excused{% endblocktrans %}</th> - <th>{% blocktrans %}Excuse type{% endblocktrans %}</th> - <th>{% blocktrans %}Extra marks{% endblocktrans %}</th> - <th>{% blocktrans %}Remarks{% endblocktrans %}</th> - </tr> - </thead> - <tbody> - {% for form in personal_note_formset %} - {% if can_edit_register_object_personalnote %} - <tr> - {{ form.id }} - <td class="person-name">{{ form.person_name }}{{ form.person_name.value }} - <p> - {% for assignment in form.instance.person.group_roles.all %} - {% include "alsijil/group_role/chip.html" with role=assignment.role %} - {% endfor %} - </p> - </td> - <td class="center-align"> - <label> - {{ form.absent }} - <span><span class="hide-on-large-only">{{ form.absent.label }}</span></span> - </label> - </td> - <td> - <div class="input-field"> - {{ form.tardiness }} - <label for="{{ form.absent.id_for_label }}"> - {% trans "Tardiness (in m)" %} - </label> - </div> - </td> - <td class="center-align"> - <label> - {{ form.excused }} - <span><span class="hide-on-large-only">{{ form.excused.label }}</span></span> - </label> - </td> - <td> - <div class="input-field"> - {{ form.excuse_type }} - <label for="{{ form.excuse_type.id_for_label }}"> - {% trans "Excuse type" %} - </label> - </div> - </td> - <td> - {% for group, items in form.extra_marks|select_options %} - {% for choice, value, selected in items %} - <label class="{% if selected %} active{% endif %} alsijil-check-box"> - <input type="checkbox" - {% if value == None or value == '' %}disabled{% else %}value="{{ value }}"{% endif %} - {% if selected %} checked="checked"{% endif %} - name="{{ form.extra_marks.html_name }}"> - <span>{{ choice }}</span> - </label> - {% endfor %} - {% endfor %} - </td> - <td> - <div class="input-field"> - {{ form.remarks }} - <label for="{{ form.remarks.id_for_label }}"> - {% trans "Remarks" %} - </label> - </div> - </td> - </tr> - {% else %} - <tr> - <td>{{ form.person_name.value }} - <p> - {% for assignment in form.instance.person.group_roles.all %} - {% include "alsijil/group_role/chip.html" with role=assignment.role %} - {% endfor %} - </p> - </td> - <td> - <i class="material-icons iconify center" data-icon="mdi:{{ form.absent.value|yesno:"check,close" }}"></i> - </td> - <td> - <i class="material-icons iconify center" data-icon="mdi:{{ form.tardiness.value|yesno:"check,close" }}"></i> - <span class="alsijil-tardiness-text"> - {% if form.tardiness.value %}{{ form.tardiness.value|to_time|time:"i\m" }}{% endif %} - </span> - </td> - <td> - <i class="material-icons iconify center" data-icon="mdi:{{ form.excused.value|yesno:"check,close" }}"></i> - </td> - <td>{% firstof form.instance.excuse_type "–" %}</td> - <td> - {% for extra_mark in form.instance.extra_marks.all %} - {{ extra_mark }}{% if not forloop.last %},{% endif %} - {% empty %} - – - {% endfor %} - </td> - <td>{% firstof form.remarks.value "–" %}</td> - </tr> - {% endif %} - {% endfor %} - </tbody> - </table> - </div> -</div> -{% if not blocked_because_holidays %} - {% if can_edit_lesson_documentation or can_edit_register_object_personalnote %} - <button type="submit" - class="btn waves-effect waves-light green margin-bottom hundred-percent hide-on-med-and-up"> - <i class="material-icons iconify left" data-icon="mdi:content-save-outline"></i> {% trans "Save" %} - </button> - {% endif %} -{% endif %} \ No newline at end of file diff --git a/aleksis/apps/alsijil/templates/alsijil/partials/lesson/tabs/previous_lesson.html b/aleksis/apps/alsijil/templates/alsijil/partials/lesson/tabs/previous_lesson.html deleted file mode 100644 index 8457576b786f579032d5c02052c3ea02a940b6ce..0000000000000000000000000000000000000000 --- a/aleksis/apps/alsijil/templates/alsijil/partials/lesson/tabs/previous_lesson.html +++ /dev/null @@ -1,62 +0,0 @@ -{% load i18n rules %} - -<div class="card"> - <div class="card-content"> - <span class="card-title"> - {% blocktrans %}Overview: Previous lesson{% endblocktrans %} ({{ prev_doc.date_formatted }}, - {% blocktrans with period=prev_lesson.period.period %}{{ period }}. period{% endblocktrans %}) - </span> - - <table> - {% if prev_doc.topic %} - <tr> - <th class="collection-item">{% trans "Lesson topic of previous lesson:" %}</th> - <td>{{ prev_doc.topic }}</td> - </tr> - {% endif %} - - {% if prev_doc.homework %} - <tr> - <th class="collection-item">{% trans "Homework for this lesson:" %}</th> - <td>{{ prev_doc.homework }}</td> - </tr> - {% endif %} - - {% if prev_doc.group_note %} - <tr> - <th class="collection-item">{% trans "Group notes for previous lesson:" %}</th> - <td>{{ prev_doc.group_note }}</td> - </tr> - {% endif %} - - {% if absences %} - <tr> - <th>{% trans "Absent persons:" %}</th> - <td>{% include "alsijil/partials/absences.html" with notes=absences %}</td> - </tr> - {% endif %} - - {% if tardinesses %} - <tr> - <th>{% trans "Late persons:" %}</th> - <td>{% include "alsijil/partials/tardinesses.html" with notes=tardinesses %}</td> - </tr> - {% endif %} - - {% for extra_mark, notes in extra_marks.items %} - <tr> - <th>{{ extra_mark.name }}</th> - <td> - {% for note in notes %} - {% has_perm "alsijil.view_personalnote_rule" user note as can_view_personalnote %} - {% if can_view_personalnote %} - <span>{{ note.person }}{% if not forloop.last %},{% endif %}</span> - {% endif %} - {% endfor %} - </td> - </tr> - {% endfor %} - - </table> - </div> -</div> \ No newline at end of file diff --git a/aleksis/apps/alsijil/templates/alsijil/partials/lesson/tabs/seating_plan.html b/aleksis/apps/alsijil/templates/alsijil/partials/lesson/tabs/seating_plan.html deleted file mode 100644 index de5e9f44f947ac49a951d30b6de58a02e844f044..0000000000000000000000000000000000000000 --- a/aleksis/apps/alsijil/templates/alsijil/partials/lesson/tabs/seating_plan.html +++ /dev/null @@ -1,89 +0,0 @@ -{% load i18n material_form_internal material_form time_helpers rules %} - - -{% if seating_plan %} - <div class="card no-mobile-card"> - <div class="card-content"> - <div class="card-title margin-bottom"> - {% blocktrans with group=seating_plan.group room=seating_plan.room %}Seating plan for {{ group }} in - {{ room }}{% endblocktrans %} - </div> - {% if seating_plan_parent %} - <figure class="alert primary"> - <i class="material-icons iconify left" data-icon="information-outline"></i> - {% blocktrans with child_group=first_group %} - This seating plan is taken from the parent group of {{ child_group }}. - If you want, you can take it over for your group and then customize it. - {% endblocktrans %} - </figure> - {% endif %} - - <div class="row margin-bottom no-padding"> - <div class="col s12 no-padding"> - {% has_perm "stoelindeling.edit_seatingplan_rule" user seating_plan as can_edit %} - {% has_perm "stoelindeling.copy_seatingplan_for_group_rule" user first_group as can_copy %} - - {% if can_edit %} - <a class="btn orange waves-effect waves-light" - href="{% url "edit_seating_plan" seating_plan.pk %}?next={{ back_url }}#seating-plan"> - <i class="material-icons iconify left" data-icon="mdi:pencil-outline"></i> - {% trans "Edit seating plan" %} - </a> - {% endif %} - {% if can_copy and seating_plan_parent %} - <a class="btn orange waves-effect waves-light" - href="{% url "copy_seating_plan" seating_plan.pk %}?next={{ back_url }}#seating-plan"> - <i class="material-icons iconify left" data-icon="mdi:content-copy"></i> - {% trans "Copy plan and edit" %} - </a> - {% endif %} - </div> - </div> - - <div class="row"> - <div class="col s12"> - {% include "stoelindeling/seating_plan/render.html" %} - </div> - </div> - </div> - </div> -{% else %} - <div class="container"> - <div class="card"> - <div class="card-content"> - <div class="card-title"> - <i class="material-icons iconify left small orange-text" data-icon="mdi:alert-outline"></i> - {% trans "There is no seating plan for this lesson." %} - </div> - {% has_perm "stoelindeling.create_seatingplan_rule" user first_group as can_add %} - {% if can_add %} - <div class="row margin-bottom"> - <div class="col s12"> - <a class="btn waves-effect waves-light" href="{% url "create_seating_plan" %}?group={{ first_group.pk }}&subject={{ register_object.get_subject.pk }}&room={{ register_object.get_room.pk }}&next={{ back_url }}#seating-plan"> - <i class="material-icons iconify left" data-icon="mdi:plus"></i> - {% blocktrans with group=first_group.name subject=register_object.get_subject.name room=register_object.get_room.name %} - Create a new seating plan for {{ group }} ({{ subject }}) in {{ room }} - {% endblocktrans %} - </a> - </div> - </div> - {% endif %} - {% for parent_group in first_group.parent_groups.all %} - {% has_perm "stoelindeling.create_seatingplan_rule" user parent_group as can_add %} - {% if can_add %} - <div class="row"> - <div class="col s12"> - <a class="btn waves-effect waves-light" href="{% url "create_seating_plan" %}?group={{ parent_group.pk }}&subject={{ register_object.get_subject.pk }}&room={{ register_object.get_room.pk }}&next={{ back_url }}#seating-plan"> - <i class="material-icons iconify left" data-icon="mdi:plus"></i> - {% blocktrans with group=parent_group.name room=register_object.get_room.name %} - Create a new seating plan for {{ group }} in {{ room }} - {% endblocktrans %} - </a> - </div> - </div> - {% endif %} - {% endfor %} - </div> - </div> - </div> -{% endif %} diff --git a/aleksis/apps/alsijil/tests/test_actions.py b/aleksis/apps/alsijil/tests/test_actions.py deleted file mode 100644 index 8dd499ac953583637a3ade36c835aa0e5d6aae24..0000000000000000000000000000000000000000 --- a/aleksis/apps/alsijil/tests/test_actions.py +++ /dev/null @@ -1,96 +0,0 @@ -from datetime import date, time - -import pytest - -from aleksis.apps.alsijil.actions import ( - delete_personal_note, - mark_as_excuse_type_generator, - mark_as_excused, - mark_as_unexcused, -) -from aleksis.apps.alsijil.models import ExcuseType, PersonalNote -from aleksis.apps.chronos.models import Event, TimePeriod -from aleksis.core.models import Person - -pytestmark = pytest.mark.django_db - - -def _generate_event(day: date): - period_from = TimePeriod.objects.create( - weekday=0, period=1, time_start=time(10, 00), time_end=time(11, 00) - ) - period_to = TimePeriod.objects.create( - weekday=0, period=2, time_start=time(11, 00), time_end=time(12, 00) - ) - - event = Event.objects.create( - date_start=day, date_end=day, period_from=period_from, period_to=period_to - ) - return event - - -def _prepare_notes(): - """Create some minimal personal notes.""" - person, __ = Person.objects.get_or_create(first_name="Jane", last_name="Doe") - - excuse_type, __ = ExcuseType.objects.get_or_create(short_name="Foo", name="Fooooooooooooo") - notes = [ - PersonalNote( - person=person, - event=_generate_event(date(2021, 10, 1)), - absent=True, - remarks="This is baz.", - ), - PersonalNote(person=person, event=_generate_event(date(2021, 11, 1)), absent=True), - PersonalNote( - person=person, event=_generate_event(date(2022, 10, 1)), absent=True, excused=True - ), - PersonalNote( - person=person, - event=_generate_event(date(2021, 3, 1)), - absent=True, - excused=True, - excuse_type=excuse_type, - ), - PersonalNote(person=person, event=_generate_event(date(2021, 10, 4)), tardiness=10), - PersonalNote( - person=person, event=_generate_event(date(2032, 10, 11)), remarks="Good work!" - ), - PersonalNote(person=person, event=_generate_event(date(2032, 10, 11))), - ] - PersonalNote.objects.bulk_create(notes) - return notes - - -def test_mark_as_excused_action(): - notes = _prepare_notes() - assert PersonalNote.objects.filter(excused=True).count() == 2 - mark_as_excused(None, None, PersonalNote.objects.all()) - assert PersonalNote.objects.filter(excused=True).count() == 4 - assert PersonalNote.objects.filter(excuse_type=None, excused=True).count() == 4 - - -def test_mark_as_unexcused_action(): - notes = _prepare_notes() - assert PersonalNote.objects.filter(excused=True).count() == 2 - mark_as_unexcused(None, None, PersonalNote.objects.all()) - assert PersonalNote.objects.filter(excused=True).count() == 0 - assert PersonalNote.objects.filter(excuse_type=None, excused=True).count() == 0 - - -def test_mark_as_excuse_type_generator_action(): - excuse_type, __ = ExcuseType.objects.get_or_create(short_name="Foo", name="Fooooooooooooo") - notes = _prepare_notes() - assert PersonalNote.objects.filter(excused=True).count() == 2 - assert PersonalNote.objects.filter(excused=True, excuse_type=excuse_type).count() == 1 - mark_as_foo = mark_as_excuse_type_generator(excuse_type=excuse_type) - mark_as_foo(None, None, PersonalNote.objects.all()) - assert PersonalNote.objects.filter(excused=True).count() == 4 - assert PersonalNote.objects.filter(excuse_type=excuse_type, excused=True).count() == 4 - - -def test_delete_personal_note_action(): - notes = _prepare_notes() - assert PersonalNote.objects.not_empty().count() == 6 - delete_personal_note(None, None, PersonalNote.objects.all()) - assert PersonalNote.objects.not_empty().count() == 0 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..2a27bca7e2260a13d12199027d3501add3494b60 100644 --- a/aleksis/apps/alsijil/util/predicates.py +++ b/aleksis/apps/alsijil/util/predicates.py @@ -1,4 +1,4 @@ -from typing import Any, Union +from typing import Union from django.contrib.auth.models import User from django.db.models import Q @@ -6,77 +6,13 @@ from django.utils.timezone import localdate, now from rules import predicate -from aleksis.apps.chronos.models import Event, ExtraLesson, LessonEvent, LessonPeriod +from aleksis.apps.chronos.models import LessonEvent from aleksis.apps.cursus.models import Course 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 - - -@predicate -def is_none(user: User, obj: Any) -> bool: - """Predicate that checks if the provided object is None-like.""" - return not bool(obj) - - -@predicate -def is_lesson_teacher(user: User, obj: Union[LessonPeriod, Event, ExtraLesson]) -> bool: - """Predicate for teachers of a lesson. - - Checks whether the person linked to the user is a teacher in the register object. - If the register object is a lesson period and has a substitution linked, - this will **only** check if the person is one of the substitution teachers. - """ - if obj: - return user.person in obj.get_teachers().all() - return False - - -@predicate -def is_lesson_original_teacher(user: User, obj: Union[LessonPeriod, Event, ExtraLesson]) -> bool: - """Predicate for teachers of a lesson. - - Checks whether the person linked to the user is a teacher in the register object. - If the register object is a lesson period and has a substitution linked, - this will **also** check if the person is one of the substitution teachers. - """ - if obj: - if isinstance(obj, LessonPeriod) and user.person in obj.lesson.teachers.all(): - return True - return user.person in obj.get_teachers().all() - return False - - -@predicate -def is_lesson_participant(user: User, obj: LessonPeriod) -> bool: - """Predicate for participants of a lesson. - - Checks whether the person linked to the user is a member in - the groups linked to the given LessonPeriod. - """ - if hasattr(obj, "lesson") or hasattr(obj, "groups"): - for group in obj.get_groups().all(): - if user.person in list(group.members.all()): - return True - return False - - -@predicate -def is_lesson_parent_group_owner(user: User, obj: LessonPeriod) -> bool: - """ - Predicate for parent group owners of a lesson. - - Checks whether the person linked to the user is the owner of - any parent groups of any groups of the given LessonPeriods lesson. - """ - if hasattr(obj, "lesson") or hasattr(obj, "groups"): - for group in obj.get_groups().all(): - for parent_group in group.parent_groups.all(): - if user.person in list(parent_group.owners.all()): - return True - return False +from ..models import Documentation, NewPersonalNote @predicate @@ -130,19 +66,6 @@ def use_prefetched(obj, attr): return getattr(obj, attr).all() -@predicate -def is_person_primary_group_owner(user: User, obj: Person) -> bool: - """ - Predicate for group owners of the person's primary group. - - Checks whether the person linked to the user is - the owner of the primary group of the given person. - """ - if obj.primary_group: - return user.person in use_prefetched(obj.primary_group, "owners") - return False - - def has_person_group_object_perm(perm: str): """Predicate builder for permissions on a set of member groups. @@ -171,55 +94,6 @@ def is_group_member(user: User, obj: Union[Group, Person]) -> bool: return False -def has_lesson_group_object_perm(perm: str): - """Predicate builder for permissions on lesson groups. - - Checks whether a user has a permission on any group of a LessonPeriod. - """ - name = f"has_lesson_group_object_perm:{perm}" - - @predicate(name) - def fn(user: User, obj: LessonPeriod) -> bool: - if hasattr(obj, "lesson"): - groups = obj.lesson.groups.all() - for group in groups: - if check_object_permission(user, perm, group, checker_obj=obj): - return True - return False - - return fn - - -def has_personal_note_group_perm(perm: str): - """Predicate builder for permissions on personal notes. - - Checks whether a user has a permission on any group of a person of a PersonalNote. - """ - name = f"has_personal_note_person_or_group_perm:{perm}" - - @predicate(name) - def fn(user: User, obj: PersonalNote) -> bool: - if hasattr(obj, "person"): - groups = obj.person.member_of.all() - for group in groups: - if check_object_permission(user, perm, group, checker_obj=obj): - return True - return False - - return fn - - -@predicate -def is_own_personal_note(user: User, obj: PersonalNote) -> bool: - """Predicate for users referred to in a personal note. - - Checks whether the user referred to in a PersonalNote is the active user. - """ - if hasattr(obj, "person") and obj.person is user.person: - return True - return False - - @predicate def is_parent_group_owner(user: User, obj: Group) -> bool: """Predicate which checks whether the user is the owner of any parent group of the group.""" @@ -230,66 +104,6 @@ def is_parent_group_owner(user: User, obj: Group) -> bool: return False -@predicate -def is_personal_note_lesson_teacher(user: User, obj: PersonalNote) -> bool: - """Predicate for teachers of a register object linked to a personal note. - - Checks whether the person linked to the user is a teacher - in the register object linked to the personal note. - If the register object is a lesson period and has a substitution linked, - this will **only** check if the person is one of the substitution teachers. - """ - if hasattr(obj, "register_object"): - return user.person in obj.register_object.get_teachers().all() - return False - - -@predicate -def is_personal_note_lesson_original_teacher(user: User, obj: PersonalNote) -> bool: - """Predicate for teachers of a register object linked to a personal note. - - Checks whether the person linked to the user is a teacher - in the register object linked to the personal note. - If the register object is a lesson period and has a substitution linked, - this will **also** check if the person is one of the substitution teachers. - """ - if hasattr(obj, "register_object"): - if ( - isinstance(obj.register_object, LessonPeriod) - and user.person in obj.lesson_period.lesson.teachers.all() - ): - return True - - return user.person in obj.register_object.get_teachers().all() - return False - - -@predicate -def is_personal_note_lesson_parent_group_owner(user: User, obj: PersonalNote) -> bool: - """ - Predicate for parent group owners of a lesson referred to in the lesson of a personal note. - - Checks whether the person linked to the user is the owner of - any parent groups of any groups of the given LessonPeriod lesson of the given PersonalNote. - If so, also checks whether the person linked to the personal note actually is a member of this - parent group. - """ - if hasattr(obj, "register_object"): - for group in obj.register_object.get_groups().all(): - for parent_group in group.parent_groups.all(): - if user.person in use_prefetched( - parent_group, "owners" - ) and obj.person in use_prefetched(parent_group, "members"): - return True - return False - - -@predicate -def is_teacher(user: User, obj: Person) -> bool: - """Predicate which checks if the provided object is a teacher.""" - return user.person.is_teacher - - @predicate def is_group_role_assignment_group_owner(user: User, obj: Union[Group, Person]) -> bool: """Predicate for group owners of a group role assignment. 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)