diff --git a/aleksis/apps/alsijil/forms.py b/aleksis/apps/alsijil/forms.py index 228e825bae4e1170a3327629b6c8a973c192f7c0..d1d47cb0bb23259aed7695c81cd0ed4ca60d5190 100644 --- a/aleksis/apps/alsijil/forms.py +++ b/aleksis/apps/alsijil/forms.py @@ -1,8 +1,9 @@ -from datetime import datetime +from datetime import datetime, timedelta from django import forms from django.core.exceptions import ValidationError from django.db.models import Count, Q +from django.utils import timezone from django.utils.translation import gettext_lazy as _ from django_select2.forms import ModelSelect2MultipleWidget, ModelSelect2Widget, Select2Widget @@ -10,8 +11,8 @@ 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.models import Group, Person, SchoolTerm from aleksis.core.util.core_helpers import get_site_preferences from aleksis.core.util.predicates import check_global_permission @@ -254,3 +255,64 @@ class GroupRoleAssignmentEditForm(forms.ModelForm): class Meta: model = GroupRoleAssignment fields = ["date_start", "date_end"] + + +class FilterRegisterObjectForm(forms.Form): + 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, for_person=True, *args, **kwargs): + self.request = request + person = self.request.user.person + + # Fill in initial data + kwargs["initial"] = self.get_initial() + super().__init__(*args, **kwargs) + + # Build querysets + self.fields["school_term"].queryset = SchoolTerm.objects.all() + + # Filter selectable groups by permissions + group_qs = Group.objects.all() + if for_person: + group_qs = group_qs.filter( + Q(lessons__teachers=person) + | Q(lessons__lesson_periods__substitutions__teachers=person) + | Q(events__teachers=person) + | Q(extra_lessons__teachers=person) + ) + elif not check_global_permission(self.request.user, "alsijil.view_full_register"): + group_qs = group_qs.union( + group_qs.filter( + pk__in=get_objects_for_user( + self.request.user, "core.view_full_register_group", Group + ).values_list("pk", flat=True) + ) + ) + + # Flatten query by filtering groups by pk + groups_flat = Group.objects.filter(pk__in=list(group_qs.values_list("pk", flat=True))) + self.fields["group"].queryset = groups_flat + + # Filter subjects by selectable groups + subject_qs = Subject.objects.filter( + Q(lessons__groups__in=groups_flat) | Q(extra_lessons__groups__in=groups_flat) + ).distinct() + self.fields["subject"].queryset = subject_qs diff --git a/aleksis/apps/alsijil/tables.py b/aleksis/apps/alsijil/tables.py index c3835e973164f5a6cd3b50540fa7b9f5a3d4ea43..d78f899ba9cd964cad239616b52a55fde447df47 100644 --- a/aleksis/apps/alsijil/tables.py +++ b/aleksis/apps/alsijil/tables.py @@ -78,3 +78,28 @@ class GroupRoleTable(tables.Table): self.columns.hide("edit") if not request.user.has_perm("alsijil.delete_grouprole"): self.columns.hide("delete") + + +class RegisterObjectTable(tables.Table): + class Meta: + attrs = {"class": "highlight responsive-table"} + + status = tables.Column(accessor="register_object") + date = tables.Column(order_by="date_sort") + period = tables.Column(order_by="period_sort") + groups = tables.Column() + subject = tables.Column() + topic = tables.Column() + homework = tables.Column() + group_note = tables.Column() + + 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, + ), + ) diff --git a/aleksis/apps/alsijil/templates/alsijil/class_register/person.html b/aleksis/apps/alsijil/templates/alsijil/class_register/person.html index 7254e0e24be0f979f9fa1f01a05a0b4c8f10e532..5302e24890221a731940bbe7999675bbc92bf143 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 %} @@ -22,249 +19,280 @@ {% 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"> + <h5>{% trans "Lesson filter" %}</h5> + <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> + <h5>{% trans "Lesson table" %}</h5> + {% render_table register_object_table %} + </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/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/util/alsijil_helpers.py b/aleksis/apps/alsijil/util/alsijil_helpers.py index 9aedcb24ea2c8dd2c529d273b701b082281786ea..3d30d2395a017c4338a96ee5625f235d9c1aea30 100644 --- a/aleksis/apps/alsijil/util/alsijil_helpers.py +++ b/aleksis/apps/alsijil/util/alsijil_helpers.py @@ -1,15 +1,26 @@ +from operator import itemgetter from typing import List, Optional, 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, + LessonSubstitution, +) from aleksis.apps.chronos.util.chronos_helpers import get_el_by_pk +from aleksis.core.models import SchoolTerm def get_register_object_by_pk( @@ -99,3 +110,216 @@ def register_objects_sorter(register_object: Union[LessonPeriod, Event, ExtraLes return register_object.period_from_on_day else: return 0 + + +def generate_list_of_all_register_objects(filter_dict: dict) -> List[dict]: + # Get data for filtering + initial_filter_data = FilterRegisterObjectForm.get_initial() + # Always force a selected school term so that queries won't get to big + filter_school_term = filter_dict.get("school_term", SchoolTerm.current) + filter_person = filter_dict.get("person") + should_have_documentation = filter_dict.get("has_documentation") + filter_group = filter_dict.get("group") + filter_subject = filter_dict.get("subject") + filter_date_start = filter_dict.get("date_start", initial_filter_data.get("date_start")) + filter_date_end = filter_dict.get("date_end", initial_filter_data.get("date_end")) + filter_date = filter_date_start and filter_date_end + + # Get all holidays in the selected school term to sort all data in holidays out + holidays = Holiday.objects.within_dates( + filter_school_term.date_start, filter_school_term.date_end + ) + event_q = Q() + extra_lesson_q = Q() + holiday_days = [] + for holiday in holidays: + event_q = event_q | Q(date_start__lte=holiday.date_end, date_end__gte=holiday.date_start) + extra_lesson_q = extra_lesson_q | Q(day__gte=holiday.date_start, day__lte=holiday.date_end) + holiday_days += list(holiday.get_days()) + + lesson_periods = ( + LessonPeriod.objects.select_related("lesson") + .prefetch_related("lesson__teachers", "lesson__groups") + .filter(lesson__validity__school_term=filter_school_term) + .distinct() + .order_by("lesson__validity__school_term__date_start") + ) + events = Event.objects.filter(school_term=filter_school_term).exclude(event_q).distinct() + extra_lessons = ( + ExtraLesson.objects.annotate_day() + .filter(school_term=filter_school_term) + .exclude(extra_lesson_q) + .distinct() + ) + + # Do filtering by date, by person, by group and by subject (if activated) + if filter_date: + events = events.within_dates(filter_date_start, filter_date_end) + extra_lessons = extra_lessons.filter(day__gte=filter_date_start, day__lte=filter_date_end) + if filter_person: + lesson_periods = lesson_periods.filter( + Q(lesson__teachers=filter_person) | Q(substitutions__teachers=filter_person) + ) + events = events.filter_teacher(filter_person) + extra_lessons = extra_lessons.filter_teacher(filter_person) + if filter_group: + lesson_periods = lesson_periods.filter_group(filter_group) + events = events.filter_group(filter_group) + extra_lessons = extra_lessons.filter_group(filter_group) + if filter_subject: + lesson_periods = lesson_periods.filter( + Q(lesson__subject=filter_subject) | Q(substitutions__subject=filter_subject) + ) + # As events have no subject, we exclude them at all + events = [] + extra_lessons = extra_lessons.filter(subject=filter_subject) + + # 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) + ) + substitutions = LessonSubstitution.objects.filter(lesson_period__in=lesson_periods) + if filter_person: + substitutions = substitutions.filter(teachers=filter_person) + + if lesson_periods: + # Get date range for which lesson periods should be added + date_start = lesson_periods.first().lesson.validity.school_term.date_start + date_end = lesson_periods.last().lesson.validity.school_term.date_end + if filter_date and filter_date_start > date_start and filter_date_start < date_end: + date_start = filter_date_start + if filter_date and filter_date_end < date_end and filter_date_start > date_start: + date_end = filter_date_end + print(date_start, 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_date or (filter_date_start <= day <= filter_date_end)): + + filtered_substitutions = list( + filter(lambda s: s.lesson_period_id == lesson_period.id, substitutions) + ) + # Skip lesson period if the person isn't a teacher + # or substitution teacher of this lesson period + if filter_person and ( + filter_person not in lesson_period.lesson.teachers.all() + and not filtered_substitutions + ): + continue + + # Annotate substitution to lesson period + sub = filtered_substitutions[0] if filtered_substitutions else None + + subject = sub.subject if sub and sub.subject else lesson_period.lesson.subject + + if filter_subject and filter_subject != 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, + ) + ) + has_documentation = bool(filtered_documentations) + + if ( + should_have_documentation is not None + and has_documentation != should_have_documentation + ): + continue + + # Build table entry + entry = { + "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, + "subject": 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) + + for register_object in list(extra_lessons) + list(events): + filtered_documentations = list( + filter( + lambda d: getattr(d, f"{register_object.label_}_id") == register_object.pk, + documentations, + ) + ) + has_documentation = bool(filtered_documentations) + + if ( + should_have_documentation is not None + and has_documentation != should_have_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 = { + "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, + "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) + + # 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 b2378f891f579b328c9c51114a7d212adba5d1ec..6a597bbfdaf7756ff7df28342048762f061b7505 100644 --- a/aleksis/apps/alsijil/views.py +++ b/aleksis/apps/alsijil/views.py @@ -18,7 +18,7 @@ from django.views.generic import DetailView import reversion from calendarweek import CalendarWeek -from django_tables2 import SingleTableView +from django_tables2 import RequestConfig, SingleTableView from reversion.views import RevisionMixin from rules.contrib.views import PermissionRequiredMixin, permission_required @@ -40,6 +40,7 @@ from .forms import ( AssignGroupRoleForm, ExcuseTypeForm, ExtraMarkForm, + FilterRegisterObjectForm, GroupRoleAssignmentEditForm, GroupRoleForm, LessonDocumentationForm, @@ -55,9 +56,10 @@ from .models import ( LessonDocumentation, PersonalNote, ) -from .tables import ExcuseTypeTable, ExtraMarkTable, GroupRoleTable +from .tables import ExcuseTypeTable, ExtraMarkTable, GroupRoleTable, 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, @@ -894,6 +896,16 @@ 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, True, request.GET or None) + 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) + RequestConfig(request,).configure(table) # paginate={"per_page": 100} + context["register_object_table"] = table return render(request, "alsijil/class_register/person.html", context)