diff --git a/aleksis/apps/alsijil/forms.py b/aleksis/apps/alsijil/forms.py index dde0bc501f3a3aa4726561034625c52742859ac5..f27596eef71d1c934296e6643b7af280558320c1 100644 --- a/aleksis/apps/alsijil/forms.py +++ b/aleksis/apps/alsijil/forms.py @@ -11,7 +11,13 @@ from material import Layout, Row from aleksis.apps.chronos.managers import TimetableType from aleksis.core.models import Group, Person -from .models import ExtraMark, LessonDocumentation, PersonalNote, PersonalNoteFilter +from .models import ( + ExcuseType, + ExtraMark, + LessonDocumentation, + PersonalNote, + PersonalNoteFilter, +) class LessonDocumentationForm(forms.ModelForm): @@ -28,7 +34,7 @@ class LessonDocumentationForm(forms.ModelForm): class PersonalNoteForm(forms.ModelForm): class Meta: model = PersonalNote - fields = ["absent", "late", "excused", "extra_marks", "remarks"] + fields = ["absent", "late", "excused", "excuse_type", "extra_marks", "remarks"] person_name = forms.CharField(disabled=True) @@ -47,10 +53,7 @@ class SelectForm(forms.Form): layout = Layout(Row("group", "teacher")) group = forms.ModelChoiceField( - queryset=None, - label=_("Group"), - required=False, - widget=Select2Widget, + queryset=None, label=_("Group"), required=False, widget=Select2Widget, ) teacher = forms.ModelChoiceField( queryset=Person.objects.annotate( @@ -81,8 +84,10 @@ class SelectForm(forms.Form): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) - self.fields["group"].queryset = Group.objects.for_current_school_term_or_all().annotate(lessons_count=Count("lessons")).filter( - lessons_count__gt=0 + self.fields["group"].queryset = ( + Group.objects.for_current_school_term_or_all() + .annotate(lessons_count=Count("lessons")) + .filter(lessons_count__gt=0) ) @@ -124,3 +129,11 @@ class ExtraMarkForm(forms.ModelForm): class Meta: model = ExtraMark fields = ["short_name", "name"] + + +class ExcuseTypeForm(forms.ModelForm): + layout = Layout("short_name", "name") + + class Meta: + model = ExcuseType + fields = ["short_name", "name"] diff --git a/aleksis/apps/alsijil/menus.py b/aleksis/apps/alsijil/menus.py index e18a62352ebe850649baec923d4857734b6b89c9..081c1ec2abca720d1f8e4eda519ceae4a931d80b 100644 --- a/aleksis/apps/alsijil/menus.py +++ b/aleksis/apps/alsijil/menus.py @@ -36,6 +36,12 @@ MENUS = { "icon": "filter_list", "validators": ["menu_generator.validators.is_superuser"], }, + { + "name": _("Excuse types"), + "url": "excuse_types", + "icon": "label", + "validators": ["menu_generator.validators.is_superuser"], + }, { "name": _("Extra marks"), "url": "extra_marks", diff --git a/aleksis/apps/alsijil/migrations/0002_excuse_type.py b/aleksis/apps/alsijil/migrations/0002_excuse_type.py new file mode 100644 index 0000000000000000000000000000000000000000..6e4df12f7219969a1059abcd0757be1999a4fb6a --- /dev/null +++ b/aleksis/apps/alsijil/migrations/0002_excuse_type.py @@ -0,0 +1,73 @@ +# Generated by Django 3.0.8 on 2020-07-10 10:46 + +import django.contrib.postgres.fields.jsonb +import django.contrib.sites.managers +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("sites", "0002_alter_domain_unique"), + ("alsijil", "0001_initial"), + ] + + operations = [ + migrations.CreateModel( + name="ExcuseType", + fields=[ + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ( + "extended_data", + django.contrib.postgres.fields.jsonb.JSONField( + default=dict, editable=False + ), + ), + ( + "short_name", + models.CharField( + max_length=255, unique=True, verbose_name="Short name" + ), + ), + ( + "name", + models.CharField(max_length=255, unique=True, verbose_name="Name"), + ), + ( + "site", + models.ForeignKey( + default=1, + editable=False, + on_delete=django.db.models.deletion.CASCADE, + to="sites.Site", + ), + ), + ], + options={ + "verbose_name": "Excuse type", + "verbose_name_plural": "Excuse types", + "ordering": ["name"], + }, + managers=[("objects", django.contrib.sites.managers.CurrentSiteManager()),], + ), + migrations.AddField( + model_name="personalnote", + name="excuse_type", + field=models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.SET_NULL, + to="alsijil.ExcuseType", + verbose_name="Excuse type", + ), + ), + ] diff --git a/aleksis/apps/alsijil/model_extensions.py b/aleksis/apps/alsijil/model_extensions.py index d0129090a5faf99547695eb4568748d996d5274f..ecba9f773f54ba1343fbf86ab2cd2fc7003a760b 100644 --- a/aleksis/apps/alsijil/model_extensions.py +++ b/aleksis/apps/alsijil/model_extensions.py @@ -8,7 +8,7 @@ from calendarweek import CalendarWeek from aleksis.apps.chronos.models import LessonPeriod from aleksis.core.models import Group, Person -from .models import ExtraMark, LessonDocumentation, PersonalNote +from .models import ExcuseType, ExtraMark, LessonDocumentation, PersonalNote @Person.method @@ -18,6 +18,7 @@ def mark_absent( from_period: int = 0, absent: bool = True, excused: bool = False, + excuse_type: Optional[ExcuseType] = None, remarks: str = "", ): """Mark a person absent for all lessons in a day, optionally starting with a selected period number. @@ -44,7 +45,7 @@ def mark_absent( person=self, lesson_period=lesson_period, week=wanted_week.week, - defaults={"absent": absent, "excused": excused}, + defaults={"absent": absent, "excused": excused, "excuse_type": excuse_type}, ) if remarks: diff --git a/aleksis/apps/alsijil/models.py b/aleksis/apps/alsijil/models.py index 1f0b1a7191395dc306cfdfbec06cda52bf26d16f..76b3b8a98c4ed20a97d89acf906d66568afc6aeb 100644 --- a/aleksis/apps/alsijil/models.py +++ b/aleksis/apps/alsijil/models.py @@ -8,6 +8,30 @@ 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")) + + def __str__(self): + return f"{self.name} ({self.short_name})" + + @property + def count_label(self): + return f"{self.short_name}_count" + + class Meta: + ordering = ["name"] + verbose_name = _("Excuse type") + verbose_name_plural = _("Excuse types") + + class PersonalNote(ExtensibleModel): """A personal note about a single person. @@ -27,6 +51,13 @@ class PersonalNote(ExtensibleModel): absent = models.BooleanField(default=False) late = models.IntegerField(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) @@ -34,6 +65,11 @@ class PersonalNote(ExtensibleModel): "ExtraMark", null=True, blank=True, verbose_name=_("Extra marks") ) + def save(self, *args, **kwargs): + if self.excuse_type: + self.excused = True + super().save(*args, **kwargs) + class Meta: verbose_name = _("Personal note") verbose_name_plural = _("Personal notes") diff --git a/aleksis/apps/alsijil/static/css/alsijil/full_register.css b/aleksis/apps/alsijil/static/css/alsijil/full_register.css index 2c7327003c36e7dde492103eb5ad1b4cb5a65801..9a3dc493aa468c989ed766817436d20b40cd0bff 100644 --- a/aleksis/apps/alsijil/static/css/alsijil/full_register.css +++ b/aleksis/apps/alsijil/static/css/alsijil/full_register.css @@ -1,4 +1,4 @@ -table.small-print { +table.small-print, td.small-print, th.small-print { font-size: 10pt; } @@ -25,7 +25,7 @@ tr.lessons-day-first { border-top: 3px solid rgba(0, 0, 0, 0.3); } -th.lessons-day-head { +th.lessons-day-head, td.rotate, th.rotate { text-align: center; transform: rotate(-90deg); } diff --git a/aleksis/apps/alsijil/tables.py b/aleksis/apps/alsijil/tables.py index c3b393b61d8b513157c7984940624394c9e8026f..866da69e08affc731f76d56577aff7e91dee6bfe 100644 --- a/aleksis/apps/alsijil/tables.py +++ b/aleksis/apps/alsijil/tables.py @@ -37,3 +37,23 @@ class ExtraMarkTable(tables.Table): text=_("Delete"), attrs={"a": {"class": "btn-flat waves-effect waves-red red-text"}}, ) + + +class ExcuseTypeTable(tables.Table): + class Meta: + attrs = {"class": "highlight"} + + name = tables.LinkColumn("edit_excuse_type", args=[A("id")]) + short_name = tables.Column() + 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"}}, + ) diff --git a/aleksis/apps/alsijil/templates/alsijil/class_register/lesson.html b/aleksis/apps/alsijil/templates/alsijil/class_register/lesson.html index b819c932233df2927db8f6ddd2f424043b027078..f803493bf3d75c28f8a094206d8fcb13a33ecdab 100644 --- a/aleksis/apps/alsijil/templates/alsijil/class_register/lesson.html +++ b/aleksis/apps/alsijil/templates/alsijil/class_register/lesson.html @@ -156,6 +156,7 @@ <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> @@ -185,6 +186,14 @@ <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 %} diff --git a/aleksis/apps/alsijil/templates/alsijil/class_register/week_view.html b/aleksis/apps/alsijil/templates/alsijil/class_register/week_view.html index 5d26d3d294c4e8f4137151b7add4ef0a43c87a2a..594e876b65f561ff8489fae3226bbeacc24bbd98 100644 --- a/aleksis/apps/alsijil/templates/alsijil/class_register/week_view.html +++ b/aleksis/apps/alsijil/templates/alsijil/class_register/week_view.html @@ -88,20 +88,20 @@ {% blocktrans %}Personal notes{% endblocktrans %} </span> {% for person in persons %} - <h5 class="card-title">{{ person.full_name }}</h5> + <h5 class="card-title">{{ person.person.full_name }}</h5> <p class="card-text"> - {% trans "Absent" %}: {{ person.absences_count }} - ({{ person.unexcused_count }} {% trans "unexcused" %}) + {% trans "Absent" %}: {{ person.person.absences_count }} + ({{ person.person.unexcused_count }} {% trans "unexcused" %}) </p> <p class="card-text"> - {% trans "Summed up tardiness" %}: {{ person.tardiness_sum }}' + {% trans "Summed up tardiness" %}: {{ person.person.tardiness_sum }}' </p> {% for extra_mark in extra_marks %} <p class="card-text"> - {{ extra_mark.name }}: {{ person|get_dict:extra_mark.count_label }} + {{ extra_mark.name }}: {{ person.person|get_dict:extra_mark.count_label }} </p> {% endfor %} - {% for note in person.personal_notes|only_week:week %} + {% for note in person.personal_notes %} {% if note.remarks %} <blockquote> {{ note.remarks }} @@ -123,11 +123,11 @@ <div class="card red darken-1"> <div class="card-content white-text"> <span class="card-title"> - {% blocktrans %}No group selected{% endblocktrans %} + {% blocktrans %}No lessons available{% endblocktrans %} </span> <p> {% blocktrans %} - There are no lessons for the selected group, teacher or time. + There are no lessons for the selected group or teacher in this week. {% endblocktrans %} </p> </div> diff --git a/aleksis/apps/alsijil/templates/alsijil/excuse_type/create.html b/aleksis/apps/alsijil/templates/alsijil/excuse_type/create.html new file mode 100644 index 0000000000000000000000000000000000000000..6fc6faefb2543cecf32b02e7dcb7ea2a40f3d73b --- /dev/null +++ b/aleksis/apps/alsijil/templates/alsijil/excuse_type/create.html @@ -0,0 +1,18 @@ +{# -*- 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 new file mode 100644 index 0000000000000000000000000000000000000000..78396ed66264cc19abdac2085d1cc89ff931bb38 --- /dev/null +++ b/aleksis/apps/alsijil/templates/alsijil/excuse_type/edit.html @@ -0,0 +1,17 @@ +{# -*- 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 new file mode 100644 index 0000000000000000000000000000000000000000..2be1f28c96e70f35e63fe4f5cefb50c232e38e0a --- /dev/null +++ b/aleksis/apps/alsijil/templates/alsijil/excuse_type/list.html @@ -0,0 +1,20 @@ +{# -*- engine:django -*- #} + +{% extends "core/base.html" %} + +{% load i18n %} +{% 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" %} + + <a class="btn green waves-effect waves-light" href="{% url 'create_excuse_type' %}"> + <i class="material-icons left">add</i> + {% trans "Create excuse type" %} + </a> + + {% 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 new file mode 100644 index 0000000000000000000000000000000000000000..d90d2e8205b1c91c18e74e02654fde3daebc4971 --- /dev/null +++ b/aleksis/apps/alsijil/templates/alsijil/excuse_type/warning.html @@ -0,0 +1,10 @@ +{% load i18n %} +<div class="alert warning"> + <p> + <i class="material-icons left">warning</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 %} + </p> +</div> diff --git a/aleksis/apps/alsijil/templates/alsijil/partials/absences.html b/aleksis/apps/alsijil/templates/alsijil/partials/absences.html index 6584142bf36e2828471bbb73367442c80f572aee..6deaa3891c480026b22fbad2cfc75a4f2b260110 100644 --- a/aleksis/apps/alsijil/templates/alsijil/partials/absences.html +++ b/aleksis/apps/alsijil/templates/alsijil/partials/absences.html @@ -1,6 +1,6 @@ {% load i18n %} {% for note in notes %} <span class="{% if note.excused %}green-text{% else %}red-text{% endif %}">{{ note.person }} - {% if note.excused %}{% trans "(e)" %}{% else %}{% trans "(u)" %}{% endif %}{% if not forloop.last %},{% endif %} + {% if note.excused %}{% if note.excuse_type %}({{ note.excuse_type.short_name }}){% else %}{% trans "(e)" %}{% endif %}{% else %}{% trans "(u)" %}{% endif %}{% if not forloop.last %},{% endif %} </span> {% endfor %} diff --git a/aleksis/apps/alsijil/templates/alsijil/print/full_register.html b/aleksis/apps/alsijil/templates/alsijil/print/full_register.html index 18298344c2083f45fb16543ffdbffd208d96faed..6a81d401f9cfacc7bbbb4da1305b687afc16658a 100644 --- a/aleksis/apps/alsijil/templates/alsijil/print/full_register.html +++ b/aleksis/apps/alsijil/templates/alsijil/print/full_register.html @@ -14,6 +14,8 @@ <div class="center-align"> <h1>{% trans 'Class register' %}</h1> + <h5>{{ school_term }}</h5> + <p>({{ school_term.date_start }}–{{ school_term.date_end }})</p> {% static "img/aleksis-banner.svg" as aleksis_banner %} <img src="{% firstof request.site.preferences.theme__logo.url aleksis_banner %}" alt="{{ request.site.preferences.general__title }} – Logo" class="max-size-600 center"> @@ -64,6 +66,40 @@ <div class="page-break"> </div> + <h4>{% trans "Abbreviations" %}</h4> + + <h5>{% trans "General" %}</h5> + + <ul class="collection"> + <li class="collection-item"> + <strong>(a)</strong> {% trans "Absent" %} + </li> + <li class="collection-item"> + <strong>(b)</strong> {% trans "Late" %} + </li> + <li class="collection-item"> + <strong>(u)</strong> {% trans "Unexcused" %} + </li> + <li class="collection-item"> + <strong>(e)</strong> {% trans "Excused" %} + </li> + </ul> + + {% if excuse_types %} + <h5>{% trans "Custom excuse types" %}</h5> + + <ul class="collection"> + {% for excuse_type in excuse_types %} + <li class="collection-item"> + <strong>({{ excuse_type.short_name }})</strong> {{ excuse_type.name }} + </li> + {% endfor %} + </ul> + {% endif %} + + <div class="page-break"> </div> + + <h4>{% trans 'Persons in group' %} {{ group.name }}</h4> <table id="persons"> @@ -74,9 +110,13 @@ <th>{% trans 'First name' %}</th> <th>{% trans 'Sex' %}</th> <th>{% trans 'Date of birth' %}</th> - <th>{% trans 'Absences' %}</th> - <th>{% trans 'Unexcused' %}</th> - <th>{% trans 'Tard.' %}</th> + <th>{% trans '(a)' %}</th> + <th>{% trans "(e)" %}</th> + {% for excuse_type in excuse_types %} + <th>({{ excuse_type.short_name }})</th> + {% endfor %} + <th>{% trans '(u)' %}</th> + <th>{% trans '(b)' %}</th> {% for extra_mark in extra_marks %} <th>{{ extra_mark.short_name }}</th> {% endfor %} @@ -92,6 +132,10 @@ <td>{{ person.get_sex_display }}</td> <td>{{ person.date_of_birth }}</td> <td>{{ person.absences_count }}</td> + <td>{{ person.excused }}</td> + {% for excuse_type in excuse_types %} + <td>{{ person|get_dict:excuse_type.count_label }}</td> + {% endfor %} <td>{{ person.unexcused }}</td> <td>{{ person.tardiness }}'</td> {% for extra_mark in extra_marks %} @@ -123,8 +167,8 @@ <tr> <td>{{ lesson.subject.name }}</td> <td>{{ lesson.teachers.all|join:', ' }}</td> - <td>{{ lesson.date_start }}</td> - <td>{{ lesson.date_end }}</td> + <td>{{ lesson.validity.date_start }}</td> + <td>{{ lesson.validity.date_end }}</td> <td>{{ lesson.lesson_periods.count }}</td> </tr> {% endfor %} @@ -156,8 +200,8 @@ <td>{{ child_group.name }}</td> <td>{{ lesson.subject.name }}</td> <td>{{ lesson.teachers.all|join:', ' }}</td> - <td>{{ lesson.date_start }}</td> - <td>{{ lesson.date_end }}</td> + <td>{{ lesson.validity.date_start }}</td> + <td>{{ lesson.validity.date_end }}</td> <td>{{ lesson.lesson_periods.count }}</td> </tr> {% endfor %} @@ -232,21 +276,28 @@ <h5>{% trans 'Absences and tardiness' %}</h5> <table> - <thead> <tr> - <th>{% trans 'Absences' %}</th> - <th>{% trans 'Unexcused' %}</th> - <th>{% trans 'Tardiness' %}</th> + <th colspan="2">{% trans 'Absences' %}</th> + <td>{{ person.absences_count }}</td> </tr> - </thead> - - <tbody> <tr> - <td>{{ person.absences_count }}</td> + <td rowspan="{{ excuse_types.count|add:2 }}" style="width: 16mm;" + class="rotate small-print">{% trans "thereof" %}</td> + <th>{% trans 'Excused' %}</th> + <td>{{ person.excused }}</td> + </tr> + {% for excuse_type in excuse_types %} + <th>{{ excuse_type.name }}</th> + <td>{{ person|get_dict:excuse_type.count_label }}</td> + {% endfor %} + <tr> + <th>{% trans 'Unexcused' %}</th> <td>{{ person.unexcused }}</td> + </tr> + <tr> + <th colspan="2">{% trans 'Tardiness' %}</th> <td>{{ person.tardiness }}'</td> </tr> - </tbody> </table> {% if extra_marks %} @@ -287,8 +338,12 @@ <td> {% if note.absent %} {% trans 'Yes' %} - {% if note.escused %} - ({% trans 'e' %}) + {% if note.excused %} + {% if note.excuse_type %} + ({{ note.excuse_type.short_name }}) + {% else %} + ({% trans 'e' %}) + {% endif %} {% endif %} {% endif %} </td> @@ -370,8 +425,12 @@ {{ note.person.last_name }}, {{ note.person.first_name|slice:"0:1" }}. {% if note.excused %} <span class="lesson-note-excused"> - ({% trans 'e' %}) - </span> + {% if note.excuse_type %} + ({{ note.excuse_type.short_name }}) + {% else %} + ({% trans 'e' %}) + {% endif %} + </span> {% endif %} </span> {% endif %} @@ -381,8 +440,12 @@ ({{ note.late }}′) {% if note.excused %} <span class="lesson-note-excused"> - ({% trans 'e' %}) - </span> + {% if note.excuse_type %} + ({{ note.excuse_type.short_name }}) + {% else %} + ({% trans 'e' %}) + {% endif %} + </span> {% endif %} </span> {% endif %} diff --git a/aleksis/apps/alsijil/urls.py b/aleksis/apps/alsijil/urls.py index 7f022fbb127ecfe523da6840041b7b75f043f849..12c12bc444f9e91a5c05611133ba567eea2f5b1d 100644 --- a/aleksis/apps/alsijil/urls.py +++ b/aleksis/apps/alsijil/urls.py @@ -57,4 +57,20 @@ urlpatterns = [ views.ExtraMarkDeleteView.as_view(), name="delete_extra_mark", ), + path("excuse_types/", views.ExcuseTypeListView.as_view(), name="excuse_types"), + path( + "excuse_types/create/", + views.ExcuseTypeCreateView.as_view(), + name="create_excuse_type", + ), + path( + "excuse_types/<int:pk>/edit/", + views.ExcuseTypeEditView.as_view(), + name="edit_excuse_type", + ), + path( + "excuse_types/<int:pk>/delete/", + views.ExcuseTypeDeleteView.as_view(), + name="delete_excuse_type", + ), ] diff --git a/aleksis/apps/alsijil/views.py b/aleksis/apps/alsijil/views.py index 4231ea11e892f86d9cc1e5ea8f443dd2815f10c8..1e109d7e55e0f22300b7a73f60bc65a828d23705 100644 --- a/aleksis/apps/alsijil/views.py +++ b/aleksis/apps/alsijil/views.py @@ -21,6 +21,7 @@ from aleksis.core.models import Group, Person, SchoolTerm from aleksis.core.util import messages from .forms import ( + ExcuseTypeForm, ExtraMarkForm, LessonDocumentationForm, PersonalNoteFilterForm, @@ -28,8 +29,8 @@ from .forms import ( RegisterAbsenceForm, SelectForm, ) -from .models import ExtraMark, LessonDocumentation, PersonalNoteFilter -from .tables import ExtraMarkTable, PersonalNoteFilterTable +from .models import ExcuseType, ExtraMark, LessonDocumentation, PersonalNoteFilter +from .tables import ExcuseTypeTable, ExtraMarkTable, PersonalNoteFilterTable def lesson( @@ -103,6 +104,8 @@ def lesson( if lesson_documentation_form.is_valid(): lesson_documentation_form.save() + messages.success(request, _("The lesson documentation has been saved.")) + if personal_note_formset.is_valid(): instances = personal_note_formset.save() @@ -113,8 +116,16 @@ def lesson( lesson_period.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" + ) + context["lesson_documentation"] = lesson_documentation context["lesson_documentation_form"] = lesson_documentation_form context["personal_note_formset"] = personal_note_formset @@ -189,16 +200,25 @@ def week_view( if lesson_periods: # Aggregate all personal notes for this group and week - persons = ( - Person.objects.filter(is_active=True) - .filter(member_of__lessons__lesson_periods__in=lesson_periods) - .distinct() + lesson_periods_pk = list(lesson_periods.values_list("pk", flat=True)) + + persons_qs = Person.objects.filter(is_active=True) + + if group: + persons_qs = persons_qs.filter(member_of=group) + else: + persons_qs = persons_qs.filter( + member_of__lessons__lesson_periods__in=lesson_periods_pk + ) + + persons_qs = ( + persons_qs.distinct() .prefetch_related("personal_notes") .annotate( absences_count=Count( "personal_notes", filter=Q( - personal_notes__lesson_period__in=lesson_periods, + personal_notes__lesson_period__in=lesson_periods_pk, personal_notes__week=wanted_week.week, personal_notes__absent=True, ), @@ -207,7 +227,7 @@ def week_view( unexcused_count=Count( "personal_notes", filter=Q( - personal_notes__lesson_period__in=lesson_periods, + personal_notes__lesson_period__in=lesson_periods_pk, personal_notes__week=wanted_week.week, personal_notes__absent=True, personal_notes__excused=False, @@ -217,7 +237,7 @@ def week_view( tardiness_sum=Subquery( Person.objects.filter( pk=OuterRef("pk"), - personal_notes__lesson_period__in=lesson_periods, + personal_notes__lesson_period__in=lesson_periods_pk, personal_notes__week=wanted_week.week, ) .distinct() @@ -228,12 +248,12 @@ def week_view( ) for extra_mark in ExtraMark.objects.all(): - persons = persons.annotate( + persons_qs = persons_qs.annotate( **{ extra_mark.count_label: Count( "personal_notes", filter=Q( - personal_notes__lesson_period__in=lesson_periods, + personal_notes__lesson_period__in=lesson_periods_pk, personal_notes__week=wanted_week.week, personal_notes__extra_marks=extra_mark, ), @@ -242,6 +262,16 @@ def week_view( } ) + persons = [] + for person in persons_qs: + persons.append( + { + "person": person, + "personal_notes": person.personal_notes.filter( + week=wanted_week.week, lesson_period__in=lesson_periods_pk + ), + } + ) else: persons = None @@ -325,6 +355,14 @@ def full_register_group(request: HttpRequest, id_: int) -> HttpResponse: absences_count=Count( "personal_notes__absent", filter=Q(personal_notes__absent=True) ), + excused=Count( + "personal_notes__absent", + filter=Q( + personal_notes__absent=True, + personal_notes__excused=True, + personal_notes__excuse_type__isnull=True, + ), + ), unexcused=Count( "personal_notes__absent", filter=Q(personal_notes__absent=True, personal_notes__excused=False), @@ -341,6 +379,19 @@ def full_register_group(request: HttpRequest, id_: int) -> HttpResponse: } ) + for excuse_type in ExcuseType.objects.all(): + persons = persons.annotate( + **{ + excuse_type.count_label: Count( + "personal_notes__absent", + filter=Q( + personal_notes__absent=True, + personal_notes__excuse_type=excuse_type, + ), + ) + } + ) + # FIXME Move to manager personal_note_filters = PersonalNoteFilter.objects.all() for personal_note_filter in personal_note_filters: @@ -356,9 +407,11 @@ def full_register_group(request: HttpRequest, id_: int) -> HttpResponse: } ) - context["extra_marks"] = ExtraMark.objects.all() + context["school_term"] = current_school_term context["persons"] = persons context["personal_note_filters"] = personal_note_filters + context["excuse_types"] = ExcuseType.objects.all() + context["extra_marks"] = ExtraMark.objects.all() context["group"] = group context["weeks"] = weeks context["periods_by_day"] = periods_by_day @@ -490,3 +543,44 @@ class ExtraMarkDeleteView(AdvancedDeleteView, PermissionRequiredMixin, RevisionM template_name = "core/pages/delete.html" success_url = reverse_lazy("extra_marks") success_message = _("The extra mark has been deleted.") + + +class ExcuseTypeListView(SingleTableView, PermissionRequiredMixin): + """Table of all excuse types.""" + + model = ExcuseType + table_class = ExcuseTypeTable + permission_required = "core.view_excusetype" + template_name = "alsijil/excuse_type/list.html" + + +class ExcuseTypeCreateView(AdvancedCreateView, PermissionRequiredMixin): + """Create view for excuse types.""" + + model = ExcuseType + form_class = ExcuseTypeForm + permission_required = "core.create_excusetype" + template_name = "alsijil/excuse_type/create.html" + success_url = reverse_lazy("excuse_types") + success_message = _("The excuse type has been created.") + + +class ExcuseTypeEditView(AdvancedEditView, PermissionRequiredMixin): + """Edit view for excuse types.""" + + model = ExcuseType + form_class = ExcuseTypeForm + permission_required = "core.edit_excusetype" + template_name = "alsijil/excuse_type/edit.html" + success_url = reverse_lazy("excuse_types") + success_message = _("The excuse type has been saved.") + + +class ExcuseTypeDeleteView(AdvancedDeleteView, PermissionRequiredMixin, RevisionMixin): + """Delete view for excuse types""" + + model = ExcuseType + permission_required = "core.delete_excusetype" + template_name = "core/pages/delete.html" + success_url = reverse_lazy("excuse_types") + success_message = _("The excuse type has been deleted.")