Skip to content
Snippets Groups Projects

Compare revisions

Changes are shown as if the source revision was being merged into the target revision. Learn more about comparing revisions.

Source

Select target project
No results found

Target

Select target project
  • AlekSIS/official/AlekSIS-App-Chronos
  • sunweaver/AlekSIS-App-Chronos
  • sggua/AlekSIS-App-Chronos
  • tincmeKdenka/AlekSIS-App-Chronos
  • ligquamacti/AlekSIS-App-Chronos
  • 1crotatilhe/AlekSIS-App-Chronos
  • 1compluningi/AlekSIS-App-Chronos
  • starwardcarfi/AlekSIS-App-Chronos
  • ceohecholeg/AlekSIS-App-Chronos
  • 7quecontranchi/AlekSIS-App-Chronos
  • 8evsubcesza/AlekSIS-App-Chronos
  • unscinKibdzu/AlekSIS-App-Chronos
  • delucPchondmu/AlekSIS-App-Chronos
13 results
Show changes
Commits on Source (1)
Showing
with 680 additions and 311 deletions
import pkg_resources import pkg_resources
try: try:
__version__ = pkg_resources.get_distribution('BiscuIT-App-Chronos').version __version__ = pkg_resources.get_distribution("BiscuIT-App-Chronos").version
except Exception: except Exception:
__version__ = 'unknown' __version__ = "unknown"
default_app_config = 'biscuit.apps.chronos.apps.ChronosConfig' default_app_config = "biscuit.apps.chronos.apps.ChronosConfig"
...@@ -2,5 +2,5 @@ from biscuit.core.util.apps import AppConfig ...@@ -2,5 +2,5 @@ from biscuit.core.util.apps import AppConfig
class ChronosConfig(AppConfig): class ChronosConfig(AppConfig):
name = 'biscuit.apps.chronos' name = "biscuit.apps.chronos"
verbose_name = 'BiscuIT - Chronos (Timetables)' verbose_name = "BiscuIT - Chronos (Timetables)"
from django import forms from django import forms
from django.db.models import Count from django.db.models import Count
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from django_select2.forms import ModelSelect2MultipleWidget, Select2Widget from django_select2.forms import ModelSelect2MultipleWidget, Select2Widget
from biscuit.core.models import Person, Group from biscuit.core.models import Group, Person
from .models import Room, LessonSubstitution, Subject, LessonPeriod from .models import LessonPeriod, LessonSubstitution, Room, Subject
class SelectForm(forms.Form): class SelectForm(forms.Form):
group = forms.ModelChoiceField( group = forms.ModelChoiceField(
queryset=Group.objects.annotate(lessons_count=Count('lessons')).filter(lessons_count__gt=0), queryset=Group.objects.annotate(lessons_count=Count("lessons")).filter(
label=_('Group'), required=False, widget=Select2Widget) lessons_count__gt=0
),
label=_("Group"),
required=False,
widget=Select2Widget,
)
teacher = forms.ModelChoiceField( teacher = forms.ModelChoiceField(
queryset=Person.objects.annotate(lessons_count=Count( queryset=Person.objects.annotate(
'lessons_as_teacher')).filter(lessons_count__gt=0), lessons_count=Count("lessons_as_teacher")
label=_('Teacher'), required=False, widget=Select2Widget) ).filter(lessons_count__gt=0),
label=_("Teacher"),
required=False,
widget=Select2Widget,
)
room = forms.ModelChoiceField( room = forms.ModelChoiceField(
queryset=Room.objects.annotate(lessons_count=Count( queryset=Room.objects.annotate(lessons_count=Count("lesson_periods")).filter(
'lesson_periods')).filter(lessons_count__gt=0), lessons_count__gt=0
label=_('Room'), required=False, widget=Select2Widget) ),
label=_("Room"),
required=False,
widget=Select2Widget,
)
class LessonSubstitutionForm(forms.ModelForm): class LessonSubstitutionForm(forms.ModelForm):
class Meta: class Meta:
model = LessonSubstitution model = LessonSubstitution
fields = ['week', 'lesson_period', 'subject', 'teachers', 'room', 'cancelled'] fields = ["week", "lesson_period", "subject", "teachers", "room", "cancelled"]
widgets = { widgets = {
'teachers': ModelSelect2MultipleWidget(search_fields=['first_name__icontains', 'last_name__icontains', 'short_name__icontains']) "teachers": ModelSelect2MultipleWidget(
search_fields=[
"first_name__icontains",
"last_name__icontains",
"short_name__icontains",
]
)
} }
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
MENUS = { MENUS = {
'NAV_MENU_CORE': [ "NAV_MENU_CORE": [
{ {
'name': _('Timetables'), "name": _("Timetables"),
'url': '#', "url": "#",
'root': True, "root": True,
'validators': ['menu_generator.validators.is_authenticated', 'biscuit.core.util.core_helpers.has_person'], "validators": [
'submenu': [ "menu_generator.validators.is_authenticated",
"biscuit.core.util.core_helpers.has_person",
],
"submenu": [
{ {
'name': _('Timetable'), "name": _("Timetable"),
'url': 'timetable', "url": "timetable",
'validators': ['menu_generator.validators.is_authenticated'] "validators": ["menu_generator.validators.is_authenticated"],
}, },
{ {
'name': _('Daily lessons'), "name": _("Daily lessons"),
'url': 'lessons_day', "url": "lessons_day",
'validators': ['menu_generator.validators.is_authenticated'] "validators": ["menu_generator.validators.is_authenticated"],
}, },
{ {
'name': _('Substitutions'), "name": _("Substitutions"),
'url': 'substitutions', "url": "substitutions",
'validators': ['menu_generator.validators.is_authenticated'] "validators": ["menu_generator.validators.is_authenticated"],
} },
] ],
} }
] ]
} }
# Generated by Django 2.2.5 on 2019-09-03 18:30 # Generated by Django 2.2.5 on 2019-09-03 18:30
import biscuit.apps.chronos.util
import biscuit.core.util.core_helpers
import django.core.validators import django.core.validators
from django.db import migrations, models
import django.db.models.deletion import django.db.models.deletion
from django.db import migrations, models
import biscuit.apps.chronos.util
import biscuit.core.util.core_helpers
class Migration(migrations.Migration): class Migration(migrations.Migration):
...@@ -12,123 +13,335 @@ class Migration(migrations.Migration): ...@@ -12,123 +13,335 @@ class Migration(migrations.Migration):
initial = True initial = True
dependencies = [ dependencies = [
('core', '0001_initial'), ("core", "0001_initial"),
] ]
operations = [ operations = [
migrations.CreateModel( migrations.CreateModel(
name='Lesson', name="Lesson",
fields=[ fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), (
('date_start', models.DateField(null=True, verbose_name='Effective start date of lesson')), "id",
('date_end', models.DateField(null=True, verbose_name='Effective end date of lesson')), models.AutoField(
('groups', models.ManyToManyField(related_name='lessons', to='core.Group')), auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
(
"date_start",
models.DateField(
null=True, verbose_name="Effective start date of lesson"
),
),
(
"date_end",
models.DateField(
null=True, verbose_name="Effective end date of lesson"
),
),
(
"groups",
models.ManyToManyField(related_name="lessons", to="core.Group"),
),
], ],
options={ options={"ordering": ["date_start"],},
'ordering': ['date_start'],
},
), ),
migrations.CreateModel( migrations.CreateModel(
name='LessonPeriod', name="LessonPeriod",
fields=[ fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), (
('lesson', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='lesson_periods', to='chronos.Lesson')), "id",
models.AutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
(
"lesson",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name="lesson_periods",
to="chronos.Lesson",
),
),
], ],
options={ options={
'ordering': ['lesson__date_start', 'period__weekday', 'period__period'], "ordering": ["lesson__date_start", "period__weekday", "period__period"],
}, },
), ),
migrations.CreateModel( migrations.CreateModel(
name='TimePeriod', name="TimePeriod",
fields=[ fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), (
('weekday', models.PositiveSmallIntegerField(choices=[(0, 'Sunday'), (1, 'Monday'), (2, 'Tuesday'), (3, 'Wednesday'), (4, 'Thursday'), (5, 'Friday'), (6, 'Saturday')], verbose_name='Week day')), "id",
('period', models.PositiveSmallIntegerField(verbose_name='Number of period')), models.AutoField(
('time_start', models.TimeField(verbose_name='Time the period starts')), auto_created=True,
('time_end', models.TimeField(verbose_name='Time the period ends')), primary_key=True,
('school', models.ForeignKey(default=1, on_delete=django.db.models.deletion.CASCADE, to='core.School')), serialize=False,
verbose_name="ID",
),
),
(
"weekday",
models.PositiveSmallIntegerField(
choices=[
(0, "Sunday"),
(1, "Monday"),
(2, "Tuesday"),
(3, "Wednesday"),
(4, "Thursday"),
(5, "Friday"),
(6, "Saturday"),
],
verbose_name="Week day",
),
),
(
"period",
models.PositiveSmallIntegerField(verbose_name="Number of period"),
),
("time_start", models.TimeField(verbose_name="Time the period starts")),
("time_end", models.TimeField(verbose_name="Time the period ends")),
(
"school",
models.ForeignKey(
default=1,
on_delete=django.db.models.deletion.CASCADE,
to="core.School",
),
),
], ],
options={ options={
'ordering': ['weekday', 'period'], "ordering": ["weekday", "period"],
'unique_together': {('school', 'weekday', 'period')}, "unique_together": {("school", "weekday", "period")},
}, },
), ),
migrations.CreateModel( migrations.CreateModel(
name='Subject', name="Subject",
fields=[ fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), (
('abbrev', models.CharField(max_length=10, verbose_name='Abbreviation of subject in timetable')), "id",
('name', models.CharField(max_length=30, verbose_name='Long name of subject')), models.AutoField(
('colour_fg', models.CharField(blank=True, max_length=7, validators=[django.core.validators.RegexValidator('#[0-9A-F]{6}')], verbose_name='Foreground colour in timetable')), auto_created=True,
('colour_bg', models.CharField(blank=True, max_length=7, validators=[django.core.validators.RegexValidator('#[0-9A-F]{6}')], verbose_name='Background colour in timetable')), primary_key=True,
('school', models.ForeignKey(default=1, on_delete=django.db.models.deletion.CASCADE, to='core.School')), serialize=False,
verbose_name="ID",
),
),
(
"abbrev",
models.CharField(
max_length=10,
verbose_name="Abbreviation of subject in timetable",
),
),
(
"name",
models.CharField(
max_length=30, verbose_name="Long name of subject"
),
),
(
"colour_fg",
models.CharField(
blank=True,
max_length=7,
validators=[
django.core.validators.RegexValidator("#[0-9A-F]{6}")
],
verbose_name="Foreground colour in timetable",
),
),
(
"colour_bg",
models.CharField(
blank=True,
max_length=7,
validators=[
django.core.validators.RegexValidator("#[0-9A-F]{6}")
],
verbose_name="Background colour in timetable",
),
),
(
"school",
models.ForeignKey(
default=1,
on_delete=django.db.models.deletion.CASCADE,
to="core.School",
),
),
], ],
options={ options={
'ordering': ['name', 'abbrev'], "ordering": ["name", "abbrev"],
'unique_together': {('school', 'abbrev'), ('school', 'name')}, "unique_together": {("school", "abbrev"), ("school", "name")},
}, },
), ),
migrations.CreateModel( migrations.CreateModel(
name='Room', name="Room",
fields=[ fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), (
('short_name', models.CharField(max_length=10, verbose_name='Short name, e.g. room number')), "id",
('name', models.CharField(max_length=30, verbose_name='Long name')), models.AutoField(
('school', models.ForeignKey(default=1, on_delete=django.db.models.deletion.CASCADE, to='core.School')), auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
(
"short_name",
models.CharField(
max_length=10, verbose_name="Short name, e.g. room number"
),
),
("name", models.CharField(max_length=30, verbose_name="Long name")),
(
"school",
models.ForeignKey(
default=1,
on_delete=django.db.models.deletion.CASCADE,
to="core.School",
),
),
], ],
options={ options={
'ordering': ['name', 'short_name'], "ordering": ["name", "short_name"],
'unique_together': {('school', 'name'), ('school', 'short_name')}, "unique_together": {("school", "name"), ("school", "short_name")},
}, },
), ),
migrations.CreateModel( migrations.CreateModel(
name='LessonSubstitution', name="LessonSubstitution",
fields=[ fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), (
('week', models.IntegerField(default=biscuit.apps.chronos.util.CalendarWeek.current_week, verbose_name='Week')), "id",
('lesson_period', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='substitutions', to='chronos.LessonPeriod')), models.AutoField(
('room', models.ForeignKey(null=True, blank=True, on_delete=django.db.models.deletion.CASCADE, to='chronos.Room', verbose_name='Room')), auto_created=True,
('school', models.ForeignKey(default=1, on_delete=django.db.models.deletion.CASCADE, to='core.School')), primary_key=True,
('subject', models.ForeignKey(null=True, blank=True, on_delete=django.db.models.deletion.CASCADE, related_name='lesson_substitutions', to='chronos.Subject', verbose_name='Subject')), serialize=False,
('teachers', models.ManyToManyField(blank=True, related_name='lesson_substitutions', to='core.Person')), verbose_name="ID",
),
),
(
"week",
models.IntegerField(
default=biscuit.apps.chronos.util.CalendarWeek.current_week,
verbose_name="Week",
),
),
(
"lesson_period",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name="substitutions",
to="chronos.LessonPeriod",
),
),
(
"room",
models.ForeignKey(
null=True,
blank=True,
on_delete=django.db.models.deletion.CASCADE,
to="chronos.Room",
verbose_name="Room",
),
),
(
"school",
models.ForeignKey(
default=1,
on_delete=django.db.models.deletion.CASCADE,
to="core.School",
),
),
(
"subject",
models.ForeignKey(
null=True,
blank=True,
on_delete=django.db.models.deletion.CASCADE,
related_name="lesson_substitutions",
to="chronos.Subject",
verbose_name="Subject",
),
),
(
"teachers",
models.ManyToManyField(
blank=True,
related_name="lesson_substitutions",
to="core.Person",
),
),
], ],
options={ options={
'ordering': ['lesson_period__lesson__date_start', 'week', 'lesson_period__period__weekday', 'lesson_period__period__period'], "ordering": [
"lesson_period__lesson__date_start",
"week",
"lesson_period__period__weekday",
"lesson_period__period__period",
],
}, },
), ),
migrations.AddField( migrations.AddField(
model_name='lessonperiod', model_name="lessonperiod",
name='period', name="period",
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='lesson_periods', to='chronos.TimePeriod'), field=models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name="lesson_periods",
to="chronos.TimePeriod",
),
), ),
migrations.AddField( migrations.AddField(
model_name='lessonperiod', model_name="lessonperiod",
name='room', name="room",
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, related_name='lesson_periods', to='chronos.Room'), field=models.ForeignKey(
null=True,
on_delete=django.db.models.deletion.CASCADE,
related_name="lesson_periods",
to="chronos.Room",
),
), ),
migrations.AddField( migrations.AddField(
model_name='lessonperiod', model_name="lessonperiod",
name='school', name="school",
field=models.ForeignKey(default=1, on_delete=django.db.models.deletion.CASCADE, to='core.School'), field=models.ForeignKey(
default=1, on_delete=django.db.models.deletion.CASCADE, to="core.School"
),
), ),
migrations.AddField( migrations.AddField(
model_name='lesson', model_name="lesson",
name='periods', name="periods",
field=models.ManyToManyField(related_name='lessons', through='chronos.LessonPeriod', to='chronos.TimePeriod'), field=models.ManyToManyField(
related_name="lessons",
through="chronos.LessonPeriod",
to="chronos.TimePeriod",
),
), ),
migrations.AddField( migrations.AddField(
model_name='lesson', model_name="lesson",
name='school', name="school",
field=models.ForeignKey(default=1, on_delete=django.db.models.deletion.CASCADE, to='core.School'), field=models.ForeignKey(
default=1, on_delete=django.db.models.deletion.CASCADE, to="core.School"
),
), ),
migrations.AddField( migrations.AddField(
model_name='lesson', model_name="lesson",
name='subject', name="subject",
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='lessons', to='chronos.Subject'), field=models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name="lessons",
to="chronos.Subject",
),
), ),
migrations.AddField( migrations.AddField(
model_name='lesson', model_name="lesson",
name='teachers', name="teachers",
field=models.ManyToManyField(related_name='lessons', to='core.Person'), field=models.ManyToManyField(related_name="lessons", to="core.Person"),
), ),
] ]
...@@ -6,25 +6,31 @@ from django.db import migrations, models ...@@ -6,25 +6,31 @@ from django.db import migrations, models
class Migration(migrations.Migration): class Migration(migrations.Migration):
dependencies = [ dependencies = [
('core', '0001_initial'), ("core", "0001_initial"),
('chronos', '0001_initial'), ("chronos", "0001_initial"),
] ]
operations = [ operations = [
migrations.AlterUniqueTogether( migrations.AlterUniqueTogether(
name='lessonsubstitution', name="lessonsubstitution",
unique_together={('school', 'lesson_period', 'week')}, unique_together={("school", "lesson_period", "week")},
), ),
migrations.AddIndex( migrations.AddIndex(
model_name='lesson', model_name="lesson",
index=models.Index(fields=['date_start', 'date_end'], name='chronos_les_date_st_5ecc62_idx'), index=models.Index(
fields=["date_start", "date_end"], name="chronos_les_date_st_5ecc62_idx"
),
), ),
migrations.AddIndex( migrations.AddIndex(
model_name='lessonperiod', model_name="lessonperiod",
index=models.Index(fields=['lesson', 'period'], name='chronos_les_lesson__05250e_idx'), index=models.Index(
fields=["lesson", "period"], name="chronos_les_lesson__05250e_idx"
),
), ),
migrations.AddIndex( migrations.AddIndex(
model_name='timeperiod', model_name="timeperiod",
index=models.Index(fields=['time_start', 'time_end'], name='chronos_tim_time_st_491e4c_idx'), index=models.Index(
fields=["time_start", "time_end"], name="chronos_tim_time_st_491e4c_idx"
),
), ),
] ]
...@@ -6,17 +6,22 @@ from django.db import migrations, models ...@@ -6,17 +6,22 @@ from django.db import migrations, models
class Migration(migrations.Migration): class Migration(migrations.Migration):
dependencies = [ dependencies = [
('chronos', '0002_db_indexes'), ("chronos", "0002_db_indexes"),
] ]
operations = [ operations = [
migrations.AddField( migrations.AddField(
model_name='lessonsubstitution', model_name="lessonsubstitution",
name='cancelled', name="cancelled",
field=models.BooleanField(default=False), field=models.BooleanField(default=False),
), ),
migrations.AddConstraint( migrations.AddConstraint(
model_name='lessonsubstitution', model_name="lessonsubstitution",
constraint=models.CheckConstraint(check=models.Q(('cancelled', True), ('subject__isnull', False), _negated=True), name='either_substituted_or_cancelled'), constraint=models.CheckConstraint(
check=models.Q(
("cancelled", True), ("subject__isnull", False), _negated=True
),
name="either_substituted_or_cancelled",
),
), ),
] ]
...@@ -6,12 +6,11 @@ from django.db import migrations ...@@ -6,12 +6,11 @@ from django.db import migrations
class Migration(migrations.Migration): class Migration(migrations.Migration):
dependencies = [ dependencies = [
('chronos', '0003_substitution_cancelled_or_subject'), ("chronos", "0003_substitution_cancelled_or_subject"),
] ]
operations = [ operations = [
migrations.AlterUniqueTogether( migrations.AlterUniqueTogether(
name='room', name="room", unique_together={("school", "short_name")},
unique_together={('school', 'short_name')},
), ),
] ]
...@@ -22,10 +22,11 @@ class LessonPeriodManager(models.Manager): ...@@ -22,10 +22,11 @@ class LessonPeriodManager(models.Manager):
def get_queryset(self): def get_queryset(self):
""" Ensures all related lesson data is loaded as well. """ """ Ensures all related lesson data is loaded as well. """
return super().get_queryset().select_related( return (
'lesson', 'lesson__subject', 'period', 'room' super()
).prefetch_related( .get_queryset()
'lesson__groups', 'lesson__teachers', 'substitutions' .select_related("lesson", "lesson__subject", "period", "room")
.prefetch_related("lesson__groups", "lesson__teachers", "substitutions")
) )
...@@ -41,8 +42,8 @@ class LessonPeriodQuerySet(models.QuerySet): ...@@ -41,8 +42,8 @@ class LessonPeriodQuerySet(models.QuerySet):
""" Filter for all lessons within a calendar week. """ """ Filter for all lessons within a calendar week. """
return self.within_dates( return self.within_dates(
wanted_week[0] + timedelta(days=1) * (models.F('period__weekday') - 1), wanted_week[0] + timedelta(days=1) * (models.F("period__weekday") - 1),
wanted_week[0] + timedelta(days=1) * (models.F('period__weekday') - 1) wanted_week[0] + timedelta(days=1) * (models.F("period__weekday") - 1),
).annotate_week(wanted_week) ).annotate_week(wanted_week)
def on_day(self, day: date): def on_day(self, day: date):
...@@ -50,9 +51,11 @@ class LessonPeriodQuerySet(models.QuerySet): ...@@ -50,9 +51,11 @@ class LessonPeriodQuerySet(models.QuerySet):
week, weekday = week_weekday_from_date(day) week, weekday = week_weekday_from_date(day)
return self.within_dates(day, day).filter( return (
period__weekday=weekday self.within_dates(day, day)
).annotate_week(week) .filter(period__weekday=weekday)
.annotate_week(week)
)
def at_time(self, when: Optional[datetime] = None): def at_time(self, when: Optional[datetime] = None):
""" Filter for the lessons taking place at a certain point in time. """ """ Filter for the lessons taking place at a certain point in time. """
...@@ -65,32 +68,39 @@ class LessonPeriodQuerySet(models.QuerySet): ...@@ -65,32 +68,39 @@ class LessonPeriodQuerySet(models.QuerySet):
lesson__date_end__gte=now.date(), lesson__date_end__gte=now.date(),
period__weekday=now.isoweekday(), period__weekday=now.isoweekday(),
period__time_start__lte=now.time(), period__time_start__lte=now.time(),
period__time_end__gte=now.time() period__time_end__gte=now.time(),
).annotate_week(week) ).annotate_week(week)
def filter_participant(self, person: Union[Person, int]): def filter_participant(self, person: Union[Person, int]):
""" Filter for all lessons a participant (student) attends. """ """ Filter for all lessons a participant (student) attends. """
return self.filter( return self.filter(
Q(lesson__groups__members=person) | Q(lesson__groups__parent_groups__members=person)) Q(lesson__groups__members=person)
| Q(lesson__groups__parent_groups__members=person)
)
def filter_group(self, group: Union[Group, int]): def filter_group(self, group: Union[Group, int]):
""" Filter for all lessons a group (class) regularly attends. """ """ Filter for all lessons a group (class) regularly attends. """
return self.filter( return self.filter(
Q(lesson__groups=group) | Q(lesson__groups__parent_groups=group)) Q(lesson__groups=group) | Q(lesson__groups__parent_groups=group)
)
def filter_teacher(self, teacher: Union[Person, int]): def filter_teacher(self, teacher: Union[Person, int]):
""" Filter for all lessons given by a certain teacher. """ """ Filter for all lessons given by a certain teacher. """
return self.filter( return self.filter(
Q(substitutions__teachers=teacher, substitutions__week=models.F('_week')) | Q(lesson__teachers=teacher)) Q(substitutions__teachers=teacher, substitutions__week=models.F("_week"))
| Q(lesson__teachers=teacher)
)
def filter_room(self, room: Union[Room, int]): def filter_room(self, room: Union[Room, int]):
""" Filter for all lessons taking part in a certain room. """ """ Filter for all lessons taking part in a certain room. """
return self.filter( return self.filter(
Q(substitutions__room=room, substitutions__week=models.F('_week')) | Q(room=room)) Q(substitutions__room=room, substitutions__week=models.F("_week"))
| Q(room=room)
)
def annotate_week(self, week: Union[CalendarWeek, int]): def annotate_week(self, week: Union[CalendarWeek, int]):
""" Annotate all lessons in the QuerySet with the number of the provided calendar week. """ """ Annotate all lessons in the QuerySet with the number of the provided calendar week. """
...@@ -100,9 +110,7 @@ class LessonPeriodQuerySet(models.QuerySet): ...@@ -100,9 +110,7 @@ class LessonPeriodQuerySet(models.QuerySet):
else: else:
week_num = week week_num = week
return self.annotate( return self.annotate(_week=models.Value(week_num, models.IntegerField()))
_week=models.Value(week_num, models.IntegerField())
)
def next(self, reference: LessonPeriod, offset: Optional[int] = 1) -> LessonPeriod: def next(self, reference: LessonPeriod, offset: Optional[int] = 1) -> LessonPeriod:
""" Get another lesson in an ordered set of lessons. """ Get another lesson in an ordered set of lessons.
...@@ -112,7 +120,7 @@ class LessonPeriodQuerySet(models.QuerySet): ...@@ -112,7 +120,7 @@ class LessonPeriodQuerySet(models.QuerySet):
previous lesson can be selected. previous lesson can be selected.
""" """
index = list(self.values_list('id', flat=True)).index(reference.id) index = list(self.values_list("id", flat=True)).index(reference.id)
next_index = index + offset next_index = index + offset
if next_index > self.count() - 1: if next_index > self.count() - 1:
...@@ -132,35 +140,40 @@ class LessonPeriodQuerySet(models.QuerySet): ...@@ -132,35 +140,40 @@ class LessonPeriodQuerySet(models.QuerySet):
All three fields are filtered, in order. All three fields are filtered, in order.
""" """
if query_data.get('group', None): if query_data.get("group", None):
return self.filter_group(int(query_data['group'])) return self.filter_group(int(query_data["group"]))
if query_data.get('teacher', None): if query_data.get("teacher", None):
return self.filter_teacher(int(query_data['teacher'])) return self.filter_teacher(int(query_data["teacher"]))
if query_data.get('room', None): if query_data.get("room", None):
return self.filter_room(int(query_data['room'])) return self.filter_room(int(query_data["room"]))
class TimePeriod(models.Model): class TimePeriod(models.Model):
WEEKDAY_CHOICES = [ WEEKDAY_CHOICES = [
(0, _('Sunday')), (0, _("Sunday")),
(1, _('Monday')), (1, _("Monday")),
(2, _('Tuesday')), (2, _("Tuesday")),
(3, _('Wednesday')), (3, _("Wednesday")),
(4, _('Thursday')), (4, _("Thursday")),
(5, _('Friday')), (5, _("Friday")),
(6, _('Saturday')) (6, _("Saturday")),
] ]
weekday = models.PositiveSmallIntegerField(verbose_name=_( weekday = models.PositiveSmallIntegerField(
'Week day'), choices=WEEKDAY_CHOICES) verbose_name=_("Week day"), choices=WEEKDAY_CHOICES
period = models.PositiveSmallIntegerField( )
verbose_name=_('Number of period')) period = models.PositiveSmallIntegerField(verbose_name=_("Number of period"))
time_start = models.TimeField(verbose_name=_('Time the period starts')) time_start = models.TimeField(verbose_name=_("Time the period starts"))
time_end = models.TimeField(verbose_name=_('Time the period ends')) 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) return "%s, %d. period (%s - %s)" % (
self.weekday,
self.period,
self.time_start,
self.time_end,
)
@classmethod @classmethod
def get_times_dict(cls) -> Dict[int, Tuple[datetime, datetime]]: def get_times_dict(cls) -> Dict[int, Tuple[datetime, datetime]]:
...@@ -175,113 +188,143 @@ class TimePeriod(models.Model): ...@@ -175,113 +188,143 @@ class TimePeriod(models.Model):
wanted_week = week wanted_week = week
else: else:
year = date.today().year year = date.today().year
week_number = week or getattr(self, '_week', None) or CalendarWeek().week week_number = week or getattr(self, "_week", None) or CalendarWeek().week
if week_number < self.school.current_term.date_start.isocalendar()[1]: if week_number < self.school.current_term.date_start.isocalendar()[1]:
year += 1 year += 1
wanted_week = CalendarWeek(year=year, week=week_number) wanted_week = CalendarWeek(year=year, week=week_number)
return wanted_week[self.weekday-1] return wanted_week[self.weekday - 1]
class Meta: class Meta:
unique_together = [['weekday', 'period']] unique_together = [["weekday", "period"]]
ordering = ['weekday', 'period'] ordering = ["weekday", "period"]
indexes = [models.Index(fields=['time_start', 'time_end'])] indexes = [models.Index(fields=["time_start", "time_end"])]
class Subject(models.Model): class Subject(models.Model):
abbrev = models.CharField(verbose_name=_( abbrev = models.CharField(
'Abbreviation of subject in timetable'), max_length=10, unique=True) verbose_name=_("Abbreviation of subject in timetable"),
name = models.CharField(verbose_name=_( max_length=10,
'Long name of subject'), max_length=30, unique=True) unique=True,
)
colour_fg = models.CharField(verbose_name=_('Foreground colour in timetable'), blank=True, validators=[ name = models.CharField(
validators.RegexValidator(r'#[0-9A-F]{6}')], max_length=7) verbose_name=_("Long name of subject"), max_length=30, unique=True
colour_bg = models.CharField(verbose_name=_('Background colour in timetable'), blank=True, validators=[ )
validators.RegexValidator(r'#[0-9A-F]{6}')], max_length=7)
colour_fg = models.CharField(
verbose_name=_("Foreground colour in timetable"),
blank=True,
validators=[validators.RegexValidator(r"#[0-9A-F]{6}")],
max_length=7,
)
colour_bg = models.CharField(
verbose_name=_("Background colour in timetable"),
blank=True,
validators=[validators.RegexValidator(r"#[0-9A-F]{6}")],
max_length=7,
)
def __str__(self) -> str: def __str__(self) -> str:
return '%s - %s' % (self.abbrev, self.name) return "%s - %s" % (self.abbrev, self.name)
class Meta: class Meta:
ordering = ['name', 'abbrev'] ordering = ["name", "abbrev"]
class Room(models.Model): class Room(models.Model):
short_name = models.CharField(verbose_name=_( short_name = models.CharField(
'Short name, e.g. room number'), max_length=10, unique=True) verbose_name=_("Short name, e.g. room number"), max_length=10, unique=True
name = models.CharField(verbose_name=_('Long name'), )
max_length=30) name = models.CharField(verbose_name=_("Long name"), max_length=30)
def __str__(self) -> str: def __str__(self) -> str:
return '%s (%s)' % (self.name, self.short_name) return "%s (%s)" % (self.name, self.short_name)
class Meta: class Meta:
ordering = ['name', 'short_name'] ordering = ["name", "short_name"]
class Lesson(models.Model): class Lesson(models.Model):
subject = models.ForeignKey( subject = models.ForeignKey(
'Subject', on_delete=models.CASCADE, related_name='lessons') "Subject", on_delete=models.CASCADE, related_name="lessons"
teachers = models.ManyToManyField('core.Person', related_name='lessons_as_teacher') )
teachers = models.ManyToManyField("core.Person", related_name="lessons_as_teacher")
periods = models.ManyToManyField( periods = models.ManyToManyField(
'TimePeriod', related_name='lessons', through='LessonPeriod') "TimePeriod", related_name="lessons", through="LessonPeriod"
groups = models.ManyToManyField('core.Group', related_name='lessons') )
groups = models.ManyToManyField("core.Group", related_name="lessons")
date_start = models.DateField(verbose_name=_( date_start = models.DateField(
'Effective start date of lesson'), null=True) verbose_name=_("Effective start date of lesson"), null=True
date_end = models.DateField(verbose_name=_( )
'Effective end date of lesson'), null=True) date_end = models.DateField(
verbose_name=_("Effective end date of lesson"), null=True
)
@property @property
def teacher_names(self, sep: Optional[str] = ', ') -> str: def teacher_names(self, sep: Optional[str] = ", ") -> str:
return sep.join([teacher.full_name for teacher in self.teachers.all()]) return sep.join([teacher.full_name for teacher in self.teachers.all()])
@property @property
def group_names(self, sep: Optional[str] = ', ') -> str: def group_names(self, sep: Optional[str] = ", ") -> str:
return sep.join([group.short_name for group in self.groups.all()]) return sep.join([group.short_name for group in self.groups.all()])
def get_calendar_week(self, week: int): def get_calendar_week(self, week: int):
year = self.date_start.year year = self.date_start.year
if week < int(self.date_start.strftime('%V')): if week < int(self.date_start.strftime("%V")):
year += 1 year += 1
return CalendarWeek(year=year, week=week) return CalendarWeek(year=year, week=week)
class Meta: class Meta:
ordering = ['date_start'] ordering = ["date_start"]
indexes = [models.Index(fields=['date_start', 'date_end'])] indexes = [models.Index(fields=["date_start", "date_end"])]
class LessonSubstitution(models.Model): class LessonSubstitution(models.Model):
week = models.IntegerField(verbose_name=_('Week'), week = models.IntegerField(
default=CalendarWeek.current_week) verbose_name=_("Week"), default=CalendarWeek.current_week
)
lesson_period = models.ForeignKey( lesson_period = models.ForeignKey("LessonPeriod", models.CASCADE, "substitutions")
'LessonPeriod', models.CASCADE, 'substitutions')
subject = models.ForeignKey( subject = models.ForeignKey(
'Subject', on_delete=models.CASCADE, "Subject",
related_name='lesson_substitutions', null=True, blank=True, verbose_name=_('Subject')) on_delete=models.CASCADE,
teachers = models.ManyToManyField('core.Person', related_name="lesson_substitutions",
related_name='lesson_substitutions', blank=True) null=True,
room = models.ForeignKey('Room', models.CASCADE, null=True, blank=True, verbose_name=_('Room')) blank=True,
verbose_name=_("Subject"),
)
teachers = models.ManyToManyField(
"core.Person", related_name="lesson_substitutions", blank=True
)
room = models.ForeignKey(
"Room", models.CASCADE, null=True, blank=True, verbose_name=_("Room")
)
cancelled = models.BooleanField(default=False) cancelled = models.BooleanField(default=False)
def clean(self) -> None: def clean(self) -> None:
if self.subject and self.cancelled: if self.subject and self.cancelled:
raise ValidationError(_('Lessons can only be either substituted or cancelled.')) raise ValidationError(
_("Lessons can only be either substituted or cancelled.")
)
class Meta: class Meta:
unique_together = [['lesson_period', 'week']] unique_together = [["lesson_period", "week"]]
ordering = ['lesson_period__lesson__date_start', 'week', ordering = [
'lesson_period__period__weekday', 'lesson_period__period__period'] "lesson_period__lesson__date_start",
"week",
"lesson_period__period__weekday",
"lesson_period__period__period",
]
constraints = [ constraints = [
models.CheckConstraint( models.CheckConstraint(
check=~Q(cancelled=True, subject__isnull=False), check=~Q(cancelled=True, subject__isnull=False),
name='either_substituted_or_cancelled' name="either_substituted_or_cancelled",
) )
] ]
...@@ -289,13 +332,17 @@ class LessonSubstitution(models.Model): ...@@ -289,13 +332,17 @@ class LessonSubstitution(models.Model):
class LessonPeriod(models.Model, ExtensibleModel): class LessonPeriod(models.Model, ExtensibleModel):
objects = LessonPeriodManager.from_queryset(LessonPeriodQuerySet)() objects = LessonPeriodManager.from_queryset(LessonPeriodQuerySet)()
lesson = models.ForeignKey('Lesson', models.CASCADE, related_name='lesson_periods') lesson = models.ForeignKey("Lesson", models.CASCADE, related_name="lesson_periods")
period = models.ForeignKey('TimePeriod', models.CASCADE, related_name='lesson_periods') period = models.ForeignKey(
"TimePeriod", models.CASCADE, related_name="lesson_periods"
)
room = models.ForeignKey('Room', models.CASCADE, null=True, related_name='lesson_periods') room = models.ForeignKey(
"Room", models.CASCADE, null=True, related_name="lesson_periods"
)
def get_substitution(self, week: Optional[int] = None) -> LessonSubstitution: def get_substitution(self, week: Optional[int] = None) -> LessonSubstitution:
wanted_week = week or getattr(self, '_week', None) or CalendarWeek().week wanted_week = week or getattr(self, "_week", None) or CalendarWeek().week
# We iterate over all substitutions because this can make use of # We iterate over all substitutions because this can make use of
# prefetching when this model is loaded from outside, in contrast # prefetching when this model is loaded from outside, in contrast
...@@ -323,17 +370,20 @@ class LessonPeriod(models.Model, ExtensibleModel): ...@@ -323,17 +370,20 @@ class LessonPeriod(models.Model, ExtensibleModel):
else: else:
return self.room return self.room
def get_teacher_names(self, sep: Optional[str] = ', ') -> str: def get_teacher_names(self, sep: Optional[str] = ", ") -> str:
return sep.join([teacher.full_name for teacher in self.get_teachers().all()]) return sep.join([teacher.full_name for teacher in self.get_teachers().all()])
def get_groups(self) -> models.query.QuerySet: def get_groups(self) -> models.query.QuerySet:
return self.lesson.groups return self.lesson.groups
def __str__(self) -> str: def __str__(self) -> str:
return '%s, %d., %s, %s' % (self.period.get_weekday_display(), self.period.period, return "%s, %d., %s, %s" % (
', '.join(list(self.lesson.groups.values_list('short_name', flat=True))), self.period.get_weekday_display(),
self.lesson.subject.name) self.period.period,
", ".join(list(self.lesson.groups.values_list("short_name", flat=True))),
self.lesson.subject.name,
)
class Meta: class Meta:
ordering = ['lesson__date_start', 'period__weekday', 'period__period'] ordering = ["lesson__date_start", "period__weekday", "period__period"]
indexes = [models.Index(fields=['lesson', 'period'])] indexes = [models.Index(fields=["lesson", "period"])]
...@@ -3,43 +3,59 @@ from __future__ import annotations ...@@ -3,43 +3,59 @@ from __future__ import annotations
from typing import Optional from typing import Optional
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
import django_tables2 as tables import django_tables2 as tables
from django_tables2.utils import A from django_tables2.utils import A
from .models import LessonPeriod from .models import LessonPeriod
def _css_class_from_lesson_state(record: Optional[LessonPeriod] = None, table: Optional[LessonsTable] = None) -> str: def _css_class_from_lesson_state(
record: Optional[LessonPeriod] = None, table: Optional[LessonsTable] = None
) -> str:
if record.get_substitution(record._week): if record.get_substitution(record._week):
if record.get_substitution(record._week).cancelled: if record.get_substitution(record._week).cancelled:
return 'table-danger' return "table-danger"
else: else:
return 'table-warning' return "table-warning"
else: else:
return '' return ""
class LessonsTable(tables.Table): class LessonsTable(tables.Table):
class Meta: class Meta:
attrs = {'class': 'table table-striped table-bordered table-hover table-responsive-xl'} attrs = {
row_attrs = {'class': _css_class_from_lesson_state} "class": "table table-striped table-bordered table-hover table-responsive-xl"
}
period__period = tables.Column(accessor='period.period') row_attrs = {"class": _css_class_from_lesson_state}
lesson__groups = tables.Column(accessor='lesson.group_names', verbose_name=_('Groups'))
lesson__teachers = tables.Column(accessor='lesson.teacher_names', verbose_name=_('Teachers')) period__period = tables.Column(accessor="period.period")
lesson__subject = tables.Column(accessor='lesson.subject') lesson__groups = tables.Column(
room = tables.Column(accessor='room') accessor="lesson.group_names", verbose_name=_("Groups")
)
lesson__teachers = tables.Column(
accessor="lesson.teacher_names", verbose_name=_("Teachers")
)
lesson__subject = tables.Column(accessor="lesson.subject")
room = tables.Column(accessor="room")
edit_substitution = tables.LinkColumn( edit_substitution = tables.LinkColumn(
'edit_substitution', args=[A('id'), A('_week')], text=_('Substitution')) "edit_substitution", args=[A("id"), A("_week")], text=_("Substitution")
)
class SubstitutionsTable(tables.Table): class SubstitutionsTable(tables.Table):
class Meta: class Meta:
attrs = {'class': 'table table-striped table-bordered table-hover table-responsive-xl'} attrs = {
"class": "table table-striped table-bordered table-hover table-responsive-xl"
lesson_period = tables.Column(verbose_name=_('Lesson')) }
lesson__groups = tables.Column(accessor='lesson_period.lesson.group_names', verbose_name=_('Groups'))
lesson__teachers = tables.Column(accessor='lesson_period.get_teacher_names', verbose_name=_('Teachers')) lesson_period = tables.Column(verbose_name=_("Lesson"))
lesson__subject = tables.Column(accessor='subject') lesson__groups = tables.Column(
room = tables.Column(accessor='room') accessor="lesson_period.lesson.group_names", verbose_name=_("Groups")
cancelled = tables.BooleanColumn(accessor='cancelled', verbose_name=_('Cancelled')) )
lesson__teachers = tables.Column(
accessor="lesson_period.get_teacher_names", verbose_name=_("Teachers")
)
lesson__subject = tables.Column(accessor="subject")
room = tables.Column(accessor="room")
cancelled = tables.BooleanColumn(accessor="cancelled", verbose_name=_("Cancelled"))
...@@ -4,8 +4,7 @@ from typing import Optional, Union ...@@ -4,8 +4,7 @@ from typing import Optional, Union
from django import template from django import template
from django.db.models.query import QuerySet from django.db.models.query import QuerySet
from ..util import CalendarWeek, week_weekday_to_date, week_period_to_date from ..util import CalendarWeek, week_period_to_date, week_weekday_to_date
register = template.Library() register = template.Library()
......
...@@ -2,14 +2,25 @@ from django.urls import path ...@@ -2,14 +2,25 @@ from django.urls import path
from . import views from . import views
urlpatterns = [ urlpatterns = [
path('timetable', views.timetable, name='timetable'), path("timetable", views.timetable, name="timetable"),
path('timetable/<int:year>/<int:week>', views.timetable, name='timetable_by_week'), path("timetable/<int:year>/<int:week>", views.timetable, name="timetable_by_week"),
path('lessons', views.lessons_day, name='lessons_day'), path("lessons", views.lessons_day, name="lessons_day"),
path('lessons/<when>', views.lessons_day, name='lessons_day_by_date'), path("lessons/<when>", views.lessons_day, name="lessons_day_by_date"),
path('lessons/<int:id_>/<int:week>/substition', views.edit_substitution, name='edit_substitution'), path(
path('lessons/<int:id_>/<int:week>/substition/delete', views.delete_substitution, name='delete_substitution'), "lessons/<int:id_>/<int:week>/substition",
path('substitutions', views.substitutions, name='substitutions'), views.edit_substitution,
path('substitutions/<int:year>/<int:week>', views.substitutions, name='substitutions_by_week') name="edit_substitution",
),
path(
"lessons/<int:id_>/<int:week>/substition/delete",
views.delete_substitution,
name="delete_substitution",
),
path("substitutions", views.substitutions, name="substitutions"),
path(
"substitutions/<int:year>/<int:week>",
views.substitutions,
name="substitutions_by_week",
),
] ]
...@@ -20,7 +20,7 @@ class CalendarWeek: ...@@ -20,7 +20,7 @@ class CalendarWeek:
def from_date(cls, when: date): def from_date(cls, when: date):
""" Get the calendar week by a date object (the week this date is in). """ """ Get the calendar week by a date object (the week this date is in). """
return cls(year=when.year, week=int(when.strftime('%V'))) return cls(year=when.year, week=int(when.strftime("%V")))
@classmethod @classmethod
def current_week(cls) -> int: def current_week(cls) -> int:
...@@ -33,7 +33,7 @@ class CalendarWeek: ...@@ -33,7 +33,7 @@ class CalendarWeek:
""" Get all calendar weeks within a date range. """ """ Get all calendar weeks within a date range. """
if start > end: if start > end:
raise ValueError('End date must be after start date.') raise ValueError("End date must be after start date.")
current = start current = start
weeks = [] weeks = []
...@@ -49,22 +49,30 @@ class CalendarWeek: ...@@ -49,22 +49,30 @@ class CalendarWeek:
if not self.year: if not self.year:
self.year = today.year self.year = today.year
if not self.week: if not self.week:
self.week = int(today.strftime('%V')) self.week = int(today.strftime("%V"))
def __str__(self) -> str: def __str__(self) -> str:
return '%s %d (%s %s %s)' % (_('Calendar Week'), self.week, self[0], _('to'), self[-1]) return "%s %d (%s %s %s)" % (
_("Calendar Week"),
self.week,
self[0],
_("to"),
self[-1],
)
def __len__(self) -> int: def __len__(self) -> int:
return 7 return 7
def __getitem__(self, n: int) -> date: def __getitem__(self, n: int) -> date:
if n < -7 or n > 6: if n < -7 or n > 6:
raise IndexError('Week day %d is out of range.' % n) raise IndexError("Week day %d is out of range." % n)
if n < 0: if n < 0:
n += 7 n += 7
return datetime.strptime('%d-%d-%d' % (self.year, self.week, n + 1), '%G-%V-%u').date() return datetime.strptime(
"%d-%d-%d" % (self.year, self.week, n + 1), "%G-%V-%u"
).date()
def __contains__(self, day: date) -> bool: def __contains__(self, day: date) -> bool:
return self.__class__.form_date(day) == self return self.__class__.form_date(day) == self
......
from datetime import date, datetime, timedelta
from collections import OrderedDict from collections import OrderedDict
from datetime import date, datetime, timedelta
from typing import Optional from typing import Optional
from django.contrib.auth.decorators import login_required from django.contrib.auth.decorators import login_required
...@@ -14,14 +14,16 @@ from django_tables2 import RequestConfig ...@@ -14,14 +14,16 @@ from django_tables2 import RequestConfig
from biscuit.core.decorators import admin_required from biscuit.core.decorators import admin_required
from biscuit.core.util import messages from biscuit.core.util import messages
from .forms import SelectForm, LessonSubstitutionForm from .forms import LessonSubstitutionForm, SelectForm
from .models import LessonPeriod, TimePeriod, LessonSubstitution from .models import LessonPeriod, LessonSubstitution, TimePeriod
from .util import CalendarWeek
from .tables import LessonsTable, SubstitutionsTable from .tables import LessonsTable, SubstitutionsTable
from .util import CalendarWeek
@login_required @login_required
def timetable(request: HttpRequest, year: Optional[int] = None, week: Optional[int] = None) -> HttpResponse: def timetable(
request: HttpRequest, year: Optional[int] = None, week: Optional[int] = None
) -> HttpResponse:
context = {} context = {}
if year and week: if year and week:
...@@ -32,60 +34,77 @@ def timetable(request: HttpRequest, year: Optional[int] = None, week: Optional[i ...@@ -32,60 +34,77 @@ def timetable(request: HttpRequest, year: Optional[int] = None, week: Optional[i
lesson_periods = LessonPeriod.objects.in_week(wanted_week) lesson_periods = LessonPeriod.objects.in_week(wanted_week)
# Incrementally filter lesson periods by GET parameters # Incrementally filter lesson periods by GET parameters
if request.GET.get('group', None) or request.GET.get('teacher', None) or request.GET.get('room', None): 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) lesson_periods = lesson_periods.filter_from_query(request.GET)
else: else:
# Redirect to a selected view if no filter provided # Redirect to a selected view if no filter provided
if request.user.person: if request.user.person:
if request.user.person.primary_group: if request.user.person.primary_group:
return redirect(reverse('timetable') + '?group=%d' % request.user.person.primary_group.pk) return redirect(
reverse("timetable")
+ "?group=%d" % request.user.person.primary_group.pk
)
elif lesson_periods.filter(lesson__teachers=request.user.person).exists(): elif lesson_periods.filter(lesson__teachers=request.user.person).exists():
return redirect(reverse('timetable') + '?teacher=%d' % request.user.person.pk) return redirect(
reverse("timetable") + "?teacher=%d" % request.user.person.pk
)
# Regroup lesson periods per weekday # Regroup lesson periods per weekday
per_day = {} per_day = {}
for lesson_period in lesson_periods: for lesson_period in lesson_periods:
per_day.setdefault(lesson_period.period.weekday, per_day.setdefault(lesson_period.period.weekday, {})[
{})[lesson_period.period.period] = lesson_period lesson_period.period.period
] = lesson_period
# Determine overall first and last day and period # Determine overall first and last day and period
min_max = TimePeriod.objects.aggregate( min_max = TimePeriod.objects.aggregate(
Min('period'), Max('period'), Min("period"), Max("period"), Min("weekday"), Max("weekday")
Min('weekday'), Max('weekday')) )
# Fill in empty lessons # Fill in empty lessons
for weekday_num in range(min_max.get('weekday__min', 0), for weekday_num in range(
min_max.get('weekday__max', 6) + 1): min_max.get("weekday__min", 0), min_max.get("weekday__max", 6) + 1
):
# Fill in empty weekdays # Fill in empty weekdays
if weekday_num not in per_day.keys(): if weekday_num not in per_day.keys():
per_day[weekday_num] = {} per_day[weekday_num] = {}
# Fill in empty lessons on this workday # Fill in empty lessons on this workday
for period_num in range(min_max.get('period__min', 1), for period_num in range(
min_max.get('period__max', 7) + 1): min_max.get("period__min", 1), min_max.get("period__max", 7) + 1
):
if period_num not in per_day[weekday_num].keys(): if period_num not in per_day[weekday_num].keys():
per_day[weekday_num][period_num] = None per_day[weekday_num][period_num] = None
# Order this weekday by periods # Order this weekday by periods
per_day[weekday_num] = OrderedDict( per_day[weekday_num] = OrderedDict(sorted(per_day[weekday_num].items()))
sorted(per_day[weekday_num].items()))
# Add a form to filter the view # Add a form to filter the view
select_form = SelectForm(request.GET or None) select_form = SelectForm(request.GET or None)
context['current_head'] = _('Timetable') context["current_head"] = _("Timetable")
context['lesson_periods'] = OrderedDict(sorted(per_day.items())) context["lesson_periods"] = OrderedDict(sorted(per_day.items()))
context['periods'] = TimePeriod.get_times_dict() context["periods"] = TimePeriod.get_times_dict()
context['weekdays'] = dict(TimePeriod.WEEKDAY_CHOICES) context["weekdays"] = dict(TimePeriod.WEEKDAY_CHOICES)
context['week'] = wanted_week context["week"] = wanted_week
context['select_form'] = select_form context["select_form"] = select_form
week_prev = wanted_week - 1 week_prev = wanted_week - 1
week_next = wanted_week + 1 week_next = wanted_week + 1
context['url_prev'] = '%s?%s' % (reverse('timetable_by_week', args=[week_prev.year, week_prev.week]), request.GET.urlencode()) context["url_prev"] = "%s?%s" % (
context['url_next'] = '%s?%s' % (reverse('timetable_by_week', args=[week_next.year, week_next.week]), request.GET.urlencode()) reverse("timetable_by_week", args=[week_prev.year, week_prev.week]),
request.GET.urlencode(),
)
context["url_next"] = "%s?%s" % (
reverse("timetable_by_week", args=[week_next.year, week_next.week]),
request.GET.urlencode(),
)
return render(request, 'chronos/tt_week.html', context) return render(request, "chronos/tt_week.html", context)
@login_required @login_required
...@@ -93,7 +112,7 @@ def lessons_day(request: HttpRequest, when: Optional[str] = None) -> HttpRespons ...@@ -93,7 +112,7 @@ def lessons_day(request: HttpRequest, when: Optional[str] = None) -> HttpRespons
context = {} context = {}
if when: if when:
day = datetime.strptime(when, '%Y-%m-%d').date() day = datetime.strptime(when, "%Y-%m-%d").date()
else: else:
day = date.today() day = date.today()
...@@ -104,17 +123,21 @@ def lessons_day(request: HttpRequest, when: Optional[str] = None) -> HttpRespons ...@@ -104,17 +123,21 @@ def lessons_day(request: HttpRequest, when: Optional[str] = None) -> HttpRespons
lessons_table = LessonsTable(lesson_periods.all()) lessons_table = LessonsTable(lesson_periods.all())
RequestConfig(request).configure(lessons_table) RequestConfig(request).configure(lessons_table)
context['current_head'] = _('Lessons %s') % (day) context["current_head"] = _("Lessons %s") % (day)
context['lessons_table'] = lessons_table context["lessons_table"] = lessons_table
context['day'] = day context["day"] = day
context['lesson_periods'] = lesson_periods context["lesson_periods"] = lesson_periods
day_prev = day - timedelta(days=1) day_prev = day - timedelta(days=1)
day_next = day + timedelta(days=1) day_next = day + timedelta(days=1)
context['url_prev'] = reverse('lessons_day_by_date', args=[day_prev.strftime('%Y-%m-%d')]) context["url_prev"] = reverse(
context['url_next'] = reverse('lessons_day_by_date', args=[day_next.strftime('%Y-%m-%d')]) "lessons_day_by_date", args=[day_prev.strftime("%Y-%m-%d")]
)
context["url_next"] = reverse(
"lessons_day_by_date", args=[day_next.strftime("%Y-%m-%d")]
)
return render(request, 'chronos/lessons_day.html', context) return render(request, "chronos/lessons_day.html", context)
@admin_required @admin_required
...@@ -125,26 +148,33 @@ def edit_substitution(request: HttpRequest, id_: int, week: int) -> HttpResponse ...@@ -125,26 +148,33 @@ def edit_substitution(request: HttpRequest, id_: int, week: int) -> HttpResponse
wanted_week = lesson_period.lesson.get_calendar_week(week) wanted_week = lesson_period.lesson.get_calendar_week(week)
lesson_substitution = LessonSubstitution.objects.filter( lesson_substitution = LessonSubstitution.objects.filter(
week=wanted_week.week, lesson_period=lesson_period).first() week=wanted_week.week, lesson_period=lesson_period
).first()
if lesson_substitution: if lesson_substitution:
edit_substitution_form = LessonSubstitutionForm( edit_substitution_form = LessonSubstitutionForm(
request.POST or None, instance=lesson_substitution) request.POST or None, instance=lesson_substitution
)
else: else:
edit_substitution_form = LessonSubstitutionForm( edit_substitution_form = LessonSubstitutionForm(
request.POST or None, initial={'week': wanted_week.week, 'lesson_period': lesson_period}) request.POST or None,
initial={"week": wanted_week.week, "lesson_period": lesson_period},
)
context['substitution'] = lesson_substitution context["substitution"] = lesson_substitution
if request.method == 'POST': if request.method == "POST":
if edit_substitution_form.is_valid(): if edit_substitution_form.is_valid():
edit_substitution_form.save(commit=True) edit_substitution_form.save(commit=True)
messages.success(request, _('The substitution has been saved.')) messages.success(request, _("The substitution has been saved."))
return redirect('lessons_day_by_date', when=wanted_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 context["edit_substitution_form"] = edit_substitution_form
return render(request, 'chronos/edit_substitution.html', context) return render(request, "chronos/edit_substitution.html", context)
@admin_required @admin_required
...@@ -158,11 +188,16 @@ def delete_substitution(request: HttpRequest, id_: int, week: int) -> HttpRespon ...@@ -158,11 +188,16 @@ def delete_substitution(request: HttpRequest, id_: int, week: int) -> HttpRespon
week=wanted_week.week, lesson_period=lesson_period week=wanted_week.week, lesson_period=lesson_period
).delete() ).delete()
messages.success(request, _('The substitution has been deleted.')) messages.success(request, _("The substitution has been deleted."))
return redirect('lessons_day_by_date', when=wanted_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"),
)
def substitutions(request: HttpRequest, year: Optional[int] = None, week: Optional[int] = None) -> HttpResponse: def substitutions(
request: HttpRequest, year: Optional[int] = None, week: Optional[int] = None
) -> HttpResponse:
context = {} context = {}
if week: if week:
...@@ -176,11 +211,15 @@ def substitutions(request: HttpRequest, year: Optional[int] = None, week: Option ...@@ -176,11 +211,15 @@ def substitutions(request: HttpRequest, year: Optional[int] = None, week: Option
substitutions_table = SubstitutionsTable(substitutions) substitutions_table = SubstitutionsTable(substitutions)
RequestConfig(request).configure(substitutions_table) RequestConfig(request).configure(substitutions_table)
context['current_head'] = str(wanted_week) context["current_head"] = str(wanted_week)
context['substitutions_table'] = substitutions_table context["substitutions_table"] = substitutions_table
week_prev = wanted_week - 1 week_prev = wanted_week - 1
week_next = wanted_week + 1 week_next = wanted_week + 1
context['url_prev'] = '%s' % (reverse('substitutions_by_week', args=[week_prev.year, week_prev.week])) context["url_prev"] = "%s" % (
context['url_next'] = '%s' % (reverse('substitutions_by_week', args=[week_next.year, week_next.week])) reverse("substitutions_by_week", args=[week_prev.year, week_prev.week])
)
return render(request, 'chronos/substitutions.html', context) context["url_next"] = "%s" % (
reverse("substitutions_by_week", args=[week_next.year, week_next.week])
)
return render(request, "chronos/substitutions.html", context)