diff --git a/aleksis/apps/alsijil/menus.py b/aleksis/apps/alsijil/menus.py index a3ec5e9cc443a951ca5ddf4803361bd29e0637f9..d20c88b567ad7d4feb86231eafa4e35d880f9e1c 100644 --- a/aleksis/apps/alsijil/menus.py +++ b/aleksis/apps/alsijil/menus.py @@ -24,6 +24,18 @@ MENUS = { "icon": "view_week", "validators": ["menu_generator.validators.is_authenticated"], }, + { + "name": _("My overview"), + "url": "overview_me", + "icon": "insert_chart", + "validators": ["menu_generator.validators.is_authenticated"], + }, + { + "name": _("My students"), + "url": "my_students", + "icon": "people", + "validators": ["menu_generator.validators.is_authenticated"], + }, { "name": _("Register absence"), "url": "register_absence", diff --git a/aleksis/apps/alsijil/models.py b/aleksis/apps/alsijil/models.py index 83107c781f4169095b8f5774a0c078564b29fbc1..721648bc4f52ddb4728de84f21319635ee9a014f 100644 --- a/aleksis/apps/alsijil/models.py +++ b/aleksis/apps/alsijil/models.py @@ -77,6 +77,9 @@ class PersonalNote(ExtensibleModel, WeekRelatedMixin): def save(self, *args, **kwargs): if self.excuse_type: self.excused = True + if not self.absent: + self.excused = False + self.excuse_type = None super().save(*args, **kwargs) class Meta: diff --git a/aleksis/apps/alsijil/templates/alsijil/class_register/person.html b/aleksis/apps/alsijil/templates/alsijil/class_register/person.html new file mode 100644 index 0000000000000000000000000000000000000000..5a442b81c4fbb2956034bb26dc1f7c98ccbab4f8 --- /dev/null +++ b/aleksis/apps/alsijil/templates/alsijil/class_register/person.html @@ -0,0 +1,202 @@ +{# -*- engine:django -*- #} +{% extends "core/base.html" %} +{% load data_helpers %} +{% load week_helpers %} +{% load i18n %} + +{% block browser_title %}{% blocktrans %}Class register: person{% endblocktrans %}{% endblock %} + + +{% block page_title %} + {% blocktrans with person=person %} + Class register overview for {{ person }} + {% endblocktrans %} +{% endblock %} + +{% block content %} + <div class="row"> + <div class="col s12 m12 l6"> + <h5>{% trans "Unexcused absences" %}</h5> + + <ul class="collection"> + {% for note in unexcused_absences %} + {% weekday_to_date note.calendar_week note.lesson_period.period.weekday as note_date %} + <li class="collection-item"> + <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" %} + </form> + <i class="material-icons left red-text">warning</i> + <p class="no-margin"> + <a href="{% url "lesson_by_week_and_period" note.year note.week note.lesson_period.pk %}">{{ note_date }}, {{ note.lesson_period }}</a> + </p> + {% if note.remarks %} + <p class="no-margin"><em>{{ note.remarks }}</em></p> + {% endif %} + <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" %} + </form> + </li> + {% empty %} + <li class="collection-item flow-text"> + {% trans "There are unexcused lessons." %} + </li> + {% endfor %} + </ul> + <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 }}'</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> + </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.lesson_period.lesson.validity.school_term %}</ul></div></li> + <li {% if forloop.first %}class="active"{% endif %}> + <div class="collapsible-header"><i + class="material-icons">date_range</i>{{ note.lesson_period.lesson.validity.school_term }}</div> + <div class="collapsible-body"> + <ul class="collection"> + {% endifchanged %} + + {% ifchanged note.week %} + <li class="collection-item"> + <strong>{% blocktrans with week=note.week %}Week {{ week }}{% endblocktrans %}</strong> + </li> + {% endifchanged %} + {% weekday_to_date note.calendar_week note.lesson_period.period.weekday as note_date %} + {% ifchanged note_date %} + <li class="collection-item"> + <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> + <i class="material-icons left">schedule</i> + {{ 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> + </li> + {% endifchanged %} + + <li class="collection-item"> + <div class="row no-margin"> + <div class="col s2 m1"> + {{ note.lesson_period.period.period }}. + </div> + + <div class="col s10 m4"> + <i class="material-icons left">event_note</i> + <a href="{% url "lesson_by_week_and_period" note.year note.week note.lesson_period.pk %}"> + {{ note.lesson_period.get_subject.name }}<br/> + {{ note.lesson_period.get_teacher_names }} + </a> + </div> + + <div class="col s12 m7 no-padding"> + {% if note.absent and not note.excused %} + <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" %} + </form> + {% endif %} + + {% 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 %} + + {% 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 %} + <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" %} + </form> + {% endif %} + </div> + </li> + {% endfor %} + </li> + </ul> + </div> + </div> +{% endblock %} diff --git a/aleksis/apps/alsijil/templates/alsijil/class_register/persons.html b/aleksis/apps/alsijil/templates/alsijil/class_register/persons.html new file mode 100644 index 0000000000000000000000000000000000000000..c3f5e35d48991bafd37ffdc3def7f445d1400247 --- /dev/null +++ b/aleksis/apps/alsijil/templates/alsijil/class_register/persons.html @@ -0,0 +1,26 @@ +{# -*- engine:django -*- #} +{% extends "core/base.html" %} +{% load data_helpers %} +{% load week_helpers %} +{% load i18n %} + +{% block browser_title %}{% blocktrans %}My students{% endblocktrans %}{% endblock %} + + +{% block page_title %} + {% blocktrans %}My students{% endblocktrans %} +{% endblock %} + +{% block content %} + <div class="collection"> + {% for person in persons %} + <a class="collection-item" href="{% url "overview_person" person.pk %}"> + {{ person }} + </a> + {% empty %} + <li class="collection-item flow-text"> + {% blocktrans %}No students available.{% endblocktrans %} + </li> + {% endfor %} + </div> +{% endblock %} diff --git a/aleksis/apps/alsijil/templates/alsijil/class_register/week_view.html b/aleksis/apps/alsijil/templates/alsijil/class_register/week_view.html index d0761f8102f60e1ea899a8b1ba1d11c1e2402535..ea549f7f15783769f79a9d598304b856481adb18 100644 --- a/aleksis/apps/alsijil/templates/alsijil/class_register/week_view.html +++ b/aleksis/apps/alsijil/templates/alsijil/class_register/week_view.html @@ -35,7 +35,8 @@ <div class="row"> - <h4 class="col s12 m6">{% blocktrans with el=el week=week.week %}CW {{ week }}: {{ instance }}{% endblocktrans %} </h4> + <h4 class="col s12 m6">{% blocktrans with el=el week=week.week %}CW {{ week }}: + {{ instance }}{% endblocktrans %} </h4> {% include "chronos/partials/week_select.html" with wanted_week=week %} </div> @@ -111,7 +112,9 @@ {% blocktrans %}Personal notes{% endblocktrans %} </span> {% for person in persons %} - <h5 class="card-title">{{ person.person.full_name }}</h5> + <h5 class="card-title"> + <a href="{% url "overview_person" person.person.pk %}">{{ person.person.full_name }}</a> + </h5> <p class="card-text"> {% trans "Absent" %}: {{ person.person.absences_count }} ({{ person.person.unexcused_count }} {% trans "unexcused" %}) diff --git a/aleksis/apps/alsijil/templates/alsijil/partials/mark_as_buttons.html b/aleksis/apps/alsijil/templates/alsijil/partials/mark_as_buttons.html new file mode 100644 index 0000000000000000000000000000000000000000..5b198afa4bceea55c2b749fa8b3f3b8d88b335e8 --- /dev/null +++ b/aleksis/apps/alsijil/templates/alsijil/partials/mark_as_buttons.html @@ -0,0 +1,12 @@ +{% load i18n %} +<button type="submit" class="btn-flat tooltipped" name="excuse_type" value="e" title="{% trans "Excused" %}" + data-position="bottom" data-tooltip="{% trans "Excused" %}" style="width: 50px;"> + {% trans "e" %} +</button> +{% for excuse_type in excuse_types %} + <button type="submit" class="btn-flat tooltipped" value="{{ excuse_type.pk }}" name="excuse_type" + title="{{ excuse_type.name }}" data-position="bottom" data-tooltip="{{ excuse_type.name }}" + style="width: 50px;"> + {{ excuse_type.short_name }} + </button> +{% endfor %} diff --git a/aleksis/apps/alsijil/templates/alsijil/print/full_register.html b/aleksis/apps/alsijil/templates/alsijil/print/full_register.html index 373ae6a88f6f42953dfa0b4ffeceb8a26716104b..03b75de7b70508885bf06f35e42d1058a5c80b1a 100644 --- a/aleksis/apps/alsijil/templates/alsijil/print/full_register.html +++ b/aleksis/apps/alsijil/templates/alsijil/print/full_register.html @@ -319,7 +319,7 @@ {% for note in person.personal_notes.all %} {% if note.lesson_period in lesson_periods %} {% if note.absent or note.late or note.remarks or note.extra_marks.all %} - {% period_to_date note.week note.lesson_period.period as note_date %} + {% weekday_to_date note.calendar_week note.lesson_period.period.weekday as note_date %} <tr> <td>{{ note_date }}</td> <td>{{ note.lesson_period.period.period }}</td> diff --git a/aleksis/apps/alsijil/urls.py b/aleksis/apps/alsijil/urls.py index 7e7139c6c26fe7aa600b6b311b55ae731389c13b..0482571bd3cd9cf7c7da4a15b4cd7fc3e0a58c36 100644 --- a/aleksis/apps/alsijil/urls.py +++ b/aleksis/apps/alsijil/urls.py @@ -26,6 +26,9 @@ urlpatterns = [ path( "print/group/<int:id_>", views.full_register_group, name="full_register_group" ), + path("persons/", views.my_students, name="my_students"), + path("persons/<int:id_>/", views.overview_person, name="overview_person"), + path("me/", views.overview_person, name="overview_me"), path("absence/new", views.register_absence, name="register_absence"), path("extra_marks/", views.ExtraMarkListView.as_view(), name="extra_marks"), path( diff --git a/aleksis/apps/alsijil/views.py b/aleksis/apps/alsijil/views.py index efa96737739abae615138d73f201e7ba36cc4591..f9840a124ee64626238b77031039837b96525554 100644 --- a/aleksis/apps/alsijil/views.py +++ b/aleksis/apps/alsijil/views.py @@ -20,7 +20,7 @@ from aleksis.apps.chronos.util.date import get_weeks_for_year, week_weekday_to_d from aleksis.core.mixins import AdvancedCreateView, AdvancedDeleteView, AdvancedEditView from aleksis.core.models import Group, Person, SchoolTerm from aleksis.core.util import messages -from aleksis.core.util.core_helpers import get_site_preferences +from aleksis.core.util.core_helpers import get_site_preferences, objectgetter_optional from .forms import ( ExcuseTypeForm, @@ -30,7 +30,7 @@ from .forms import ( RegisterAbsenceForm, SelectForm, ) -from .models import ExcuseType, ExtraMark, LessonDocumentation +from .models import ExcuseType, ExtraMark, LessonDocumentation, PersonalNote from .tables import ExcuseTypeTable, ExtraMarkTable @@ -458,6 +458,141 @@ def full_register_group(request: HttpRequest, id_: int) -> HttpResponse: return render(request, "alsijil/print/full_register.html", context) +def my_students(request: HttpRequest) -> HttpResponse: + context = {} + relevant_groups = ( + Group.objects.for_current_school_term_or_all() + .annotate(lessons_count=Count("lessons")) + .filter(lessons_count__gt=0, owners=request.user.person) + ) + persons = Person.objects.filter(member_of__in=relevant_groups) + context["persons"] = persons + return render(request, "alsijil/class_register/persons.html", context) + + +def overview_person(request: HttpRequest, id_: Optional[int] = None) -> HttpResponse: + context = {} + person = objectgetter_optional( + Person, default="request.user.person", default_eval=True + )(request, id_) + context["person"] = person + + if request.method == "POST": + if request.POST.get("excuse_type"): + # Get excuse type + excuse_type = request.POST["excuse_type"] + found = False + if excuse_type == "e": + excuse_type = None + found = True + else: + try: + excuse_type = ExcuseType.objects.get(pk=int(excuse_type)) + found = True + except (ExcuseType.DoesNotExist, ValueError): + pass + + if found: + if request.POST.get("date"): + # Mark absences on date as excused + try: + date = datetime.strptime( + request.POST["date"], "%Y-%m-%d" + ).date() + + notes = person.personal_notes.filter( + week=date.isocalendar()[1], + lesson_period__period__weekday=date.weekday(), + lesson_period__lesson__validity__date_start__lte=date, + lesson_period__lesson__validity__date_end__gte=date, + absent=True, + excused=False, + ) + notes.update(excused=True, excuse_type=excuse_type) + messages.success( + request, _("The absences have been marked as excused.") + ) + except ValueError: + pass + elif request.POST.get("personal_note"): + # Mark specific absence as excused + try: + note = PersonalNote.objects.get( + pk=int(request.POST["personal_note"]) + ) + if note.absent: + note.excused = True + note.excuse_type = excuse_type + note.save() + messages.success( + request, _("The absence has been marked as excused.") + ) + except (PersonalNote.DoesNotExist, ValueError): + pass + + person.refresh_from_db() + + unexcused_absences = person.personal_notes.filter(absent=True, excused=False) + context["unexcused_absences"] = unexcused_absences + + personal_notes = person.personal_notes.filter( + Q(absent=True) | Q(late__gt=0) | ~Q(remarks="") | Q(extra_marks__isnull=False) + ).order_by( + "-lesson_period__lesson__validity__date_start", + "-week", + "lesson_period__period__weekday", + "lesson_period__period__period", + ) + context["personal_notes"] = personal_notes + context["excuse_types"] = ExcuseType.objects.all() + + school_terms = SchoolTerm.objects.all().order_by("-date_start") + stats = [] + for school_term in school_terms: + stat = {} + personal_notes = PersonalNote.objects.filter( + person=person, lesson_period__lesson__validity__school_term=school_term + ) + + if not personal_notes.exists(): + continue + + stat.update( + personal_notes.filter(absent=True).aggregate(absences_count=Count("absent")) + ) + stat.update( + personal_notes.filter( + absent=True, excused=True, excuse_type__isnull=True + ).aggregate(excused=Count("absent")) + ) + stat.update( + personal_notes.filter(absent=True, excused=False).aggregate( + unexcused=Count("absent") + ) + ) + stat.update(personal_notes.aggregate(tardiness=Sum("late"))) + + for extra_mark in ExtraMark.objects.all(): + stat.update( + personal_notes.filter(extra_marks=extra_mark).aggregate( + **{extra_mark.count_label: Count("pk")} + ) + ) + + for excuse_type in ExcuseType.objects.all(): + stat.update( + personal_notes.filter(absent=True, excuse_type=excuse_type).aggregate( + **{excuse_type.count_label: Count("absent")} + ) + ) + + stats.append((school_term, stat)) + context["stats"] = stats + context["excuse_types"] = ExcuseType.objects.all() + context["extra_marks"] = ExtraMark.objects.all() + return render(request, "alsijil/class_register/person.html", context) + + def register_absence(request: HttpRequest) -> HttpResponse: context = {}