Newer
Older
from typing import Optional
from django.db.models import Count, Exists, OuterRef, Q, Subquery, Sum
from django.http import Http404, HttpRequest, HttpResponse, HttpResponseNotFound
from django.shortcuts import get_object_or_404, redirect, render
from django.urls import reverse, reverse_lazy
from django.utils.translation import ugettext as _
from django.views.generic import DetailView
from reversion.views import RevisionMixin
from rules.contrib.views import PermissionRequiredMixin, permission_required
from aleksis.apps.chronos.managers import TimetableType

Jonathan Weth
committed
from aleksis.apps.chronos.models import LessonPeriod, TimePeriod
from aleksis.apps.chronos.util.chronos_helpers import get_el_by_pk
from aleksis.apps.chronos.util.date import get_weeks_for_year, week_weekday_to_date
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, objectgetter_optional
LessonDocumentationForm,
PersonalNoteFormSet,
RegisterAbsenceForm,
SelectForm,
)
from .models import ExcuseType, ExtraMark, LessonDocumentation, PersonalNote
from .tables import ExcuseTypeTable, ExtraMarkTable
from .util.alsijil_helpers import get_instance_by_pk, get_lesson_period_by_pk
@permission_required("alsijil.view_lesson", fn=get_lesson_period_by_pk)
def lesson(
request: HttpRequest,
year: Optional[int] = None,
week: Optional[int] = None,
period_id: Optional[int] = None,
) -> HttpResponse:
context = {}
lesson_period = get_lesson_period_by_pk(request, year, week, period_id)
if period_id:
wanted_week = CalendarWeek(year=year, week=week)
elif hasattr(request, "user") and hasattr(request.user, "person"):
wanted_week = CalendarWeek()
if not (year and week and period_id):
if lesson_period:
"lesson_by_week_and_period",
wanted_week.year,
wanted_week.week,
lesson_period.pk,
"You either selected an invalid lesson or "
"there is currently no lesson in progress."
date_of_lesson = week_weekday_to_date(wanted_week, lesson_period.period.weekday)
if (
date_of_lesson < lesson_period.lesson.validity.date_start
or date_of_lesson > lesson_period.lesson.validity.date_end
):
return HttpResponseNotFound()
wanted_week[lesson_period.period.weekday], lesson_period.period.time_start,

