diff --git a/biscuit/apps/chronos/forms.py b/biscuit/apps/chronos/forms.py new file mode 100644 index 0000000000000000000000000000000000000000..a242f060ae59e8b28bf0e064dc071f2a56f27fbc --- /dev/null +++ b/biscuit/apps/chronos/forms.py @@ -0,0 +1,19 @@ +from django import forms +from django.db.models import Count +from django.utils.translation import ugettext_lazy as _ + +from biscuit.core.models import Person, Group + +from .models import Room + + +class SelectForm(forms.Form): + group = forms.ModelChoiceField( + queryset=Group.objects.annotate(lessons_count=Count('lessons')).filter(lessons_count__gt=0), + label=_('Group'), required=False) + teacher = forms.ModelChoiceField( + queryset=Person.objects.annotate(lessons_count=Count('lessons')).filter(lessons_count__gt=0), + label=_('Teacher'), required=False) + room = forms.ModelChoiceField( + queryset=Room.objects.annotate(lessons_count=Count('lesson_periods')).filter(lessons_count__gt=0), + label=_('Room'), required=False) diff --git a/biscuit/apps/chronos/locale/de_DE/LC_MESSAGES/django.po b/biscuit/apps/chronos/locale/de_DE/LC_MESSAGES/django.po index e42cc09b5300778ff15cd1f4c11eab73bde1cb81..3f3ecfcc53100bc1c7f672ae8a701bbf2d760126 100644 --- a/biscuit/apps/chronos/locale/de_DE/LC_MESSAGES/django.po +++ b/biscuit/apps/chronos/locale/de_DE/LC_MESSAGES/django.po @@ -7,8 +7,8 @@ msgid "" msgstr "" "Project-Id-Version: \n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2019-08-18 21:36+0200\n" -"PO-Revision-Date: 2019-08-17 13:37+0200\n" +"POT-Creation-Date: 2019-08-24 13:55+0200\n" +"PO-Revision-Date: 2019-08-24 13:55+0200\n" "Last-Translator: Tom Teichler <tom.teichler@teckids.org>\n" "Language-Team: \n" "Language: de_DE\n" @@ -21,6 +21,18 @@ msgstr "" msgid "BiscuIT - Chronos (Timetables)" msgstr "BiscuIT - Chronos (Stundenpläne)" +#: forms.py:13 +msgid "Group" +msgstr "Gruppe" + +#: forms.py:16 +msgid "Teacher" +msgstr "Lehrer" + +#: forms.py:19 +msgid "Room" +msgstr "Raum" + #: menus.py:6 msgid "Timetables" msgstr "Stundenpläne" @@ -29,97 +41,86 @@ msgstr "Stundenpläne" msgid "Timetable" msgstr "Stundenplan" -#: models.py:8 +#: models.py:15 msgid "Sunday" msgstr "Sonntag" -#: models.py:9 +#: models.py:16 msgid "Monday" msgstr "Montag" -#: models.py:10 +#: models.py:17 msgid "Tuesday" msgstr "Dienstag" -#: models.py:11 +#: models.py:18 msgid "Wednesday" msgstr "Mittwoch" -#: models.py:12 +#: models.py:19 msgid "Thursday" msgstr "Donnerstag" -#: models.py:13 +#: models.py:20 msgid "Friday" msgstr "Freitag" -#: models.py:14 +#: models.py:21 msgid "Saturday" msgstr "Samstag" -#: models.py:18 +#: models.py:25 msgid "Week day" msgstr "Wochentag" -#: models.py:20 +#: models.py:27 msgid "Number of period" msgstr "Nummer der Stunde" -#: models.py:22 +#: models.py:29 msgid "Time the period starts" msgstr "Anfangszeit der Stunde" -#: models.py:23 +#: models.py:30 msgid "Time the period ends" msgstr "Endzeit der Stunde" -#: models.py:42 +#: models.py:49 msgid "Abbreviation of subject in timetable" msgstr "Kürzel des Faches im Stundenplan" -#: models.py:44 +#: models.py:51 msgid "Long name of subject" msgstr "Name des Faches" -#: models.py:46 +#: models.py:53 msgid "Foreground colour in timetable" msgstr "Vordergrundfarbe im Stundenplan" -#: models.py:48 +#: models.py:55 msgid "Background colour in timetable" msgstr "Hintergrundfarbe im Stundenplan" -#: models.py:57 +#: models.py:64 msgid "Short name, e.g. room number" msgstr "Kurzer Name, z.B. Raumnummer" -#: models.py:58 +#: models.py:65 msgid "Long name" msgstr "Langer Name" -#: models.py:73 +#: models.py:81 msgid "Effective start date of lesson" msgstr "Effektives Startdatum des Unterrichts" -#: models.py:75 +#: models.py:83 msgid "Effective end date of lesson" msgstr "Effektives Enddatum des Unterrichts" -#: templates/chronos/tt_week.html:24 +#: models.py:86 +msgid "Week" +msgstr "Kalenderwoche" + +#: templates/chronos/tt_week.html:28 msgid "Times" msgstr "Zeiten" - -#: views.py:24 -#, python-format -msgid "Group: %s" -msgstr "Gruppen: %s" - -#: views.py:29 -#, python-format -msgid "Teacher: %s" -msgstr "Lehrer: %s" - -#: views.py:34 -#, python-format -msgid "Room: %s" -msgstr "Raum: %s" diff --git a/biscuit/apps/chronos/menus.py b/biscuit/apps/chronos/menus.py index 2d81f31e4ab8531d607fe60c5bf3201fe9ce61cc..4ac8cff53502f8a79cf776065846e3c76ef3904c 100644 --- a/biscuit/apps/chronos/menus.py +++ b/biscuit/apps/chronos/menus.py @@ -6,11 +6,12 @@ MENUS = { 'name': _('Timetables'), 'url': '#', 'root': True, + 'validators': ['menu_generator.validators.is_authenticated'], 'submenu': [ { 'name': _('Timetable'), 'url': 'timetable', - 'validators': ['menu_generator.validators.is_authenticated', 'menu_generator.validators.is_superuser'] + 'validators': ['menu_generator.validators.is_authenticated'] } ] } diff --git a/biscuit/apps/chronos/models.py b/biscuit/apps/chronos/models.py index 9e116b0ed80f163ebc920e3613ca65307586a555..6da90f9f9d9787c1ae0707f11a06398ff2a2a83c 100644 --- a/biscuit/apps/chronos/models.py +++ b/biscuit/apps/chronos/models.py @@ -1,5 +1,5 @@ from datetime import datetime -from typing import List, Tuple, Optional +from typing import Dict, Optional, List, Tuple from django.core import validators from django.db import models @@ -29,7 +29,7 @@ class TimePeriod(models.Model): time_start = models.TimeField(verbose_name=_('Time the period starts')) time_end = models.TimeField(verbose_name=_('Time the period ends')) - def __str__(self): str: + def __str__(self) -> str: return '%s, %d. period (%s - %s)' % (self.weekday, self.period, self.time_start, self.time_end) @classmethod @@ -82,12 +82,26 @@ class Lesson(models.Model): date_end = models.DateField(verbose_name=_( 'Effective end date of lesson'), null=True) +class LessonSubstitution(models.Model): + week = models.IntegerField(verbose_name=_('Week'), + default=current_week) + + lesson_period = models.ForeignKey( + 'LessonPeriod', models.CASCADE, 'substitutions') + + subject = models.ForeignKey( + 'Subject', on_delete=models.CASCADE, + related_name='lesson_substitutions', null=True) + teachers = models.ManyToManyField('core.Person', + related_name='lesson_substitutions') + room = models.ForeignKey('Room', models.CASCADE, null=True) + class LessonPeriod(models.Model): lesson = models.ForeignKey('Lesson', models.CASCADE, related_name='lesson_periods') period = models.ForeignKey('TimePeriod', models.CASCADE, related_name='lesson_periods') - room = models.ForeignKey('Room', models.CASCADE, null=True) + room = models.ForeignKey('Room', models.CASCADE, null=True, related_name='lesson_periods') def get_substitution(self, week: Optional[int] = None) -> LessonSubstitution: wanted_week = week or current_week() @@ -113,18 +127,3 @@ class LessonPeriod(models.Model): def get_groups(self) -> models.query.QuerySet: return self.lesson.groups - - -class LessonSubstitution(models.Model): - week = models.IntegerField(verbose_name=_('Kalenderwoche'), - default=current_week) - - lesson_period = models.ForeignKey( - 'LessonPeriod', models.CASCADE, 'substitutions') - - subject = models.ForeignKey( - 'Subject', on_delete=models.CASCADE, - related_name='lesson_substitutions', null=True) - teachers = models.ManyToManyField('core.Person', - related_name='lesson_substitutions') - room = models.ForeignKey('Room', models.CASCADE, null=True) diff --git a/biscuit/apps/chronos/static/css/chronos/timetable.css b/biscuit/apps/chronos/static/css/chronos/timetable.css index 6b692ce69e65188b09442c7b11160ecbd93eb088..6d7a5af706b33e4bc81958ada4acd360f759a43c 100644 --- a/biscuit/apps/chronos/static/css/chronos/timetable.css +++ b/biscuit/apps/chronos/static/css/chronos/timetable.css @@ -1,3 +1,8 @@ .chronos-lesson { height: 6em; } + +ul#timetable_select_form li { + dispaly: inline; + +} \ No newline at end of file diff --git a/biscuit/apps/chronos/templates/chronos/tt_week.html b/biscuit/apps/chronos/templates/chronos/tt_week.html index 7293aa68541b51d414f45df68ae51d15dc86e750..d590d6fed85156901b966027ef771cbe21357f14 100644 --- a/biscuit/apps/chronos/templates/chronos/tt_week.html +++ b/biscuit/apps/chronos/templates/chronos/tt_week.html @@ -10,9 +10,13 @@ {% block page_title %}Timetable{% endblock %} {% block content %} - <p> - {{ filter_descs }} - </p> + <form method="get"> + {% csrf_token %} + <ul id="timetable_select_form"> + {{ select_form.as_ul }} + </ul> + <input type="submit" value="Select" /> + </form> <div class="row"> <div class="col-sm px-0"> diff --git a/biscuit/apps/chronos/templatetags/__init__.py b/biscuit/apps/chronos/templatetags/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/biscuit/apps/chronos/templatetags/week_helpers.py b/biscuit/apps/chronos/templatetags/week_helpers.py new file mode 100644 index 0000000000000000000000000000000000000000..a30af503f4df8a7e56e7970f2b0e3605357e5ef8 --- /dev/null +++ b/biscuit/apps/chronos/templatetags/week_helpers.py @@ -0,0 +1,18 @@ +from datetime import date + +from django import template + +from ..util import week_days + + +register = template.Library() + + +@register.filter +def week_start(week: int) -> date: + return week_days(week)[0] + + +@register.filter +def week_end(week: int) -> date: + return week_days(week)[-1] diff --git a/biscuit/apps/chronos/util.py b/biscuit/apps/chronos/util.py index 4fdff2de99f33da620743bfb7153908844209453..d315859e8eb0af9835142d3f272e9c9913f1c055 100644 --- a/biscuit/apps/chronos/util.py +++ b/biscuit/apps/chronos/util.py @@ -1,18 +1,30 @@ -from datetime import datetime -from typing import Optional +from datetime import date, datetime, timedelta +from typing import Optional, Sequence +from django.apps import apps from django.db import models -from .models import LessonPeriod - def current_week() -> int: return int(datetime.now().strftime('%V')) +def week_days(week: Optional[int]) -> Sequence[date]: + # FIXME Make this aware of the school term concept + # cf. BiscuIT-ng#40 + + year = date.today().year + wanted_week = week or current_week() + + first_day = datetime.strptime('%d-%d-1' % (year, wanted_week), '%Y-%W-%w') + + return [(first_day + timedelta(days=offset)).date() for offset in range(0, 7)] + + def current_lesson_periods(when: Optional[datetime] = None) -> models.query.QuerySet: now = when or datetime.now() + LessonPeriod = apps.get_model('chronos.LessonPeriod') return LessonPeriod.objects.filter(lesson__date_start__lte=now.date(), lesson__date_end__gte=now.date(), period__weekday=now.isoweekday(), diff --git a/biscuit/apps/chronos/views.py b/biscuit/apps/chronos/views.py index 9659dd63b078cb689e6e253a11217bcf06fcf96d..babc471ebb49163e82e496aeaaf967da4201356e 100644 --- a/biscuit/apps/chronos/views.py +++ b/biscuit/apps/chronos/views.py @@ -1,6 +1,7 @@ from collections import OrderedDict from django.contrib.auth.decorators import login_required +from django.db.models import Max, Min from django.http import HttpRequest, HttpResponse from django.shortcuts import redirect, render from django.urls import reverse @@ -9,6 +10,7 @@ from django.utils.translation import ugettext as _ from biscuit.core.decorators import admin_required from biscuit.core.models import Group, Person +from .forms import SelectForm from .models import LessonPeriod, TimePeriod, Room from .util import current_week @@ -19,51 +21,47 @@ def timetable(request: HttpRequest) -> HttpResponse: context = {} lesson_periods = LessonPeriod.objects.all() - filter_descs = [] - if 'group' in request.GET: - lesson_periods = lesson_periods.filter( - lesson__groups__pk=int(request.GET['group'])) - filter_descs.append(_('Group: %s') % Group.objects.get( - pk=int(request.GET['group']))) - elif 'teacher' in request.GET: - lesson_periods = lesson_periods.filter( - lesson__teachers__pk=int(request.GET['teacher'])) - filter_descs.append(_('Teacher: %s') % Person.objects.get( - pk=int(request.GET['teacher']))) - elif 'room' in request.GET: - lesson_periods = lesson_periods.filter( - room__pk=int(request.GET['room'])) - filter_descs.append(_('Room: %s') % Room.objects.get( - pk=int(request.GET['room']))) + if request.GET.get('group', None) or request.GET.get('teacher', None) or request.GET.get('room', None): + # Incrementally filter lesson periods by GET parameters + if 'group' in request.GET and request.GET['group']: + lesson_periods = lesson_periods.filter( + lesson__groups__pk=int(request.GET['group'])) + if 'teacher' in request.GET and request.GET['teacher']: + lesson_periods = lesson_periods.filter( + lesson__teachers__pk=int(request.GET['teacher'])) + if 'room' in request.GET and request.GET['room']: + lesson_periods = lesson_periods.filter( + room__pk=int(request.GET['room'])) else: + # Redirect to a selected view if no filter provided if request.user.person: if request.user.person.primary_group: return redirect(reverse('timetable') + '?group=%d' % request.user.person.primary_group.pk) elif lesson_periods.filter(lesson__teachers=request.user.person).exists(): return redirect(reverse('timetable') + '?teacher=%d' % request.user.person.pk) + # Regroup lesson periods per weekday per_day = {} - period_min, period_max = None, None for lesson_period in lesson_periods: per_day.setdefault(lesson_period.period.weekday, {})[lesson_period.period.period] = lesson_period - # Expand min and max lesson to later fill in empty lessons - if period_min is None or period_min > lesson_period.period.period: - period_min = lesson_period.period.period - if period_max is None or period_max < lesson_period.period.period: - period_max = lesson_period.period.period + # Determine overall first and last day and period + min_max = TimePeriod.objects.aggregate( + Min('period'), Max('period'), + Min('weekday'), Max('weekday')) # Fill in empty lessons - for weekday_num in range(min(per_day.keys() or [0]), - max(per_day.keys() or [6]) + 1): + for weekday_num in range(min_max.get('weekday__min', 0), + min_max.get('weekday__max', 6) + 1): # Fill in empty weekdays if weekday_num not in per_day.keys(): per_day[weekday_num] = {} # Fill in empty lessons on this workday - for period_num in range(period_min, period_max + 1): + for period_num in range(min_max.get('period__min', 1), + min_max.get('period__max', 7) + 1): if period_num not in per_day[weekday_num].keys(): per_day[weekday_num][period_num] = None @@ -71,10 +69,13 @@ def timetable(request: HttpRequest) -> HttpResponse: per_day[weekday_num] = OrderedDict( sorted(per_day[weekday_num].items())) + # Add a form to filter the view + select_form = SelectForm(request.GET or None) + context['lesson_periods'] = OrderedDict(sorted(per_day.items())) - context['filter_descs'] = ', '.join(filter_descs) context['periods'] = TimePeriod.get_times_dict() context['weekdays'] = dict(TimePeriod.WEEKDAY_CHOICES) context['current_week'] = current_week() + context['select_form'] = select_form return render(request, 'chronos/tt_week.html', context)