From ef1f1904b827f7b06c8b1321966695d5e2140860 Mon Sep 17 00:00:00 2001 From: Jonathan Weth <git@jonathanweth.de> Date: Wed, 13 Jan 2021 17:38:43 +0100 Subject: [PATCH] Include support for events and extra lessons in week view --- .../alsijil/class_register/week_view.html | 101 +++++----- aleksis/apps/alsijil/util/alsijil_helpers.py | 47 ++++- aleksis/apps/alsijil/views.py | 174 +++++++++++------- 3 files changed, 213 insertions(+), 109 deletions(-) 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 0f55e11ea..c60257e88 100644 --- a/aleksis/apps/alsijil/templates/alsijil/class_register/week_view.html +++ b/aleksis/apps/alsijil/templates/alsijil/class_register/week_view.html @@ -73,17 +73,14 @@ </ul> </div> <div class="col s12" id="week-overview"> - {% regroup lesson_periods by period.get_weekday_display as periods_by_day %} - {% for weekday, periods in periods_by_day %} + {% for weekday, objects in regrouped_objects.items %} {% with weekdays|get_dict:forloop.counter0 as advanced_weekday %} - {% weekday_to_date week periods.0.period.weekday as current_date %} - {% if advanced_weekday.holiday and not request.site.preferences.alsijil__allow_entries_in_holidays %} <div class="card"> <div class="card-content"> - {% weekday_to_date week periods.0.period.weekday as current_date %} <span class="card-title"> - {{ weekday }}, {{ current_date }} <span class="badge new blue no-float">{{ advanced_weekday.holiday }}</span> + {{ advanced_weekday.name }}, {{ advanced_weekday.date }} <span + class="badge new blue no-float">{{ advanced_weekday.holiday }}</span> </span> </div> </div> @@ -91,7 +88,7 @@ <div class="card show-on-extra-large"> <div class="card-content"> <span class="card-title"> - {{ weekday }}, {{ current_date }} + {{ advanced_weekday.name }}, {{ advanced_weekday.date }} </span> <table class="striped datatable"> <thead> @@ -109,55 +106,76 @@ </tr> </thead> <tbody> - {% for period in periods %} - {% has_perm "alsijil.view_lessondocumentation" user period as can_view_lesson_documentation %} + {% for register_object in objects %} + {% has_perm "alsijil.view_lessondocumentation" user register_object as can_view_lesson_documentation %} {% if can_view_lesson_documentation %} <tr> <td class="center-align"> - {% include "alsijil/partials/lesson_status_icon.html" with period=period %} + {% include "alsijil/partials/lesson_status_icon.html" with register_object=register_object %} </td> <td class="tr-link"> <a class="tr-link" - href="{% url 'lesson_by_week_and_period' week.year week.week period.id %}"> - {{ period.period.period }}. + href="{% url 'lesson_by_week_and_period' week.year week.week register_object.id %}"> + {% if register_object.period %} + {{ register_object.period.period }}. + {% else %} + {{ register_object.date_start|date:"SHORT_DATE_FORMAT" }} + {{ register_object.period_from.period }}.–<br/> + {{ register_object.date_end|date:"SHORT_DATE_FORMAT" }} + {{ register_object.period_to.period }}. + {% endif %} </a> </td> {% if not group %} <td> <a class="tr-link" - href="{% url 'lesson_by_week_and_period' week.year week.week period.id %}"> - {{ period.lesson.group_names }} + href="{% url 'lesson_by_week_and_period' week.year week.week register_object.id %}"> + {% if register_object.lesson %} + {{ register_object.lesson.group_names }} + {% else %} + {{ register_object.group_names }} + {% endif %} </a> </td> {% endif %} <td> <a class="tr-link" - href="{% url 'lesson_by_week_and_period' week.year week.week period.id %}"> - {{ period.get_subject.name }} + href="{% url 'lesson_by_week_and_period' week.year week.week register_object.id %}"> + {% if register_object.get_subject %} + {{ register_object.get_subject.name }} + {% elif register_object.subject %} + {{ register_object.subject }} + {% else %} + {% trans "Event" %} + {% endif %} </a> </td> <td> <a class="tr-link" - href="{% url 'lesson_by_week_and_period' week.year week.week period.id %}"> - {{ period.get_teacher_names }} + href="{% url 'lesson_by_week_and_period' week.year week.week register_object.id %}"> + {% if register_object.get_teacher_names %} + {{ register_object.get_teacher_names }} + {% else %} + {{ register_object.teacher_names }} + {% endif %} </a> </td> <td> <a class="tr-link" - href="{% url 'lesson_by_week_and_period' week.year week.week period.id %}"> - {% firstof period.get_lesson_documentation.topic "–" %} + href="{% url 'lesson_by_week_and_period' week.year week.week register_object.id %}"> + {% firstof register_object.get_lesson_documentation.topic "–" %} </a> </td> <td> <a class="tr-link" - href="{% url 'lesson_by_week_and_period' week.year week.week period.id %}"> - {% firstof period.get_lesson_documentation.homework "–" %} + href="{% url 'lesson_by_week_and_period' week.year week.week register_object.id %}"> + {% firstof register_object.get_lesson_documentation.homework "–" %} </a> </td> <td> <a class="tr-link" - href="{% url 'lesson_by_week_and_period' week.year week.week period.id %}"> - {% firstof period.get_lesson_documentation.group_note "–" %} + href="{% url 'lesson_by_week_and_period' week.year week.week register_object.id %}"> + {% firstof register_object.get_lesson_documentation.group_note "–" %} </a> </td> </tr> @@ -169,48 +187,47 @@ </div> <ul class="collapsible hide-on-extra-large-only"> <li class=""> - {% weekday_to_date week periods.0.period.weekday as current_date %} <div class="collapsible-header flow-text"> - {{ weekday }}, {{ current_date }} <i class="material-icons collapsible-icon-right">expand_more</i> + {{ weekday }}, {{ advanced_weekday.date }} <i class="material-icons collapsible-icon-right">expand_more</i> </div> <div class="collapsible-body"> <div class="collection"> - {% for period in periods %} - {% has_perm "alsijil.view_lessondocumentation" user period as can_view_lesson_documentation %} + {% for register_object in objects %} + {% has_perm "alsijil.view_lessondocumentation" user register_object as can_view_lesson_documentation %} {% if can_view_lesson_documentation %} <a class="collection-item avatar" - href="{% url 'lesson_by_week_and_period' week.year week.week period.id %}"> - {% include "alsijil/partials/lesson_status_icon.html" with period=period css_class="materialize-circle" color_suffix=" " %} + href="{% url 'lesson_by_week_and_period' week.year week.week register_object.id %}"> + {% include "alsijil/partials/lesson_status_icon.html" with register_object=register_object css_class="materialize-circle" color_suffix=" " %} <table class="hide-on-med-and-down"> <tr> <th>{% trans "Subject" %}</th> - <td>{{ period.period.period }}. {{ period.get_subject.name }}</td> + <td>{{ register_object.period.period }}. {{ register_object.get_subject.name }}</td> </tr> {% if not group %} <tr> <th>{% trans "Group" %}</th> - <td>{{ period.lesson.group_names }}</td> + <td>{{ register_object.lesson.group_names }}</td> </tr> {% endif %} <tr> <th>{% trans "Teachers" %}</th> - <td>{{ period.lesson.teacher_names }}</td> + <td>{{ register_object.lesson.teacher_names }}</td> </tr> <tr> <th>{% trans "Lesson topic" %}</th> - <td>{% firstof period.get_lesson_documentation.topic "–" %}</td> + <td>{% firstof register_object.get_lesson_documentation.topic "–" %}</td> </tr> {% with period.get_lesson_documentation as lesson_documentation %} {% if lesson_documentation.homework %} <tr> <th>{% trans "Homework" %}</th> - <td>{% firstof period.get_lesson_documentation.homework "–" %}</td> + <td>{% firstof register_object.get_lesson_documentation.homework "–" %}</td> </tr> {% endif %} {% if lesson_documentation.group_note %} <tr> <th>{% trans "Group note" %}</th> - <td>{% firstof period.get_lesson_documentation.group_note "–" %}</td> + <td>{% firstof register_object.get_lesson_documentation.group_note "–" %}</td> </tr> {% endif %} {% endwith %} @@ -218,32 +235,32 @@ <div class="hide-on-large-only"> <ul class="collection"> <li class="collection-item"> - {{ period.period.period }}. {{ period.get_subject.name }} + {{ register_object.period.period }}. {{ register_object.get_subject.name }} </li> {% if not group %} <li class="collection-item"> - {{ period.lesson.group_names }} + {{ register_object.lesson.group_names }} </li> {% endif %} <li class="collection-item"> - {{ period.lesson.teacher_names }} + {{ register_object.lesson.teacher_names }} </li> <li class="collection-item"> - {{ period.get_lesson_documentation.topic }} + {{ register_object.get_lesson_documentation.topic }} </li> {% with period.get_lesson_documentation as lesson_documentation %} {% if lesson_documentation.homework %} <li class="collection-item"> <strong>{% trans "Homework" %}</strong> - {% firstof period.get_lesson_documentation.homework "–" %} + {% firstof register_object.get_lesson_documentation.homework "–" %} </li> {% endif %} {% if lesson_documentation.group_note %} <li class="collection-item"> <strong>{% trans "Group note" %}</strong> - {% firstof period.get_lesson_documentation.group_note "–" %} + {% firstof register_object.get_lesson_documentation.group_note "–" %} </li> {% endif %} {% endwith %} diff --git a/aleksis/apps/alsijil/util/alsijil_helpers.py b/aleksis/apps/alsijil/util/alsijil_helpers.py index 432afb18f..24e04320e 100644 --- a/aleksis/apps/alsijil/util/alsijil_helpers.py +++ b/aleksis/apps/alsijil/util/alsijil_helpers.py @@ -1,10 +1,14 @@ -from typing import Optional +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 calendarweek import CalendarWeek -from aleksis.apps.chronos.models import LessonPeriod +from aleksis.apps.alsijil.models import LessonDocumentation +from aleksis.apps.chronos.models import Event, ExtraLesson, LessonPeriod from aleksis.apps.chronos.util.chronos_helpers import get_el_by_pk @@ -44,3 +48,42 @@ def get_timetable_instance_by_pk( return get_el_by_pk(request, type_, id_) elif hasattr(request, "user") and hasattr(request.user, "person"): return request.user.person + + +def annotate_documentations( + klass: Union[Event, LessonPeriod, ExtraLesson], wanted_week: CalendarWeek, pks: List[int] +) -> QuerySet: + instances = klass.objects.prefetch_related( + Prefetch( + "documentations", + queryset=LessonDocumentation.objects.filter( + week=wanted_week.week, year=wanted_week.year + ), + ) + ).filter(pk__in=pks) + if klass == LessonPeriod: + instances = instances.annotate_week(wanted_week) + if klass in (LessonPeriod, ExtraLesson): + instances = instances.order_by("period__weekday", "period__period") + else: + instances = instances.order_by("period_from__weekday", "period_from__period") + args = {Event: "event", LessonPeriod: "lesson_period", ExtraLesson: "extra_lesson"} + instances = instances.annotate( + has_documentation=Exists( + LessonDocumentation.objects.filter( + ~Q(topic__exact=""), week=wanted_week.week, year=wanted_week.year, + ).filter(**{args[klass]: OuterRef("pk")}) + ) + ) + + return instances + + +def register_objects_sorter(register_object: Union[LessonPeriod, Event, ExtraLesson]) -> int: + """Sort key for sorted/sort for sorting a list of class register objects.""" + if hasattr(register_object, "period"): + return register_object.period.period + elif isinstance(register_object, Event): + return register_object.period_from_on_day + else: + return 0 diff --git a/aleksis/apps/alsijil/views.py b/aleksis/apps/alsijil/views.py index c5616940d..d5c1d44ae 100644 --- a/aleksis/apps/alsijil/views.py +++ b/aleksis/apps/alsijil/views.py @@ -1,3 +1,4 @@ +from copy import deepcopy from datetime import date, datetime, timedelta from typing import Optional @@ -18,7 +19,7 @@ from reversion.views import RevisionMixin from rules.contrib.views import PermissionRequiredMixin, permission_required from aleksis.apps.chronos.managers import TimetableType -from aleksis.apps.chronos.models import Holiday, LessonPeriod, TimePeriod +from aleksis.apps.chronos.models import Event, ExtraLesson, Holiday, LessonPeriod, TimePeriod from aleksis.apps.chronos.util.build import build_weekdays from aleksis.apps.chronos.util.date import get_weeks_for_year, week_weekday_to_date from aleksis.core.mixins import AdvancedCreateView, AdvancedDeleteView, AdvancedEditView @@ -34,9 +35,14 @@ from .forms import ( RegisterAbsenceForm, SelectForm, ) -from .models import ExcuseType, ExtraMark, LessonDocumentation, PersonalNote +from .models import ExcuseType, ExtraMark, PersonalNote from .tables import ExcuseTypeTable, ExtraMarkTable -from .util.alsijil_helpers import get_lesson_period_by_pk, get_timetable_instance_by_pk +from .util.alsijil_helpers import ( + annotate_documentations, + get_lesson_period_by_pk, + get_timetable_instance_by_pk, + register_objects_sorter, +) @permission_required("alsijil.view_lesson", fn=get_lesson_period_by_pk) @@ -199,8 +205,10 @@ def week_view( "lesson__groups__parent_groups", "lesson__groups__parent_groups__owners", ) + events = Event.objects.in_week(wanted_week) + extra_lessons = ExtraLesson.objects.in_week(wanted_week) - lesson_periods_query_exists = True + query_exists = True if type_ and id_: if isinstance(instance, HttpResponseNotFound): return HttpResponseNotFound() @@ -208,15 +216,26 @@ def week_view( type_ = TimetableType.from_string(type_) lesson_periods = lesson_periods.filter_from_type(type_, instance) + events = events.filter_from_type(type_, instance) + extra_lessons = extra_lessons.filter_from_Type(type_, instance) + elif hasattr(request, "user") and hasattr(request.user, "person"): if request.user.person.lessons_as_teacher.exists(): lesson_periods = lesson_periods.filter_teacher(request.user.person) + events = events.filter_teacher(request.user.person) + extra_lessons = extra_lessons.filter_teacher(request.user.person) + type_ = TimetableType.TEACHER else: lesson_periods = lesson_periods.filter_participant(request.user.person) + events = events.filter_participant(request.user.person) + extra_lessons = extra_lessons.filter_participant(request.user.person) + else: - lesson_periods_query_exists = False + query_exists = False lesson_periods = None + events = None + extra_lessons = None # Add a form to filter the view if type_: @@ -245,35 +264,21 @@ def week_view( extra_marks = ExtraMark.objects.all() - if lesson_periods_query_exists: + if query_exists: lesson_periods_pk = list(lesson_periods.values_list("pk", flat=True)) - lesson_periods = ( - LessonPeriod.objects.prefetch_related( - Prefetch( - "documentations", - queryset=LessonDocumentation.objects.filter( - week=wanted_week.week, year=wanted_week.year - ), - ) - ) - .filter(pk__in=lesson_periods_pk) - .annotate_week(wanted_week) - .annotate( - has_documentation=Exists( - LessonDocumentation.objects.filter( - ~Q(topic__exact=""), - lesson_period=OuterRef("pk"), - week=wanted_week.week, - year=wanted_week.year, - ) - ) - ) - .order_by("period__weekday", "period__period") - ) + lesson_periods = annotate_documentations(LessonPeriod, wanted_week, lesson_periods_pk) + + events_pk = list(events.values_list("pk", flat=True)) + events = annotate_documentations(Event, wanted_week, events_pk) + + extra_lessons_pk = list(extra_lessons.values_list("pk", flat=True)) + extra_lessons = annotate_documentations(ExtraLesson, wanted_week, extra_lessons_pk) else: lesson_periods_pk = [] + events_pk = [] + extra_lessons_pk = [] - if lesson_periods_pk: + if lesson_periods_pk or events_pk or extra_lessons_pk: # Aggregate all personal notes for this group and week persons_qs = Person.objects.filter(is_active=True) @@ -282,7 +287,29 @@ def week_view( elif 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.filter( + Q(member_of__lessons__lesson_periods__in=lesson_periods_pk) + | Q(member_of__events__in=events_pk) + | Q(member_of__extra_lessons__in=extra_lessons_pk) + ) + + personal_notes_q = ( + Q( + personal_notes__week=wanted_week.week, + personal_notes__year=wanted_week.year, + personal_notes__lesson_period__in=lesson_periods_pk, + ) + | Q( + personal_notes__event__date_start__lte=wanted_week[6], + personal_notes__event__date_end__gte=wanted_week[0], + personal_notes__event__in=events_pk, + ) + | Q( + personal_notes__extra_lesson__week=wanted_week.week, + personal_notes__extra_lesson__year=wanted_week.year, + personal_notes__extra_lesson__in=extra_lessons_pk, + ) + ) persons_qs = ( persons_qs.distinct() @@ -290,9 +317,21 @@ def week_view( Prefetch( "personal_notes", queryset=PersonalNote.objects.filter( - week=wanted_week.week, - year=wanted_week.year, - lesson_period__in=lesson_periods_pk, + Q( + week=wanted_week.week, + year=wanted_week.year, + lesson_period__in=lesson_periods_pk, + ) + | Q( + event__date_start__lte=wanted_week[6], + event__date_end__gte=wanted_week[0], + event__in=events_pk, + ) + | Q( + extra_lesson__week=wanted_week.week, + extra_lesson__year=wanted_week.year, + extra_lesson__in=extra_lessons_pk, + ) ), ), "member_of__owners", @@ -300,44 +339,25 @@ def week_view( .annotate( absences_count=Count( "personal_notes", - filter=Q( - personal_notes__lesson_period__in=lesson_periods_pk, - personal_notes__week=wanted_week.week, - personal_notes__year=wanted_week.year, - personal_notes__absent=True, - ), + filter=personal_notes_q & Q(personal_notes__absent=True,), distinct=True, ), unexcused_count=Count( "personal_notes", - filter=Q( - personal_notes__lesson_period__in=lesson_periods_pk, - personal_notes__week=wanted_week.week, - personal_notes__year=wanted_week.year, - personal_notes__absent=True, - personal_notes__excused=False, - ), + filter=personal_notes_q + & Q(personal_notes__absent=True, personal_notes__excused=False,), distinct=True, ), tardiness_sum=Subquery( - Person.objects.filter( - pk=OuterRef("pk"), - personal_notes__lesson_period__in=lesson_periods_pk, - personal_notes__week=wanted_week.week, - personal_notes__year=wanted_week.year, - ) + Person.objects.filter(personal_notes_q) + .filter(pk=OuterRef("pk"),) .distinct() .annotate(tardiness_sum=Sum("personal_notes__late")) .values("tardiness_sum") ), tardiness_count=Count( "personal_notes", - filter=Q( - personal_notes__lesson_period__in=lesson_periods_pk, - personal_notes__week=wanted_week.week, - personal_notes__year=wanted_week.year, - ) - & ~Q(personal_notes__late=0), + filter=personal_notes_q & ~Q(personal_notes__late=0), distinct=True, ), ) @@ -348,12 +368,7 @@ def week_view( **{ extra_mark.count_label: Count( "personal_notes", - filter=Q( - personal_notes__lesson_period__in=lesson_periods_pk, - personal_notes__week=wanted_week.week, - personal_notes__year=wanted_week.year, - personal_notes__extra_marks=extra_mark, - ), + filter=personal_notes_q & Q(personal_notes__extra_marks=extra_mark,), distinct=True, ) } @@ -368,13 +383,42 @@ def week_view( context["extra_marks"] = extra_marks context["week"] = wanted_week context["weeks"] = get_weeks_for_year(year=wanted_week.year) + context["lesson_periods"] = lesson_periods + context["events"] = events + context["extra_lessons"] = extra_lessons + context["persons"] = persons context["group"] = group context["select_form"] = select_form context["instance"] = instance context["weekdays"] = build_weekdays(TimePeriod.WEEKDAY_CHOICES, wanted_week) + regrouped_objects = {} + + for register_object in list(lesson_periods) + list(extra_lessons): + regrouped_objects.setdefault(register_object.period.weekday, []) + regrouped_objects[register_object.period.weekday].append(register_object) + + for event in events: + weekday_from = event.get_start_weekday(wanted_week) + weekday_to = event.get_end_weekday(wanted_week) + print(weekday_from, weekday_to) + + for weekday in range(weekday_from, weekday_to + 1): + # Make a copy in order to keep the annotation only on this weekday + event_copy = deepcopy(event) + event.annotate_day(wanted_week[weekday]) + + regrouped_objects.setdefault(weekday, []) + regrouped_objects[weekday].append(event_copy) + + # Sort register objects + for weekday in regrouped_objects.keys(): + to_sort = regrouped_objects[weekday] + regrouped_objects[weekday] = sorted(to_sort, key=register_objects_sorter) + context["regrouped_objects"] = regrouped_objects + week_prev = wanted_week - 1 week_next = wanted_week + 1 args_prev = [week_prev.year, week_prev.week] -- GitLab