from datetime import date, datetime, timedelta from typing import Optional from django.core.exceptions import PermissionDenied 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 import reversion from calendarweek import CalendarWeek from django_tables2 import SingleTableView 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 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 from .forms import ( ExcuseTypeForm, ExtraMarkForm, 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() else: wanted_week = None if not (year and week and period_id): if lesson_period: return redirect( "lesson_by_week_and_period", wanted_week.year, wanted_week.week, lesson_period.pk, ) else: raise Http404( _( "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() if ( datetime.combine( wanted_week[lesson_period.period.weekday], lesson_period.period.time_start, ) > datetime.now() 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 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 persons = Person.objects.all() 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 request.method == "POST": 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 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) def week_view( request: HttpRequest, year: Optional[int] = None, week: Optional[int] = None, type_: Optional[str] = None, id_: Optional[int] = None, ) -> HttpResponse: 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"): if request.user.person.lessons_as_teacher.exists(): lesson_periods = lesson_periods.filter_teacher(request.user.person) type_ = TimetableType.TEACHER else: lesson_periods = lesson_periods.filter_participant(request.user.person) else: lesson_periods = None # 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 if lesson_periods: # Aggregate all personal notes for this group and week 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) 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.distinct() .prefetch_related("personal_notes") .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, ), 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, ), 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, ) .distinct() .annotate(tardiness_sum=Sum("personal_notes__late")) .values("tardiness_sum") ), ) ) for extra_mark in ExtraMark.objects.all(): persons_qs = persons_qs.annotate( **{ 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, ), distinct=True, ) } ) 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, ), } ) 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["week"] = wanted_week 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 context["instance"] = instance 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") ) weeks = CalendarWeek.weeks_within( current_school_term.date_start, current_school_term.date_end, ) periods_by_day = {} for lesson_period in lesson_periods: for week in weeks: day = week[lesson_period.period.weekday] if ( lesson_period.lesson.validity.date_start <= day <= lesson_period.lesson.validity.date_end ): documentations = list( filter( lambda d: d.week == week.week and d.year == week.year, lesson_period.documentations.all(), ) ) notes = list( filter( 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( absences_count=Count( "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"), ) for extra_mark in ExtraMark.objects.all(): persons = persons.annotate( **{ extra_mark.count_label: Count( "personal_notes", filter=Q( personal_notes__extra_marks=extra_mark, personal_notes__lesson_period__lesson__validity__school_term=current_school_term, ), ) } ) 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, personal_notes__lesson_period__lesson__validity__school_term=current_school_term, ), ) } ) context["school_term"] = current_school_term context["persons"] = persons 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 context["lesson_periods"] = lesson_periods context["today"] = date.today() return render(request, "alsijil/print/full_register.html", context) @permission_required("alsijil.view_my_students") 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).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 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) @permission_required("alsijil.view_register_absence") def register_absence(request: HttpRequest) -> HttpResponse: context = {} register_absence_form = RegisterAbsenceForm(request.POST or None, request=request) if request.method == "POST": if register_absence_form.is_valid() and request.user.has_perm( "alsijil.register_absence", register_absence_form.cleaned_data["person"] ): # Get data from form 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"] to_period = register_absence_form.cleaned_data["to_period"] absent = register_absence_form.cleaned_data["absent"] excused = register_absence_form.cleaned_data["excused"] 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 for i in range(delta.days + 1): 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 ) day = start_date + timedelta(days=i) 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.")) 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() note.save() messages.success(request, _("The personal note has been deleted.")) return redirect("overview_person", note.person.pk) class ExtraMarkListView(PermissionRequiredMixin, SingleTableView): """Table of all extra marks.""" model = ExtraMark table_class = ExtraMarkTable permission_required = "alsijil.view_extramark" template_name = "alsijil/extra_mark/list.html" class ExtraMarkCreateView(PermissionRequiredMixin, AdvancedCreateView): """Create view for extra marks.""" 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): """Edit view for extra marks.""" 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): """Delete view for extra marks""" model = ExtraMark 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 permission_required = "alsijil.view_excusetypes" 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.")