Skip to content
Snippets Groups Projects
Verified Commit 517ca820 authored by Jonathan Weth's avatar Jonathan Weth :keyboard:
Browse files

Add overview of all register objects for teachers at "My overview"

parent 9e3f1aec
No related branches found
No related tags found
1 merge request!152Overview of all register objects
from datetime import datetime from datetime import datetime, timedelta
from django import forms from django import forms
from django.core.exceptions import ValidationError from django.core.exceptions import ValidationError
from django.db.models import Count, Q from django.db.models import Count, Q
from django.utils import timezone
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from django_select2.forms import ModelSelect2MultipleWidget, ModelSelect2Widget, Select2Widget from django_select2.forms import ModelSelect2MultipleWidget, ModelSelect2Widget, Select2Widget
...@@ -10,8 +11,8 @@ from guardian.shortcuts import get_objects_for_user ...@@ -10,8 +11,8 @@ from guardian.shortcuts import get_objects_for_user
from material import Fieldset, Layout, Row from material import Fieldset, Layout, Row
from aleksis.apps.chronos.managers import TimetableType from aleksis.apps.chronos.managers import TimetableType
from aleksis.apps.chronos.models import TimePeriod from aleksis.apps.chronos.models import Subject, TimePeriod
from aleksis.core.models import Group, Person from aleksis.core.models import Group, Person, SchoolTerm
from aleksis.core.util.core_helpers import get_site_preferences from aleksis.core.util.core_helpers import get_site_preferences
from aleksis.core.util.predicates import check_global_permission from aleksis.core.util.predicates import check_global_permission
...@@ -254,3 +255,64 @@ class GroupRoleAssignmentEditForm(forms.ModelForm): ...@@ -254,3 +255,64 @@ class GroupRoleAssignmentEditForm(forms.ModelForm):
class Meta: class Meta:
model = GroupRoleAssignment model = GroupRoleAssignment
fields = ["date_start", "date_end"] 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
...@@ -78,3 +78,28 @@ class GroupRoleTable(tables.Table): ...@@ -78,3 +78,28 @@ class GroupRoleTable(tables.Table):
self.columns.hide("edit") self.columns.hide("edit")
if not request.user.has_perm("alsijil.delete_grouprole"): if not request.user.has_perm("alsijil.delete_grouprole"):
self.columns.hide("delete") 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,
),
)
...@@ -2,7 +2,7 @@ ...@@ -2,7 +2,7 @@
{% now_datetime as now_dt %} {% 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> <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 %} {% elif not register_object.period %}
{% period_to_time_start week register_object.raw_period_from_on_day as time_start %} {% period_to_time_start week register_object.raw_period_from_on_day as time_start %}
...@@ -19,13 +19,13 @@ ...@@ -19,13 +19,13 @@
{% period_to_time_start week register_object.period as time_start %} {% period_to_time_start week register_object.period as time_start %}
{% period_to_time_end week register_object.period as time_end %} {% 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> <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 %} {% 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> <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 %} {% 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> <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> <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 %}
{% endif %} {% endif %}
from operator import itemgetter
from typing import List, Optional, Union from typing import List, Optional, Union
from django.db.models.expressions import Exists, OuterRef from django.db.models.expressions import Exists, OuterRef
from django.db.models.query import Prefetch, QuerySet from django.db.models.query import Prefetch, QuerySet
from django.db.models.query_utils import Q from django.db.models.query_utils import Q
from django.http import HttpRequest from django.http import HttpRequest
from django.utils.formats import date_format
from django.utils.translation import gettext as _
from calendarweek import CalendarWeek from calendarweek import CalendarWeek
from aleksis.apps.alsijil.forms import FilterRegisterObjectForm
from aleksis.apps.alsijil.models import LessonDocumentation 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.apps.chronos.util.chronos_helpers import get_el_by_pk
from aleksis.core.models import SchoolTerm
def get_register_object_by_pk( def get_register_object_by_pk(
...@@ -99,3 +110,216 @@ def register_objects_sorter(register_object: Union[LessonPeriod, Event, ExtraLes ...@@ -99,3 +110,216 @@ def register_objects_sorter(register_object: Union[LessonPeriod, Event, ExtraLes
return register_object.period_from_on_day return register_object.period_from_on_day
else: else:
return 0 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 []
...@@ -18,7 +18,7 @@ from django.views.generic import DetailView ...@@ -18,7 +18,7 @@ from django.views.generic import DetailView
import reversion import reversion
from calendarweek import CalendarWeek from calendarweek import CalendarWeek
from django_tables2 import SingleTableView from django_tables2 import RequestConfig, SingleTableView
from reversion.views import RevisionMixin from reversion.views import RevisionMixin
from rules.contrib.views import PermissionRequiredMixin, permission_required from rules.contrib.views import PermissionRequiredMixin, permission_required
...@@ -40,6 +40,7 @@ from .forms import ( ...@@ -40,6 +40,7 @@ from .forms import (
AssignGroupRoleForm, AssignGroupRoleForm,
ExcuseTypeForm, ExcuseTypeForm,
ExtraMarkForm, ExtraMarkForm,
FilterRegisterObjectForm,
GroupRoleAssignmentEditForm, GroupRoleAssignmentEditForm,
GroupRoleForm, GroupRoleForm,
LessonDocumentationForm, LessonDocumentationForm,
...@@ -55,9 +56,10 @@ from .models import ( ...@@ -55,9 +56,10 @@ from .models import (
LessonDocumentation, LessonDocumentation,
PersonalNote, PersonalNote,
) )
from .tables import ExcuseTypeTable, ExtraMarkTable, GroupRoleTable from .tables import ExcuseTypeTable, ExtraMarkTable, GroupRoleTable, RegisterObjectTable
from .util.alsijil_helpers import ( from .util.alsijil_helpers import (
annotate_documentations, annotate_documentations,
generate_list_of_all_register_objects,
get_register_object_by_pk, get_register_object_by_pk,
get_timetable_instance_by_pk, get_timetable_instance_by_pk,
register_objects_sorter, register_objects_sorter,
...@@ -894,6 +896,16 @@ def overview_person(request: HttpRequest, id_: Optional[int] = None) -> HttpResp ...@@ -894,6 +896,16 @@ def overview_person(request: HttpRequest, id_: Optional[int] = None) -> HttpResp
context["excuse_types"] = excuse_types context["excuse_types"] = excuse_types
context["extra_marks"] = extra_marks 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) return render(request, "alsijil/class_register/person.html", context)
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment