diff --git a/aleksis/apps/alsijil/actions.py b/aleksis/apps/alsijil/actions.py new file mode 100644 index 0000000000000000000000000000000000000000..8c672da4f5c8ae70ced965e45d11672d34c688a8 --- /dev/null +++ b/aleksis/apps/alsijil/actions.py @@ -0,0 +1,54 @@ +from typing import 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.core.models import Notification + + +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 = _("{} wants 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 = _("Notify teacher to check data") diff --git a/aleksis/apps/alsijil/forms.py b/aleksis/apps/alsijil/forms.py index 228e825bae4e1170a3327629b6c8a973c192f7c0..72d3d20275c9c9cfec0909a418940862ce9d6491 100644 --- a/aleksis/apps/alsijil/forms.py +++ b/aleksis/apps/alsijil/forms.py @@ -1,8 +1,11 @@ -from datetime import datetime +from datetime import datetime, timedelta +from typing import Optional, Sequence from django import forms from django.core.exceptions import ValidationError from django.db.models import Count, Q +from django.http import HttpRequest +from django.utils import timezone from django.utils.translation import gettext_lazy as _ from django_select2.forms import ModelSelect2MultipleWidget, ModelSelect2Widget, Select2Widget @@ -10,11 +13,13 @@ from guardian.shortcuts import get_objects_for_user from material import Fieldset, Layout, Row from aleksis.apps.chronos.managers import TimetableType -from aleksis.apps.chronos.models import TimePeriod -from aleksis.core.models import Group, Person +from aleksis.apps.chronos.models import Subject, TimePeriod +from aleksis.core.forms import ListActionForm +from aleksis.core.models import Group, Person, SchoolTerm from aleksis.core.util.core_helpers import get_site_preferences from aleksis.core.util.predicates import check_global_permission +from .actions import send_request_to_check_entry from .models import ( ExcuseType, ExtraMark, @@ -254,3 +259,66 @@ 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): + date_end = timezone.now().date() + date_start = date_end - timedelta(days=30) + return { + "school_term": SchoolTerm.current, + "date_start": date_start, + "date_end": date_end, + } + + def __init__( + self, + request: HttpRequest, + *args, + for_person: bool = True, + groups: Optional[Sequence[Group]] = None, + **kwargs + ): + self.request = request + person = self.request.user.person + + kwargs["initial"] = self.get_initial() + 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) + ) + 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/menus.py b/aleksis/apps/alsijil/menus.py index a7e6c8407ba49cf537bc728ffe2794a2a9bb01db..fe720477001be6e5992a71ac62b66fad00186780 100644 --- a/aleksis/apps/alsijil/menus.py +++ b/aleksis/apps/alsijil/menus.py @@ -78,6 +78,17 @@ MENUS = { ), ], }, + { + "name": _("All lessons"), + "url": "all_register_objects", + "icon": "list", + "validators": [ + ( + "aleksis.core.util.predicates.permission_validator", + "alsijil.view_register_objects_list", + ), + ], + }, { "name": _("Excuse types"), "url": "excuse_types", diff --git a/aleksis/apps/alsijil/preferences.py b/aleksis/apps/alsijil/preferences.py index 6ebcb2ae2ecae7f796c67a00c36dd6e0aafcceba..3ad751865f0534a156fd8ea7f9bfc0b558a467f8 100644 --- a/aleksis/apps/alsijil/preferences.py +++ b/aleksis/apps/alsijil/preferences.py @@ -1,7 +1,8 @@ +from django.core.exceptions import ValidationError from django.utils.translation import gettext as _ from dynamic_preferences.preferences import Section -from dynamic_preferences.types import BooleanPreference +from dynamic_preferences.types import BooleanPreference, IntegerPreference from aleksis.core.registries import person_preferences_registry, site_preferences_registry @@ -109,3 +110,17 @@ class ShowGroupRolesInLessonView(BooleanPreference): 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.")) diff --git a/aleksis/apps/alsijil/rules.py b/aleksis/apps/alsijil/rules.py index 72ddda20ce1eca248ff51030ae5d8cb44504d69d..1a8a06adc1033d690d0b20cfbbe92869621767bc 100644 --- a/aleksis/apps/alsijil/rules.py +++ b/aleksis/apps/alsijil/rules.py @@ -1,6 +1,8 @@ from rules import add_perm +from aleksis.core.models import Group from aleksis.core.util.predicates import ( + has_any_object, has_global_perm, has_object_perm, has_person, @@ -299,3 +301,9 @@ delete_group_role_assignment_predicate = ( has_global_perm("alsjil.assign_grouprole") | is_group_role_assignment_group_owner ) add_perm("alsijil.delete_grouproleassignment", delete_group_role_assignment_predicate) + +view_register_objects_list_predicate = has_person & ( + has_any_object("core.view_full_register_group", Group) + | has_global_perm("core.view_full_register") +) +add_perm("alsijil.view_register_objects_list", view_register_objects_list_predicate) diff --git a/aleksis/apps/alsijil/tables.py b/aleksis/apps/alsijil/tables.py index c3835e973164f5a6cd3b50540fa7b9f5a3d4ea43..336ca5827932b81fab534ab277ee5e2dab0aad6d 100644 --- a/aleksis/apps/alsijil/tables.py +++ b/aleksis/apps/alsijil/tables.py @@ -4,6 +4,8 @@ from django.utils.translation import gettext_lazy as _ import django_tables2 as tables from django_tables2.utils import A +from aleksis.core.util.tables import SelectColumn + class ExtraMarkTable(tables.Table): class Meta: @@ -78,3 +80,51 @@ class GroupRoleTable(tables.Table): self.columns.hide("edit") if not request.user.has_perm("alsijil.delete_grouprole"): self.columns.hide("delete") + + +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): + return render_to_string( + "alsijil/partials/lesson_status_icon.html", + dict( + week=record.get("week"), + has_documentation=record.get("has_documentation", False), + substitution=record.get("substitution"), + register_object=value, + ), + ) + + +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/templates/alsijil/class_register/all_objects.html b/aleksis/apps/alsijil/templates/alsijil/class_register/all_objects.html new file mode 100644 index 0000000000000000000000000000000000000000..b5e57e0ee276ab395ebd824327c7584a063dba63 --- /dev/null +++ b/aleksis/apps/alsijil/templates/alsijil/class_register/all_objects.html @@ -0,0 +1,19 @@ +{# -*- 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/person.html b/aleksis/apps/alsijil/templates/alsijil/class_register/person.html index 7254e0e24be0f979f9fa1f01a05a0b4c8f10e532..96729befb2851ba6194c29ce1b1280d04033d6bd 100644 --- a/aleksis/apps/alsijil/templates/alsijil/class_register/person.html +++ b/aleksis/apps/alsijil/templates/alsijil/class_register/person.html @@ -1,9 +1,6 @@ {# -*- engine:django -*- #} {% extends "core/base.html" %} -{% load rules %} -{% load data_helpers %} -{% load week_helpers %} -{% load i18n %} +{% load rules data_helpers week_helpers i18n material_form django_tables2 %} {% block browser_title %}{% blocktrans %}Class register: person{% endblocktrans %}{% endblock %} @@ -12,7 +9,7 @@ {% has_perm "alsijil.view_my_students" user as has_students %} {% if has_students %} <a href="{% url "my_students" %}" - class="btn-flat primary-color-text waves-light waves-effect"> + class="btn-flat primary-color-text waves-light waves-effect"> <i class="material-icons left">chevron_left</i> {% trans "Back" %} </a> {% endif %} @@ -22,249 +19,271 @@ {% endblock %} {% block content %} - {% has_perm "alsijil.edit_person_overview_personalnote" user person as can_mark_all_as_excused %} - {% has_perm "alsijil.register_absence" user person as can_register_absence %} - {% if can_register_absence %} - <a class="btn primary-color waves-effect waves-light" href="{% url "register_absence" person.pk %}"> - <i class="material-icons left">rate_review</i> - {% trans "Register absence" %} - </a> - {% endif %} - <div class="row"> - <div class="col s12 m12 l6"> - <h5>{% trans "Unexcused absences" %}</h5> - - <ul class="collection"> - {% for note in unexcused_absences %} - <li class="collection-item"> - {% has_perm "alsijil.edit_personalnote" user note as can_edit_personal_note %} - {% if can_edit_personal_note %} - <form action="" method="post" class="right hide-on-small-only" style="margin-top: -7px;"> - {% csrf_token %} - {% trans "Mark as" %} - <input type="hidden" value="{{ note.pk }}" name="personal_note"> - {% include "alsijil/partials/mark_as_buttons.html" %} - <a class="btn-flat red-text" title="{% trans "Delete note" %}" - href="{% url "delete_personal_note" note.pk %}"> - <i class="material-icons center">cancel</i> - </a> - </form> - {% endif %} - <i class="material-icons left red-text">warning</i> - <p class="no-margin"> - <a href="{{ note.get_absolute_url }}">{{ note.date }}, {{ note.lesson_period }}</a> - </p> - {% if note.remarks %} - <p class="no-margin"><em>{{ note.remarks }}</em></p> - {% endif %} - {% if can_edit_personal_note %} - <form action="" method="post" class="hide-on-med-and-up"> - {% csrf_token %} - {% trans "Mark as" %} - <input type="hidden" value="{{ note.pk }}" name="personal_note"> - {% include "alsijil/partials/mark_as_buttons.html" %} - <a class="btn-flat red-text" title="{% trans "Delete note" %}" - href="{% url "delete_personal_note" note.pk %}"> - <i class="material-icons center">cancel</i> - </a> - </form> - {% endif %} - </li> - {% empty %} - <li class="collection-item avatar valign-wrapper"> - <i class="material-icons left materialize-circle green white-text">check</i> - <span class="title">{% trans "There are no unexcused lessons." %}</span> + <div class="col s12"> + <ul class="tabs"> + {% if register_object_table %} + <li class="tab"> + <a href="#lesson-documentations">{% trans "Lesson documentations" %}</a> </li> - {% endfor %} + {% endif %} + <li class="tab"> + <a href="#personal-notes">{% trans "Personal notes" %}</a> + </li> </ul> - {% if stats %} - <h5>{% trans "Statistics on absences, tardiness and remarks" %}</h5> - <ul class="collapsible"> - {% for school_term, stat in stats %} - <li {% if forloop.first %}class="active"{% endif %}> - <div class="collapsible-header"> - <i class="material-icons">date_range</i>{{ school_term }}</div> - <div class="collapsible-body"> - <table> - <tr> - <th colspan="2">{% trans 'Absences' %}</th> - <td>{{ stat.absences_count }}</td> - </tr> - <tr> - <td rowspan="{{ excuse_types.count|add:2 }}" class="hide-on-small-only">{% trans "thereof" %}</td> - <td rowspan="{{ excuse_types.count|add:2 }}" class="hide-on-med-and-up"></td> - <th class="truncate">{% trans 'Excused' %}</th> - <td>{{ stat.excused }}</td> - </tr> - {% for excuse_type in excuse_types %} - <th>{{ excuse_type.name }}</th> - <td>{{ stat|get_dict:excuse_type.count_label }}</td> - {% endfor %} - <tr> - <th>{% trans 'Unexcused' %}</th> - <td>{{ stat.unexcused }}</td> - </tr> - <tr> - <th colspan="2">{% trans 'Tardiness' %}</th> - <td>{{ stat.tardiness }}'/{{ stat.tardiness_count }} ×</td> - </tr> - {% for extra_mark in extra_marks %} - <tr> - <th colspan="2">{{ extra_mark.name }}</th> - <td>{{ stat|get_dict:extra_mark.count_label }}</td> - </tr> - {% endfor %} - </table> - </div> - </li> - {% endfor %} - </ul> - {% endif %} </div> - <div class="col s12 m12 l6"> - <h5>{% trans "Relevant personal notes" %}</h5> - <ul class="collapsible"> - <li> - <div> - <ul> - {% for note in personal_notes %} - {% ifchanged note.school_term %}</ul></div></li> - <li {% if forloop.first %}class="active"{% endif %}> - <div class="collapsible-header"><i - class="material-icons">date_range</i>{{ note.school_term }}</div> - <div class="collapsible-body"> - <ul class="collection"> - {% endifchanged %} - - {% ifchanged note.week %} - <li class="collection-item"> - <strong>{% blocktrans with week=note.calendar_week.week %}Week {{ week }}{% endblocktrans %}</strong> - </li> - {% endifchanged %} - {% ifchanged note.date %} - <li class="collection-item"> - {% if can_mark_all_as_excused and note.date %} - <form action="" method="post" class="right hide-on-small-only" style="margin-top: -7px;"> - {% csrf_token %} - {% trans "Mark all as" %} - <input type="hidden" value="{{ note.date|date:"Y-m-d" }}" name="date"> - {% include "alsijil/partials/mark_as_buttons.html" %} - </form> - {% endif %} - <i class="material-icons left">schedule</i> + {% 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 %} + <div class="col s12" id="personal-notes"> + {% has_perm "alsijil.edit_person_overview_personalnote" user person as can_mark_all_as_excused %} + {% has_perm "alsijil.register_absence" user person as can_register_absence %} + {% if can_register_absence %} + <a class="btn primary-color waves-effect waves-light" href="{% url "register_absence" person.pk %}"> + <i class="material-icons left">rate_review</i> + {% trans "Register absence" %} + </a> + {% endif %} - {% if note.date %} - {{ note.date }} - {% else %} - {{ note.register_object.date_start }} - {{ note.register_object.period_from.period }}.–{{ note.register_object.date_end }} - {{ note.register_object.period_to.period }}. - {% endif %} + <div class="row"> + <div class="col s12 m12 l6"> + <h5>{% trans "Unexcused absences" %}</h5> - {% if can_mark_all_as_excused and note.date %} - <form action="" method="post" class="hide-on-med-and-up"> - {% csrf_token %} - {% trans "Mark all as" %} - <input type="hidden" value="{{ note.date|date:"Y-m-d" }}" name="date"> - {% include "alsijil/partials/mark_as_buttons.html" %} - </form> - {% endif %} - </li> - {% endifchanged %} + <ul class="collection"> + {% for note in unexcused_absences %} + <li class="collection-item"> + {% has_perm "alsijil.edit_personalnote" user note as can_edit_personal_note %} + {% if can_edit_personal_note %} + <form action="" method="post" class="right hide-on-small-only" style="margin-top: -7px;"> + {% csrf_token %} + {% trans "Mark as" %} + <input type="hidden" value="{{ note.pk }}" name="personal_note"> + {% include "alsijil/partials/mark_as_buttons.html" %} + <a class="btn-flat red-text" title="{% trans "Delete note" %}" + href="{% url "delete_personal_note" note.pk %}"> + <i class="material-icons center">cancel</i> + </a> + </form> + {% endif %} + <i class="material-icons left red-text">warning</i> + <p class="no-margin"> + <a href="{{ note.get_absolute_url }}">{{ note.date }}, {{ note.lesson_period }}</a> + </p> + {% if note.remarks %} + <p class="no-margin"><em>{{ note.remarks }}</em></p> + {% endif %} + {% if can_edit_personal_note %} + <form action="" method="post" class="hide-on-med-and-up"> + {% csrf_token %} + {% trans "Mark as" %} + <input type="hidden" value="{{ note.pk }}" name="personal_note"> + {% include "alsijil/partials/mark_as_buttons.html" %} + <a class="btn-flat red-text" title="{% trans "Delete note" %}" + href="{% url "delete_personal_note" note.pk %}"> + <i class="material-icons center">cancel</i> + </a> + </form> + {% endif %} + </li> + {% empty %} + <li class="collection-item avatar valign-wrapper"> + <i class="material-icons left materialize-circle green white-text">check</i> + <span class="title">{% trans "There are no unexcused lessons." %}</span> + </li> + {% endfor %} + </ul> + {% if stats %} + <h5>{% trans "Statistics on absences, tardiness and remarks" %}</h5> + <ul class="collapsible"> + {% for school_term, stat in stats %} + <li {% if forloop.first %}class="active"{% endif %}> + <div class="collapsible-header"> + <i class="material-icons">date_range</i>{{ school_term }}</div> + <div class="collapsible-body"> + <table> + <tr> + <th colspan="2">{% trans 'Absences' %}</th> + <td>{{ stat.absences_count }}</td> + </tr> + <tr> + <td rowspan="{{ excuse_types.count|add:2 }}" class="hide-on-small-only">{% trans "thereof" %}</td> + <td rowspan="{{ excuse_types.count|add:2 }}" class="hide-on-med-and-up"></td> + <th class="truncate">{% trans 'Excused' %}</th> + <td>{{ stat.excused }}</td> + </tr> + {% for excuse_type in excuse_types %} + <th>{{ excuse_type.name }}</th> + <td>{{ stat|get_dict:excuse_type.count_label }}</td> + {% endfor %} + <tr> + <th>{% trans 'Unexcused' %}</th> + <td>{{ stat.unexcused }}</td> + </tr> + <tr> + <th colspan="2">{% trans 'Tardiness' %}</th> + <td>{{ stat.tardiness }}'/{{ stat.tardiness_count }} ×</td> + </tr> + {% for extra_mark in extra_marks %} + <tr> + <th colspan="2">{{ extra_mark.name }}</th> + <td>{{ stat|get_dict:extra_mark.count_label }}</td> + </tr> + {% endfor %} + </table> + </div> + </li> + {% endfor %} + </ul> + {% endif %} + </div> + <div class="col s12 m12 l6"> + <h5>{% trans "Relevant personal notes" %}</h5> + <ul class="collapsible"> + <li> + <div> + <ul> + {% for note in personal_notes %} + {% ifchanged note.school_term %}</ul></div></li> + <li {% if forloop.first %}class="active"{% endif %}> + <div class="collapsible-header"><i + class="material-icons">date_range</i>{{ note.school_term }}</div> + <div class="collapsible-body"> + <ul class="collection"> + {% endifchanged %} - <li class="collection-item"> - <div class="row no-margin"> - <div class="col s2 m1"> - {% if note.register_object.period %} - {{ note.register_object.period.period }}. - {% endif %} - </div> + {% ifchanged note.week %} + <li class="collection-item"> + <strong>{% blocktrans with week=note.calendar_week.week %}Week + {{ week }}{% endblocktrans %}</strong> + </li> + {% endifchanged %} + {% ifchanged note.date %} + <li class="collection-item"> + {% if can_mark_all_as_excused and note.date %} + <form action="" method="post" class="right hide-on-small-only" style="margin-top: -7px;"> + {% csrf_token %} + {% trans "Mark all as" %} + <input type="hidden" value="{{ note.date|date:"Y-m-d" }}" name="date"> + {% include "alsijil/partials/mark_as_buttons.html" %} + </form> + {% endif %} + <i class="material-icons left">schedule</i> - <div class="col s10 m4"> - <i class="material-icons left">event_note</i> - <a href="{{ note.get_absolute_url }}"> - {% if note.register_object.get_subject %} - {{ note.register_object.get_subject.name }} + {% if note.date %} + {{ note.date }} {% else %} - {% trans "Event" %} ({{ note.register_object.title }}) - {% endif %}<br/> - {{ note.register_object.teacher_names }} - </a> - </div> + {{ note.register_object.date_start }} + {{ note.register_object.period_from.period }}.–{{ note.register_object.date_end }} + {{ note.register_object.period_to.period }}. + {% endif %} - <div class="col s12 m7 no-padding"> - {% has_perm "alsijil.edit_personalnote" user note as can_edit_personal_note %} - {% if note.absent and not note.excused and can_edit_personal_note %} - <form action="" method="post" class="right hide-on-small-only" style="margin-top: -7px;"> - {% csrf_token %} - {% trans "Mark as" %} - <input type="hidden" value="{{ note.pk }}" name="personal_note"> - {% include "alsijil/partials/mark_as_buttons.html" %} - <a class="btn-flat red-text" title="{% trans "Delete note" %}" - href="{% url "delete_personal_note" note.pk %}"> - <i class="material-icons center">cancel</i> - </a> - </form> - {% elif can_edit_personal_note %} - <a class="btn-flat red-text right hide-on-small-only" title="{% trans "Delete note" %}" - href="{% url "delete_personal_note" note.pk %}"> - <i class="material-icons center">cancel</i> - </a> - {% endif %} + {% if can_mark_all_as_excused and note.date %} + <form action="" method="post" class="hide-on-med-and-up"> + {% csrf_token %} + {% trans "Mark all as" %} + <input type="hidden" value="{{ note.date|date:"Y-m-d" }}" name="date"> + {% include "alsijil/partials/mark_as_buttons.html" %} + </form> + {% endif %} + </li> + {% endifchanged %} - {% if note.absent %} - <div class="chip red white-text"> - {% trans 'Absent' %} - </div> - {% endif %} - {% if note.excused %} - <div class="chip green white-text"> - {% if note.excuse_type %} - {{ note.excuse_type.name }} - {% else %} - {% trans 'Excused' %} + <li class="collection-item"> + <div class="row no-margin"> + <div class="col s2 m1"> + {% if note.register_object.period %} + {{ note.register_object.period.period }}. {% endif %} </div> - {% endif %} - {% if note.late %} - <div class="chip orange white-text"> - {% blocktrans with late=note.late %}{{ late }}' late{% endblocktrans %} + <div class="col s10 m4"> + <i class="material-icons left">event_note</i> + <a href="{{ note.get_absolute_url }}"> + {% if note.register_object.get_subject %} + {{ note.register_object.get_subject.name }} + {% else %} + {% trans "Event" %} ({{ note.register_object.title }}) + {% endif %}<br/> + {{ note.register_object.teacher_names }} + </a> </div> - {% endif %} - {% for extra_mark in note.extra_marks.all %} - <div class="chip">{{ extra_mark.name }}</div> - {% endfor %} + <div class="col s12 m7 no-padding"> + {% has_perm "alsijil.edit_personalnote" user note as can_edit_personal_note %} + {% if note.absent and not note.excused and can_edit_personal_note %} + <form action="" method="post" class="right hide-on-small-only" style="margin-top: -7px;"> + {% csrf_token %} + {% trans "Mark as" %} + <input type="hidden" value="{{ note.pk }}" name="personal_note"> + {% include "alsijil/partials/mark_as_buttons.html" %} + <a class="btn-flat red-text" title="{% trans "Delete note" %}" + href="{% url "delete_personal_note" note.pk %}"> + <i class="material-icons center">cancel</i> + </a> + </form> + {% elif can_edit_personal_note %} + <a class="btn-flat red-text right hide-on-small-only" title="{% trans "Delete note" %}" + href="{% url "delete_personal_note" note.pk %}"> + <i class="material-icons center">cancel</i> + </a> + {% endif %} - <em>{{ note.remarks }}</em> + {% if note.absent %} + <div class="chip red white-text"> + {% trans 'Absent' %} + </div> + {% endif %} + {% if note.excused %} + <div class="chip green white-text"> + {% if note.excuse_type %} + {{ note.excuse_type.name }} + {% else %} + {% trans 'Excused' %} + {% endif %} + </div> + {% endif %} - </div> - <div class="col s12 hide-on-med-and-up"> - {% if note.absent and not note.excused and can_edit_personal_note %} - <form action="" method="post"> - {% csrf_token %} - {% trans "Mark as" %} - <input type="hidden" value="{{ note.pk }}" name="personal_note"> - {% include "alsijil/partials/mark_as_buttons.html" %} - <a class="btn-flat red-text" title="{% trans "Delete note" %}" - href="{% url "delete_personal_note" note.pk %}"> - <i class="material-icons center">cancel</i> - </a> - </form> - {% elif can_edit_personal_note %} - <a class="btn-flat red-text" title="{% trans "Delete note" %}" - href="{% url "delete_personal_note" note.pk %}"> - <i class="material-icons left">cancel</i> - {% trans "Delete" %} - </a> - {% endif %} - </div> - </li> - {% endfor %} - </li> - </ul> - </div> + {% if note.late %} + <div class="chip orange white-text"> + {% blocktrans with late=note.late %}{{ late }}' late{% endblocktrans %} + </div> + {% endif %} + + {% for extra_mark in note.extra_marks.all %} + <div class="chip">{{ extra_mark.name }}</div> + {% endfor %} + + <em>{{ note.remarks }}</em> + + </div> + <div class="col s12 hide-on-med-and-up"> + {% if note.absent and not note.excused and can_edit_personal_note %} + <form action="" method="post"> + {% csrf_token %} + {% trans "Mark as" %} + <input type="hidden" value="{{ note.pk }}" name="personal_note"> + {% include "alsijil/partials/mark_as_buttons.html" %} + <a class="btn-flat red-text" title="{% trans "Delete note" %}" + href="{% url "delete_personal_note" note.pk %}"> + <i class="material-icons center">cancel</i> + </a> + </form> + {% elif can_edit_personal_note %} + <a class="btn-flat red-text" title="{% trans "Delete note" %}" + href="{% url "delete_personal_note" note.pk %}"> + <i class="material-icons left">cancel</i> + {% trans "Delete" %} + </a> + {% endif %} + </div> + </li> + {% endfor %} + </li> + </ul> + </div> + </div> + </div> </div> {% endblock %} diff --git a/aleksis/apps/alsijil/templates/alsijil/notifications/check.html b/aleksis/apps/alsijil/templates/alsijil/notifications/check.html new file mode 100644 index 0000000000000000000000000000000000000000..d76a1a0a5abfc6fb8b7722d5a4cf16ff927f069a --- /dev/null +++ b/aleksis/apps/alsijil/templates/alsijil/notifications/check.html @@ -0,0 +1,4 @@ +{% 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_status_icon.html b/aleksis/apps/alsijil/templates/alsijil/partials/lesson_status_icon.html index ceff8c1a17ea0452b1f013d759dbee7edfd6c630..52f55e9723c3b9650bcd09f63725467a75f8993d 100644 --- a/aleksis/apps/alsijil/templates/alsijil/partials/lesson_status_icon.html +++ b/aleksis/apps/alsijil/templates/alsijil/partials/lesson_status_icon.html @@ -2,7 +2,7 @@ {% now_datetime as now_dt %} -{% if register_object.has_documentation %} +{% if has_documentation or register_object.has_documentation %} <i class="material-icons green{% firstof color_suffix "-text"%} tooltipped {{ css_class }}" data-position="bottom" data-tooltip="{% trans "Data complete" %}" title="{% trans "Data complete" %}">check_circle</i> {% elif not register_object.period %} {% period_to_time_start week register_object.raw_period_from_on_day as time_start %} @@ -19,13 +19,13 @@ {% period_to_time_start week register_object.period as time_start %} {% period_to_time_end week register_object.period as time_end %} - {% if register_object.get_substitution.cancelled %} + {% if substitution.cancelled or register_object.get_substitution.cancelled %} <i class="material-icons red{% firstof color_suffix "-text"%} tooltipped {{ css_class }}" data-position="bottom" data-tooltip="{% trans "Lesson cancelled" %}" title="{% trans "Lesson cancelled" %}">cancel</i> {% elif now_dt > time_end %} <i class="material-icons red{% firstof color_suffix "-text"%} tooltipped {{ css_class }}" data-position="bottom" data-tooltip="{% trans "Missing data" %}" title="{% trans "Missing data" %}">history</i> {% elif now_dt > time_start and now_dt < time_end %} <i class="material-icons orange{% firstof color_suffix "-text"%} tooltipped {{ css_class }}" data-position="bottom" data-tooltip="{% trans "Pending" %}" title="{% trans "Pending" %}">more_horiz</i> - {% elif register_object.get_substitution %} + {% elif substitution or register_object.get_substitution %} <i class="material-icons orange{% firstof color_suffix "-text"%} tooltipped {{ css_class }}" data-position="bottom" data-tooltip="{% trans "Substitution" %}" title="{% trans "Substitution" %}">update</i> {% endif %} {% endif %} diff --git a/aleksis/apps/alsijil/templates/alsijil/partials/objects_table.html b/aleksis/apps/alsijil/templates/alsijil/partials/objects_table.html new file mode 100644 index 0000000000000000000000000000000000000000..f13e6043935b6fe5b1e91915158cc12d39a8f2e0 --- /dev/null +++ b/aleksis/apps/alsijil/templates/alsijil/partials/objects_table.html @@ -0,0 +1,43 @@ +{% load i18n material_form django_tables2 %} +<div class="card"> + <div class="card-content"> + <div class="card-title">{% trans "Lesson filter" %}</div> + <form action="" method="get"> + {% form form=filter_form %}{% endform %} + <button type="submit" class="btn waves-effect waves-light"> + <i class="material-icons left">refresh</i> + {% trans "Update filters" %} + </button> + </form> + </div> +</div> + +{% if table %} + <div class="card"> + <div class="card-content"> + <form action="" method="post"> + {% csrf_token %} + <div class="row"> + <div class="col s12 {% if action_form %}m4 l4 xl6{% endif %}"> + <div class="card-title">{% trans "Lesson table" %}</div> + </div> + {% if action_form %} + <div class="col s12 m8 l8 xl6"> + <div class="col s12 m8"> + {% form form=action_form %}{% endform %} + </div> + <div class="col s12 m4"> + <button type="submit" class="btn waves-effect waves-primary"> + {% trans "Execute" %} + <i class="material-icons right">send</i> + </button> + </div> + </div> + {% endif %} + </div> + {% render_table table %} + + </form> + </div> + </div> +{% endif %} diff --git a/aleksis/apps/alsijil/urls.py b/aleksis/apps/alsijil/urls.py index 16ba0d0b9c5c4c976c26222a8542d3e96a93b1aa..8fb31b625ba068fc3c6ba731fe39155f9974d76e 100644 --- a/aleksis/apps/alsijil/urls.py +++ b/aleksis/apps/alsijil/urls.py @@ -100,4 +100,5 @@ urlpatterns = [ views.AssignGroupRoleMultipleView.as_view(), name="assign_group_role_multiple", ), + path("all/", views.AllRegisterObjectsView.as_view(), name="all_register_objects"), ] diff --git a/aleksis/apps/alsijil/util/alsijil_helpers.py b/aleksis/apps/alsijil/util/alsijil_helpers.py index 9aedcb24ea2c8dd2c529d273b701b082281786ea..b5879b1f2ddcb79dbe336c9c4285ddccc30f23f6 100644 --- a/aleksis/apps/alsijil/util/alsijil_helpers.py +++ b/aleksis/apps/alsijil/util/alsijil_helpers.py @@ -1,14 +1,19 @@ -from typing import List, Optional, Union +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, LessonPeriod +from aleksis.apps.chronos.models import Event, ExtraLesson, Holiday, LessonPeriod from aleksis.apps.chronos.util.chronos_helpers import get_el_by_pk @@ -99,3 +104,281 @@ def register_objects_sorter(register_object: Union[LessonPeriod, Event, ExtraLes 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 = [] + date_start = lesson_periods.first().lesson.validity.date_start + date_end = lesson_periods.last().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 = [] + for lesson_period in lesson_periods: + 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 + # or substitution teacher of this lesson period + if filter_dict.get("person") and ( + filter_dict.get("person") not in lesson_period.lesson.teachers.all() and not sub + ): + continue + + teachers = ( + sub.teacher_names + if sub and sub.teachers.all() + else lesson_period.lesson.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.day) + day_sort = register_object.day + period = f"{register_object.period.period}." + period_sort = register_object.period.period + else: + 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"]) + 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 [] diff --git a/aleksis/apps/alsijil/views.py b/aleksis/apps/alsijil/views.py index f3990be4fc351f55607a83a014d7d0cfacf3504e..31ad878230fef101e954d1a1683dc0e9d1a62377 100644 --- a/aleksis/apps/alsijil/views.py +++ b/aleksis/apps/alsijil/views.py @@ -13,12 +13,14 @@ from django.urls import reverse, reverse_lazy from django.utils import timezone from django.utils.decorators import method_decorator from django.utils.translation import ugettext 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 SingleTableView +from django_tables2 import RequestConfig, SingleTableView +from guardian.shortcuts import get_objects_for_user from reversion.views import RevisionMixin from rules.contrib.views import PermissionRequiredMixin, permission_required @@ -35,16 +37,19 @@ from aleksis.core.mixins import ( from aleksis.core.models import Group, Person, SchoolTerm from aleksis.core.util import messages from aleksis.core.util.core_helpers import get_site_preferences, objectgetter_optional +from aleksis.core.util.predicates import check_global_permission from .forms import ( AssignGroupRoleForm, ExcuseTypeForm, ExtraMarkForm, + FilterRegisterObjectForm, GroupRoleAssignmentEditForm, GroupRoleForm, LessonDocumentationForm, PersonalNoteFormSet, RegisterAbsenceForm, + RegisterObjectActionForm, SelectForm, ) from .models import ( @@ -55,9 +60,16 @@ from .models import ( LessonDocumentation, PersonalNote, ) -from .tables import ExcuseTypeTable, ExtraMarkTable, GroupRoleTable +from .tables import ( + ExcuseTypeTable, + ExtraMarkTable, + GroupRoleTable, + RegisterObjectSelectTable, + RegisterObjectTable, +) from .util.alsijil_helpers import ( annotate_documentations, + generate_list_of_all_register_objects, get_register_object_by_pk, get_timetable_instance_by_pk, register_objects_sorter, @@ -898,6 +910,20 @@ def overview_person(request: HttpRequest, id_: Optional[int] = None) -> HttpResp context["excuse_types"] = excuse_types context["extra_marks"] = extra_marks + # 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=True) + filter_dict = filter_form.cleaned_data if filter_form.is_valid() else {} + filter_dict["person"] = person + context["filter_form"] = filter_form + + register_objects = generate_list_of_all_register_objects(filter_dict) + if register_objects: + 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) @@ -1236,3 +1262,51 @@ class GroupRoleAssignmentDeleteView( def get_success_url(self) -> str: pk = self.object.groups.first().pk return reverse("assigned_group_roles", args=[pk]) + + +class AllRegisterObjectsView(PermissionRequiredMixin, View): + """Provide overview of all register objects for coordinators.""" + + permission_required = "alsijil.view_register_objects_list" + + 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)