Jonathan Weth
committed
and not (
get_site_preferences()["alsijil__open_periods_same_day"]
and wanted_week[lesson_period.period.weekday] <= datetime.now().date()
)
and not request.user.is_superuser
):
raise PermissionDenied(
_(
"You are not allowed to create a lesson documentation for a lesson in the future."
)
context["lesson_period"] = lesson_period
context["week"] = wanted_week
context["day"] = wanted_week[lesson_period.period.weekday]
# Create or get lesson documentation object; can be empty when first opening lesson

Jonathan Weth
committed
lesson_documentation = lesson_period.get_or_create_lesson_documentation(wanted_week)
lesson_documentation_form = LessonDocumentationForm(
request.POST or None,
instance=lesson_documentation,
prefix="lesson_documentation",
# Create a formset that holds all personal notes for all persons in this lesson
if not request.user.has_perm("alsijil.view_lesson_personalnote", lesson_period):
persons = persons.filter(pk=request.user.person.pk)
persons_qs = lesson_period.get_personal_notes(persons, wanted_week)
personal_note_formset = PersonalNoteFormSet(
request.POST or None, queryset=persons_qs, prefix="personal_notes"
)
if lesson_documentation_form.is_valid() and request.user.has_perm(
"alsijil.edit_lessondocumentation", lesson_period
):
lesson_documentation_form.save()
messages.success(request, _("The lesson documentation has been saved."))
substitution = lesson_period.get_substitution()
if (
not getattr(substitution, "cancelled", False)
or not get_site_preferences()["alsijil__block_personal_notes_for_cancelled"]
if personal_note_formset.is_valid() and request.user.has_perm(
"alsijil.edit_lesson_personalnote", lesson_period
with reversion.create_revision():
instances = personal_note_formset.save()
# Iterate over personal notes and carry changed absences to following lessons
for instance in instances:
instance.person.mark_absent(
wanted_week[lesson_period.period.weekday],
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

Jonathan Weth
committed
context["prev_lesson"] = lesson_period.prev
context["next_lesson"] = lesson_period.next
return render(request, "alsijil/class_register/lesson.html", context)
@permission_required("alsijil.view_week", fn=get_instance_by_pk)
request: HttpRequest,
year: Optional[int] = None,
week: Optional[int] = None,
type_: Optional[str] = None,
id_: Optional[int] = None,
context = {}
if year and week:
wanted_week = CalendarWeek(year=year, week=week)
else:
wanted_week = CalendarWeek()
instance = get_instance_by_pk(request, year, week, type_, id_)
lesson_periods = LessonPeriod.objects.in_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,
if type_ and id_:
if isinstance(instance, HttpResponseNotFound):
return HttpResponseNotFound()
type_ = TimetableType.from_string(type_)
lesson_periods = lesson_periods.filter_from_type(type_, instance)
elif hasattr(request, "user") and hasattr(request.user, "person"):
lesson_periods = lesson_periods.filter_teacher(request.user.person)
else:
lesson_periods = lesson_periods.filter_participant(request.user.person)
# Add a form to filter the view
if type_:
initial = {type_.value: instance}
else:
initial = {}
select_form = SelectForm(request.POST or None, initial=initial)
if request.method == "POST":
if select_form.is_valid():
if "type_" not in select_form.cleaned_data:
return redirect("week_view_by_week", wanted_week.year, wanted_week.week)
else:
return redirect(
"week_view_by_week",
wanted_week.year,
wanted_week.week,
select_form.cleaned_data["type_"].value,
select_form.cleaned_data["instance"].pk,
)
if type_ == TimetableType.GROUP:
group = instance
else:
group = None
# Aggregate all personal notes for this group and week

Jonathan Weth
committed
lesson_periods_pk = list(lesson_periods.values_list("pk", flat=True))
persons_qs = Person.objects.filter(is_active=True)
if not request.user.has_perm("alsijil.view_week_personalnote", instance):
persons_qs = persons_qs.filter(pk=request.user.person.pk)

Jonathan Weth
committed
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(
"personal_notes",

Jonathan Weth
committed
personal_notes__lesson_period__in=lesson_periods_pk,
personal_notes__week=wanted_week.week,
personal_notes__year=wanted_week.year,
distinct=True,
"personal_notes",

Jonathan Weth
committed
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,
),
distinct=True,
tardiness_sum=Subquery(
Person.objects.filter(
pk=OuterRef("pk"),

Jonathan Weth
committed
personal_notes__lesson_period__in=lesson_periods_pk,
personal_notes__week=wanted_week.week,
personal_notes__year=wanted_week.year,
)
.distinct()
.annotate(tardiness_sum=Sum("personal_notes__late"))
.values("tardiness_sum")

Jonathan Weth
committed
for extra_mark in ExtraMark.objects.all():

Jonathan Weth
committed
persons_qs = persons_qs.annotate(
**{
extra_mark.count_label: Count(
"personal_notes",
filter=Q(

Jonathan Weth
committed
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,
),
distinct=True,
)
}
)

Jonathan Weth
committed
persons = []
for person in persons_qs:
persons.append(
{
"person": person,
"personal_notes": person.personal_notes.filter(
week=wanted_week.week,
year=wanted_week.year,
lesson_period__in=lesson_periods_pk,

Jonathan Weth
committed
),
}
)
else:
persons = None
# Resort lesson periods manually because an union queryset doesn't support order_by
lesson_periods = sorted(
lesson_periods, key=lambda x: (x.period.weekday, x.period.period)
)
context["extra_marks"] = ExtraMark.objects.all()
context["weeks"] = get_weeks_for_year(year=wanted_week.year)
context["lesson_periods"] = lesson_periods
context["persons"] = persons
context["group"] = group
context["select_form"] = select_form
week_prev = wanted_week - 1
week_next = wanted_week + 1
args_prev = [week_prev.year, week_prev.week]
args_next = [week_next.year, week_next.week]
args_dest = []
if type_ and id_:
args_prev += [type_.value, id_]
args_next += [type_.value, id_]
args_dest += [type_.value, id_]
context["week_select"] = {
"year": wanted_week.year,
"dest": reverse("week_view_placeholders", args=args_dest),
context["url_prev"] = reverse("week_view_by_week", args=args_prev)
context["url_next"] = reverse("week_view_by_week", args=args_next)
return render(request, "alsijil/class_register/week_view.html", context)
@permission_required(
"alsijil.view_full_register", fn=objectgetter_optional(Group, None, False)
)
def full_register_group(request: HttpRequest, id_: int) -> HttpResponse:
context = {}
group = get_object_or_404(Group, pk=id_)
current_school_term = SchoolTerm.current
if not current_school_term:
return HttpResponseNotFound(_("There is no current school term."))
# Get all lesson periods for the selected group
lesson_periods = (
LessonPeriod.objects.filter_group(group)
.filter(lesson__validity__school_term=current_school_term)
.distinct()
.prefetch_related("documentations", "personal_notes")
current_school_term.date_start, current_school_term.date_end,
periods_by_day = {}
for lesson_period in lesson_periods:
for week in weeks:
if (
lesson_period.lesson.validity.date_start
<= day
<= lesson_period.lesson.validity.date_end
):
lambda d: d.week == week.week and d.year == week.year,
lesson_period.documentations.all(),
)
lambda d: d.week == week.week and d.year == week.year,
lesson_period.personal_notes.all(),
)
substitution = lesson_period.get_substitution(week)
periods_by_day.setdefault(day, []).append(
(lesson_period, documentations, notes, substitution)
)
persons = Person.objects.filter(
personal_notes__groups_of_person=group,
personal_notes__lesson_period__lesson__validity__school_term=current_school_term,
).annotate(
"personal_notes__absent",
filter=Q(
personal_notes__absent=True,
personal_notes__lesson_period__lesson__validity__school_term=current_school_term,
excused=Count(
"personal_notes__absent",
filter=Q(
personal_notes__absent=True,
personal_notes__excused=True,
personal_notes__excuse_type__isnull=True,
personal_notes__lesson_period__lesson__validity__school_term=current_school_term,
unexcused=Count(
"personal_notes__absent",
filter=Q(
personal_notes__absent=True,
personal_notes__excused=False,
personal_notes__lesson_period__lesson__validity__school_term=current_school_term,
),
),
tardiness=Sum("personal_notes__late"),

Jonathan Weth
committed
for extra_mark in ExtraMark.objects.all():

Jonathan Weth
committed
extra_mark.count_label: Count(
"personal_notes",
personal_notes__extra_marks=extra_mark,
personal_notes__lesson_period__lesson__validity__school_term=current_school_term,
),
)
}
)
for excuse_type in ExcuseType.objects.all():

Nik | Klampfradler
committed
persons = persons.annotate(
excuse_type.count_label: Count(
"personal_notes__absent",
personal_notes__absent=True,
personal_notes__excuse_type=excuse_type,
personal_notes__lesson_period__lesson__validity__school_term=current_school_term,

Nik | Klampfradler
committed
)
context["school_term"] = current_school_term
context["excuse_types"] = ExcuseType.objects.all()

Jonathan Weth
committed
context["extra_marks"] = ExtraMark.objects.all()
context["group"] = group
context["weeks"] = weeks
context["periods_by_day"] = periods_by_day
context["lesson_periods"] = lesson_periods
return render(request, "alsijil/print/full_register.html", context)
@permission_required("alsijil.view_my_students")
def my_students(request: HttpRequest) -> HttpResponse:
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).distinct()
context["persons"] = persons
return render(request, "alsijil/class_register/persons.html", context)
@permission_required(
"alsijil.view_person_overview",
fn=objectgetter_optional(Person, "request.user.person", True),
)
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()
if not request.user.has_perm(
"alsijil.edit_person_overview_personalnote", person
):
raise PermissionDenied()
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,
)
for note in notes:
note.excused = True
note.excuse_type = excuse_type
with reversion.create_revision():
note.save()
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 not request.user.has_perm("alsijil.edit_personalnote", note):
raise PermissionDenied()
if note.absent:
note.excused = True
note.excuse_type = excuse_type
with reversion.create_revision():
note.save()
messages.success(
request, _("The absence has been marked as excused.")
)
except (PersonalNote.DoesNotExist, ValueError):
pass
person.refresh_from_db()
allowed_personal_notes = person.personal_notes.all()
if not request.user.has_perm("alsijil.view_person_overview_personalnote", person):
print("has")
allowed_personal_notes = allowed_personal_notes.filter(
lesson_period__lesson__groups__owners=request.user.person
)
print(allowed_personal_notes)
unexcused_absences = allowed_personal_notes.filter(absent=True, excused=False)
context["unexcused_absences"] = unexcused_absences
personal_notes = allowed_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()
if request.user.has_perm("alsijil.view_person_statistics_personalnote", person):
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
personal_notes.filter(absent=True).aggregate(
absences_count=Count("absent")
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)
@permission_required("alsijil.view_register_absence")
def register_absence(request: HttpRequest) -> HttpResponse:
register_absence_form = RegisterAbsenceForm(request.POST or None, request=request)
if register_absence_form.is_valid() and request.user.has_perm(
"alsijil.register_absence", register_absence_form.cleaned_data["person"]
):
person = register_absence_form.cleaned_data["person"]
start_date = register_absence_form.cleaned_data["date_start"]
end_date = register_absence_form.cleaned_data["date_end"]
from_period = register_absence_form.cleaned_data["from_period"]

Jonathan Weth
committed
to_period = register_absence_form.cleaned_data["to_period"]
absent = register_absence_form.cleaned_data["absent"]
excused = register_absence_form.cleaned_data["excused"]

Jonathan Weth
committed
excuse_type = register_absence_form.cleaned_data["excuse_type"]
remarks = register_absence_form.cleaned_data["remarks"]
# Mark person as absent
delta = end_date - start_date

Jonathan Weth
committed
from_period_on_day = from_period if i == 0 else TimePeriod.period_min
to_period_on_day = (
to_period if i == delta.days else TimePeriod.period_max
)

Jonathan Weth
committed
person.mark_absent(
day,
from_period_on_day,
absent,
excused,
excuse_type,
remarks,
to_period_on_day,
)
messages.success(request, _("The absence has been saved."))

Jonathan Weth
committed
return redirect("register_absence")
context["register_absence_form"] = register_absence_form
return render(request, "alsijil/absences/register.html", context)
class DeletePersonalNoteView(PermissionRequiredMixin, DetailView):
model = PersonalNote
template_name = "core/pages/delete.html"
permission_required = "alsijil.edit_personalnote"
def post(self, request, *args, **kwargs):
note = self.get_object()
with reversion.create_revision():
note.reset_values()
messages.success(request, _("The personal note has been deleted."))
return redirect("overview_person", note.person.pk)
class ExtraMarkListView(PermissionRequiredMixin, SingleTableView):
model = ExtraMark
table_class = ExtraMarkTable
permission_required = "alsijil.view_extramark"
template_name = "alsijil/extra_mark/list.html"
class ExtraMarkCreateView(PermissionRequiredMixin, AdvancedCreateView):
model = ExtraMark
form_class = ExtraMarkForm
permission_required = "alsijil.create_extramark"
template_name = "alsijil/extra_mark/create.html"
success_url = reverse_lazy("extra_marks")
success_message = _("The extra mark has been created.")
class ExtraMarkEditView(PermissionRequiredMixin, AdvancedEditView):
model = ExtraMark
form_class = ExtraMarkForm
permission_required = "alsijil.edit_extramark"
template_name = "alsijil/extra_mark/edit.html"
success_url = reverse_lazy("extra_marks")
success_message = _("The extra mark has been saved.")
class ExtraMarkDeleteView(PermissionRequiredMixin, RevisionMixin, AdvancedDeleteView):
permission_required = "alsijil.delete_extramark"
template_name = "core/pages/delete.html"
success_url = reverse_lazy("extra_marks")
success_message = _("The extra mark has been deleted.")
class ExcuseTypeListView(PermissionRequiredMixin, SingleTableView):
"""Table of all excuse types."""
model = ExcuseType
table_class = ExcuseTypeTable
template_name = "alsijil/excuse_type/list.html"
class ExcuseTypeCreateView(PermissionRequiredMixin, AdvancedCreateView):
"""Create view for excuse types."""
model = ExcuseType
form_class = ExcuseTypeForm
permission_required = "alsijil.add_excusetype"
template_name = "alsijil/excuse_type/create.html"
success_url = reverse_lazy("excuse_types")
success_message = _("The excuse type has been created.")
class ExcuseTypeEditView(PermissionRequiredMixin, AdvancedEditView):
"""Edit view for excuse types."""
model = ExcuseType
form_class = ExcuseTypeForm
permission_required = "alsijil.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(PermissionRequiredMixin, RevisionMixin, AdvancedDeleteView):
"""Delete view for excuse types"""
model = ExcuseType
permission_required = "alsijil.delete_excusetype"
template_name = "core/pages/delete.html"
success_url = reverse_lazy("excuse_types")
success_message = _("The excuse type has been deleted.")