From eeb6b95c9d2e82f2a5de74c660f8bc702fbbe5ca Mon Sep 17 00:00:00 2001 From: Dominik George <dominik.george@teckids.org> Date: Sat, 14 Sep 2019 13:47:12 +0200 Subject: [PATCH] Implement ClaendarWeek data class for correct week calculations. Advances #27. We still need to do stuff to respect the year as well, but this is tied to BiscuIT-ng#40. --- .../templates/chronos/edit_substitution.html | 1 + biscuit/apps/chronos/util.py | 71 +++++++++++++++---- biscuit/apps/chronos/views.py | 38 ++++++---- 3 files changed, 82 insertions(+), 28 deletions(-) diff --git a/biscuit/apps/chronos/templates/chronos/edit_substitution.html b/biscuit/apps/chronos/templates/chronos/edit_substitution.html index 054b9117..c205e848 100644 --- a/biscuit/apps/chronos/templates/chronos/edit_substitution.html +++ b/biscuit/apps/chronos/templates/chronos/edit_substitution.html @@ -11,6 +11,7 @@ <div class="d-flex justify-content-between"> <div class="btn-group" role="group" aria-label="Day actions"> {% if substitution %} + {# FIXME Respect year as well #} <a href="{% url 'delete_substitution' substitution.lesson_period.id substitution.week %}" class="btn btn-danger"> {% fa 'trash-o' %} </a> diff --git a/biscuit/apps/chronos/util.py b/biscuit/apps/chronos/util.py index 9781ac55..cfc5f8d1 100644 --- a/biscuit/apps/chronos/util.py +++ b/biscuit/apps/chronos/util.py @@ -1,24 +1,67 @@ +from __future__ import annotations + +from dataclasses import dataclass from datetime import date, datetime, timedelta -from typing import Optional, Sequence +from typing import Optional, Sequence, Tuple from django.apps import apps from django.db import models -def current_week() -> int: - return int(datetime.now().strftime('%V')) +@dataclass +class CalendarWeek: + """ A calendar week defined by year and ISO week number. """ + + year: Optional[int] = None + week: Optional[int] = None + + @classmethod + def from_date(cls, when: date): + return cls(year=when.strftime('%Y'), week=when.strftime('%V')) + + def __post_init__(self) -> None: + today = date.today() + + if not self.year: + self.year = today.year + if not self.week: + self.week = today.isoweekday() + + def __len__(self) -> int: + return 7 + + def __getitem__(self, n: int) -> date: + if n < -7 or n > 6: + raise IndexError('Week day %d is out of range.' % n) + + if n < 0: + n += 7 + + return datetime.strptime('%d-%d-%d' % (self.year, self.week, n + 1), '%G-%V-%u').date() + + def __contains__(self, day: date) -> bool: + return self.__class__.form_date(day) == self + + def __eq__(self, other: CalendarWeek) -> bool: + return self.year == other.year and self.week == other.week + + def __lt__(self, other: CalendarWeek) -> bool: + return self[0] < other[0] + def __gt__(self, other: CalendarWeek) -> bool: + return self[0] > other[0] -def week_days(week: Optional[int]) -> Sequence[date]: - # FIXME Make this aware of the school term concept - # cf. BiscuIT-ng#40 + def __le__(self, other: CalendarWeek) -> bool: + return self[0] <= other[0] - year = date.today().year - wanted_week = week or current_week() + def __gr__(self, other: CalendarWeek) -> bool: + return self[0] >= other[0] - first_day = datetime.strptime('%d-%d-1' % (year, wanted_week), '%G-%V-%u') + def __add__(self, weeks: int) -> CalendarWeek: + return self.__class__.from_date(self[0] + timedelta(days=weeks * 7)) - return [(first_day + timedelta(days=offset)).date() for offset in range(0, 7)] + def __sub__(self, weeks: int) -> CalendarWeek: + return self.__class__.from_date(self[0] - timedelta(days=weeks * 7)) def current_lesson_periods(when: Optional[datetime] = None) -> models.query.QuerySet: @@ -32,9 +75,9 @@ def current_lesson_periods(when: Optional[datetime] = None) -> models.query.Quer period__time_end__gte=now.time()) -def week_weekday_from_date(when: date) -> Sequence[int]: - return (int(when.strftime('%V')), int(when.strftime('%u'))) +def week_weekday_from_date(when: date) -> Tuple[CalendarWeek, int]: + return (CalendarWeek.from_date(when), when.isoweekday()) -def week_weekday_to_date(week: int, weekday: int) -> date: - return week_days(week)[weekday-1] +def week_weekday_to_date(week: CalendarWeek, weekday: int) -> date: + return week[weekday - 1] diff --git a/biscuit/apps/chronos/views.py b/biscuit/apps/chronos/views.py index 03c639a3..67939a88 100644 --- a/biscuit/apps/chronos/views.py +++ b/biscuit/apps/chronos/views.py @@ -17,7 +17,7 @@ from biscuit.core.util import messages from .forms import SelectForm, LessonSubstitutionForm from .models import LessonPeriod, TimePeriod, LessonSubstitution -from .util import current_week, week_weekday_from_date, week_days +from .util import CalendarWeek, week_weekday_from_date from .tables import LessonsTable @@ -25,17 +25,20 @@ from .tables import LessonsTable def timetable(request: HttpRequest, week: Optional[int] = None) -> HttpResponse: context = {} - wanted_week = week or current_week() + if week: + wanted_week = CalendarWeek(week) # FIXME Respect year as well + else: + wanted_week = CalendarWeek() lesson_periods = LessonPeriod.objects.filter( - lesson__date_start__lte=week_days(wanted_week)[0], - lesson__date_end__gte=week_days(wanted_week)[-1] + lesson__date_start__lte=wanted_week[0], + lesson__date_end__gte=wanted_week[-1] ).select_related( 'lesson', 'lesson__subject', 'period', 'room' ).prefetch_related( 'lesson__groups', 'lesson__teachers', 'substitutions' ).extra( - select={'_week': wanted_week} + select={'_week': wanted_week.week} # FIXME respect year as well ) if request.GET.get('group', None) or request.GET.get('teacher', None) or request.GET.get('room', None): @@ -45,7 +48,7 @@ def timetable(request: HttpRequest, week: Optional[int] = None) -> HttpResponse: Q(lesson__groups__pk=int(request.GET['group'])) | Q(lesson__groups__parent_groups__pk=int(request.GET['group']))) if 'teacher' in request.GET and request.GET['teacher']: lesson_periods = lesson_periods.filter( - Q(substitutions__teachers__pk=int(request.GET['teacher']), substitutions__week=wanted_week) | Q(lesson__teachers__pk=int(request.GET['teacher']))) + Q(substitutions__teachers__pk=int(request.GET['teacher']), substitutions__week=wanted_week.week) | Q(lesson__teachers__pk=int(request.GET['teacher']))) # FIXME Respect year as well if 'room' in request.GET and request.GET['room']: lesson_periods = lesson_periods.filter( room__pk=int(request.GET['room'])) @@ -93,10 +96,13 @@ def timetable(request: HttpRequest, week: Optional[int] = None) -> HttpResponse: context['periods'] = TimePeriod.get_times_dict() context['weekdays'] = dict(TimePeriod.WEEKDAY_CHOICES) context['week'] = wanted_week - context['url_prev'] = '%s?%s' % (reverse('timetable_by_week', week=wanted_week - 1), request.GET.urlencode()) - context['url_next'] = '%s?%s' % (reverse('timetable_by_week', week=wanted_week + 1), request.GET.urlencode()) context['select_form'] = select_form + week_prev = wanted_week - 1 + week_next = wanted_week + 1 + context['url_prev'] = '%s?%s' % (reverse('timetable_by_week', year=week_prev.year, week=week_prev.week), request.GET.urlencode()) + context['url_next'] = '%s?%s' % (reverse('timetable_by_week', year=week_next.year, week=week_next.week), request.GET.urlencode()) + return render(request, 'chronos/tt_week.html', context) @@ -118,7 +124,7 @@ def lessons_day(request: HttpRequest, when: Optional[str] = None) -> HttpRespons ) # Build table - lessons_table = LessonsTable(lesson_periods.extra(select={'_week': week}).all()) + lessons_table = LessonsTable(lesson_periods.extra(select={'_week': week.week}).all()) # FIXME Respect year as well RequestConfig(request).configure(lessons_table) context['current_head'] = _('Lessons') @@ -136,16 +142,18 @@ def lessons_day(request: HttpRequest, when: Optional[str] = None) -> HttpRespons def edit_substitution(request: HttpRequest, id_: int, week: int) -> HttpResponse: context = {} + wanted_week = CalendarWeek(week=week) # FIXME Respect year as well + lesson_period = get_object_or_404(LessonPeriod, pk=id_) lesson_substitution = LessonSubstitution.objects.filter( - week=week, lesson_period=lesson_period).first() + week=wanted_week.week, lesson_period=lesson_period).first() # FIXME Respect year as well if lesson_substitution: edit_substitution_form = LessonSubstitutionForm( request.POST or None, instance=lesson_substitution) else: edit_substitution_form = LessonSubstitutionForm( - request.POST or None, initial={'week': week, 'lesson_period': lesson_period}) + request.POST or None, initial={'week': wanted_week.week, 'lesson_period': lesson_period}) # FIXME Respect year as well context['substitution'] = lesson_substitution @@ -154,7 +162,7 @@ def edit_substitution(request: HttpRequest, id_: int, week: int) -> HttpResponse edit_substitution_form.save(commit=True) messages.success(request, _('The substitution has been saved.')) - return redirect('lessons_day_by_date', when=week_days(week)[lesson_period.period.weekday - 1].strftime('%Y-%m-%d')) + return redirect('lessons_day_by_date', when=wanted_week[lesson_period.period.weekday - 1].strftime('%Y-%m-%d')) context['edit_substitution_form'] = edit_substitution_form @@ -165,9 +173,11 @@ def edit_substitution(request: HttpRequest, id_: int, week: int) -> HttpResponse def delete_substitution(request: HttpRequest, id_: int, week: int) -> HttpResponse: context = {} + wanted_week = CalendarWeek(week=week) # FIXME Respect year as well + LessonSubstitution.objects.filter( - week=week, lesson_period__id=id_ + week=wanted_week.week, lesson_period__id=id_ # FIXME Respect year as well ).delete() messages.success(request, _('The substitution has been deleted.')) - return redirect('lessons_day_by_date', when=week_days(week)[lesson_period.period.weekday - 1].strftime('%Y-%m-%d')) + return redirect('lessons_day_by_date', when=wanted_week[lesson_period.period.weekday - 1].strftime('%Y-%m-%d')) -- GitLab