from datetime import date, datetime, timedelta from typing import Optional from django.contrib.auth.decorators import login_required from django.core.exceptions import PermissionDenied from django.db.models import Count, Exists, OuterRef, Q, Sum from django.http import Http404, HttpRequest, HttpResponse from django.shortcuts import get_object_or_404, redirect, render from django.urls import reverse from django.utils.translation import ugettext as _ from biscuit.apps.chronos.models import LessonPeriod from biscuit.apps.chronos.util import CalendarWeek from biscuit.core.models import Group, Person from biscuit.core.decorators import admin_required from biscuit.core.util import messages from .forms import LessonDocumentationForm, PersonalNoteFormSet, RegisterAbsenceForm, SelectForm from .models import LessonDocumentation, PersonalNoteFilter @login_required def lesson(request: HttpRequest, year: Optional[int] = None, week: Optional[int] = None, period_id: Optional[int] = None) -> HttpResponse: context = {} if year and week and period_id: # Get a specific lesson period if provided in URL lesson_period = LessonPeriod.objects.get(pk=period_id) wanted_week = CalendarWeek(year=year, week=week) else: # Determine current lesson by current date and time lesson_period = LessonPeriod.objects.at_time().filter_teacher(request.user.person).first() wanted_week = CalendarWeek() 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.')) if datetime.combine(wanted_week[lesson_period.period.weekday - 1], lesson_period.period.time_start) > datetime.now() 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 - 1] # Create or get lesson documentation object; can be empty when first opening lesson lesson_documentation, created = LessonDocumentation.objects.get_or_create( lesson_period=lesson_period, week=wanted_week.week) lesson_documentation_form = LessonDocumentationForm( request.POST or None, instance=lesson_documentation, prefix='leson_documentation') # Create a formset that holds all personal notes for all persons in this lesson persons_qs = lesson_period.get_personal_notes(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(): lesson_documentation_form.save() if personal_note_formset.is_valid(): 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-1], lesson_period.period.period+1, instance.absent, instance.excused ) context['lesson_documentation'] = lesson_documentation context['lesson_documentation_form'] = lesson_documentation_form context['personal_note_formset'] = personal_note_formset return render(request, 'alsijil/lesson.html', context) @login_required def week_view(request: HttpRequest, year: Optional[int] = None, week: Optional[int] = None) -> HttpResponse: context = {} if year and week: wanted_week = CalendarWeek(year=year, week=week) else: wanted_week = CalendarWeek() lesson_periods = LessonPeriod.objects.annotate( has_documentation=Exists(LessonDocumentation.objects.filter( ~Q(topic__exact=''), lesson_period=OuterRef('pk'), week=wanted_week.week )) ).in_week(wanted_week) group = None # FIXME workaround for #38 if request.GET.get('group', None) or request.GET.get('teacher', None) or request.GET.get('room', None): lesson_periods = lesson_periods.filter_from_query(request.GET) if 'group' in request.GET and request.GET['group']: group = Group.objects.get(pk=request.GET['group']) else: group = None elif hasattr(request, 'user') and hasattr(request.user, 'person'): group = request.user.person.owner_of.first() if group: lesson_periods = lesson_periods.filter_group(group) elif request.user.person.lessons_as_teacher.exists(): lesson_periods = lesson_periods.filter_teacher(request.user.person) else: lesson_periods = lesson_periods.filter_participant(request.user.person) else: lesson_periods = None if lesson_periods: # Aggregate all personal notes for this group and week persons = Person.objects.filter( is_active=True ).filter( Q(member_of=group) | Q(member_of__parent_groups=group) ).distinct().prefetch_related( 'personal_notes' ).annotate( absences=Count('personal_notes__absent', filter=Q( personal_notes__lesson_period__in=lesson_periods, personal_notes__week=wanted_week.week, personal_notes__absent=True )), unexcused=Count('personal_notes__absent', filter=Q( personal_notes__lesson_period__in=lesson_periods, personal_notes__week=wanted_week.week, personal_notes__absent=True, personal_notes__excused=False )), tardiness=Sum('personal_notes__late', filter=Q( personal_notes__lesson_period__in=lesson_periods, personal_notes__week=wanted_week.week )) ) else: persons = None # Add a form to filter the view select_form = SelectForm(request.GET or None) context['current_head'] = str(wanted_week) context['week'] = wanted_week 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 context['url_prev'] = '%s?%s' % (reverse('week_view_by_week', args=[week_prev.year, week_prev.week]), request.GET.urlencode()) context['url_next'] = '%s?%s' % (reverse('week_view_by_week', args=[week_next.year, week_next.week]), request.GET.urlencode()) return render(request, 'alsijil/week_view.html', context) @login_required def full_register_group(request: HttpRequest, id_: int) -> HttpResponse: context = {} group = get_object_or_404(Group, pk=id_) # Get all lesson periods for the selected group lesson_periods = LessonPeriod.objects.filter_group( group ).distinct().prefetch_related( 'documentations', 'personal_notes' ) weeks = CalendarWeek.weeks_within(group.school.current_term.date_start, group.school.current_term.date_end) periods_by_day = {} for lesson_period in lesson_periods: for week in weeks: day = week[lesson_period.period.weekday - 1] if lesson_period.lesson.date_start <= day and lesson_period.lesson.date_end >= day: documentations = list(filter(lambda d: d.week == week.week, lesson_period.documentations.all())) notes = list(filter(lambda d: d.week == week.week, lesson_period.personal_notes.all())) substitution = lesson_period.get_substitution(week.week) periods_by_day.setdefault(day, []).append((lesson_period, documentations, notes, substitution)) persons = group.members.annotate( absences=Count('personal_notes__absent', filter=Q( personal_notes__absent=True )), unexcused=Count('personal_notes__absent', filter=Q( personal_notes__absent=True, personal_notes__excused=False )), tardiness=Sum('personal_notes__late') ) # FIXME Move to manager personal_note_filters = PersonalNoteFilter.objects.all() for personal_note_filter in personal_note_filters: persons = persons.annotate( **{'_personal_notes_with_%s' % personal_note_filter.identifier: Count( 'personal_notes__remarks', filter=Q(personal_notes__remarks__iregex=personal_note_filter.regex) )} ) context['persons'] = persons context['personal_note_filters'] = personal_note_filters context['group'] = group context['weeks'] = weeks context['periods_by_day'] = periods_by_day context['today'] = date.today() return render(request, 'alsijil/print/full_register.html', context) @admin_required def register_absence(request: HttpRequest) -> HttpResponse: context = {} register_absence_form = RegisterAbsenceForm(request.POST or None) if request.method == 'POST': if register_absence_form.is_valid(): # 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'] starting_lesson = register_absence_form.cleaned_data['starting_lesson'] absent = register_absence_form.cleaned_data['absent'] excused = register_absence_form.cleaned_data['excused'] remarks = register_absence_form.cleaned_data['remarks'] # Mark person as absent delta = end_date - start_date for i in range(delta.days+1): starting_period = starting_lesson if i == 0 else 0 day = start_date + timedelta(days=i) person.mark_absent(day, starting_period, absent, excused, remarks) messages.success(request, _('The absence has been saved.')) return redirect('index') context['register_absence_form'] = register_absence_form return render(request, 'alsijil/register_absence.html', context)