diff --git a/aleksis/apps/chronos/admin.py b/aleksis/apps/chronos/admin.py
index 56878a6c21c4f1c588de90d8c9eaec1ad3926d7d..8c99c4b5c23d10f027fae74e34da80ecfa6ea8a6 100644
--- a/aleksis/apps/chronos/admin.py
+++ b/aleksis/apps/chronos/admin.py
@@ -1,4 +1,4 @@
-#noqa
+# noqa
 
 from django.contrib import admin
 from django.utils.html import format_html
@@ -153,7 +153,7 @@ admin.site.register(Room, RoomAdmin)
 
 class SubjectAdmin(admin.ModelAdmin):
     def _colour(self, obj):
-        return colour_badge(obj.colour_fg, obj.colour_bg, obj.short_name, )
+        return colour_badge(obj.colour_fg, obj.colour_bg, obj.short_name,)
 
     list_display = ("short_name", "name", "_colour")
     list_display_links = ("short_name", "name")
diff --git a/aleksis/apps/chronos/forms.py b/aleksis/apps/chronos/forms.py
index ab31b103aeccfd231787f82bae36f80eb8586191..0e676e2f72b16db074f7e9b3099fba72a9cce9c2 100644
--- a/aleksis/apps/chronos/forms.py
+++ b/aleksis/apps/chronos/forms.py
@@ -24,4 +24,6 @@ class LessonSubstitutionForm(forms.ModelForm):
         }
 
 
-AnnouncementForm.add_node_to_layout(Fieldset(_("Options for timetables"), "show_in_timetables"))
+AnnouncementForm.add_node_to_layout(
+    Fieldset(_("Options for timetables"), "show_in_timetables")
+)
diff --git a/aleksis/apps/chronos/managers.py b/aleksis/apps/chronos/managers.py
index 1087f8e09348ea957e0046763940e27ff4249b9a..2f32a3e49c2d1d5ed7285e1fb01d63c128af1172 100644
--- a/aleksis/apps/chronos/managers.py
+++ b/aleksis/apps/chronos/managers.py
@@ -9,8 +9,12 @@ from django.db.models import F, Q, Count
 from django.http import QueryDict
 
 from aleksis.core.models import Person, Group
+from aleksis.core.util.core_helpers import get_site_preferences
+
 
 class TimetableType(Enum):
+    """Enum for different types of timetables."""
+
     GROUP = "group"
     TEACHER = "teacher"
     ROOM = "room"
@@ -89,8 +93,10 @@ class LessonDataQuerySet(models.QuerySet, WeekQuerySetMixin):
         """ Filter for all lessons within a calendar week. """
 
         return self.within_dates(
-            wanted_week[0] + timedelta(days=1) * (F(self._period_path + "period__weekday") - 1),
-            wanted_week[0] + timedelta(days=1) * (F(self._period_path + "period__weekday") - 1),
+            wanted_week[0]
+            + timedelta(days=1) * (F(self._period_path + "period__weekday") - 1),
+            wanted_week[0]
+            + timedelta(days=1) * (F(self._period_path + "period__weekday") - 1),
         ).annotate_week(wanted_week)
 
     def on_day(self, day: date):
@@ -125,7 +131,9 @@ class LessonDataQuerySet(models.QuerySet, WeekQuerySetMixin):
 
         return self.filter(
             Q(**{self._period_path + "lesson__groups__members": person})
-            | Q(**{self._period_path + "lesson__groups__parent_groups__members": person})
+            | Q(
+                **{self._period_path + "lesson__groups__parent_groups__members": person}
+            )
         )
 
     def filter_group(self, group: Union[Group, int]):
@@ -147,7 +155,12 @@ class LessonDataQuerySet(models.QuerySet, WeekQuerySetMixin):
         """ Filter for all lessons given by a certain teacher. """
 
         qs1 = self.filter(**{self._period_path + "lesson__teachers": teacher})
-        qs2 = self.filter(**{self._subst_path + "teachers": teacher, self._subst_path + "week": F("_week"), })
+        qs2 = self.filter(
+            **{
+                self._subst_path + "teachers": teacher,
+                self._subst_path + "week": F("_week"),
+            }
+        )
 
         return qs1.union(qs2)
 
@@ -155,11 +168,15 @@ class LessonDataQuerySet(models.QuerySet, WeekQuerySetMixin):
         """ Filter for all lessons taking part in a certain room. """
 
         qs1 = self.filter(**{self._period_path + "room": room})
-        qs2 = self.filter(**{self._subst_path + "room": room, self._subst_path + "week": F("_week"),})
+        qs2 = self.filter(
+            **{self._subst_path + "room": room, self._subst_path + "week": F("_week"),}
+        )
 
         return qs1.union(qs2)
 
     def group_by_periods(self, is_person: bool = False) -> dict:
+        """Group a QuerySet of objects with attribute period by period numbers and weekdays."""
+
         per_period = {}
         for obj in self:
             period = obj.period.period
@@ -178,7 +195,11 @@ class LessonDataQuerySet(models.QuerySet, WeekQuerySetMixin):
 
         return per_period
 
-    def filter_from_type(self, type_: TimetableType, pk: int) -> Optional[models.QuerySet]:
+    def filter_from_type(
+        self, type_: TimetableType, pk: int
+    ) -> Optional[models.QuerySet]:
+        """Filter lesson data for a group, teacher or room by provided type."""
+
         if type_ == TimetableType.GROUP:
             return self.filter_group(pk)
         elif type_ == TimetableType.TEACHER:
@@ -189,6 +210,8 @@ class LessonDataQuerySet(models.QuerySet, WeekQuerySetMixin):
             return None
 
     def filter_from_person(self, person: Person) -> Optional[models.QuerySet]:
+        """Filter lesson data for a person."""
+
         type_ = person.timetable_type
 
         if type_ == TimetableType.TEACHER:
@@ -205,7 +228,11 @@ class LessonDataQuerySet(models.QuerySet, WeekQuerySetMixin):
             # If no student or teacher
             return None
 
-    def daily_lessons_for_person(self, person: Person, wanted_day: date) -> Optional[models.QuerySet]:
+    def daily_lessons_for_person(
+        self, person: Person, wanted_day: date
+    ) -> Optional[models.QuerySet]:
+        """Filter lesson data on a day by a person."""
+
         if person.timetable_type is None:
             return None
 
@@ -213,7 +240,9 @@ class LessonDataQuerySet(models.QuerySet, WeekQuerySetMixin):
 
         return lesson_periods
 
-    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.
 
         By default, it returns the next lesson in the set. By passing the offset argument,
@@ -234,17 +263,21 @@ class LessonDataQuerySet(models.QuerySet, WeekQuerySetMixin):
 
 
 class LessonPeriodQuerySet(LessonDataQuerySet):
+    """QuerySet with custom query methods for lesson periods."""
+
     _period_path = ""
     _subst_path = "substitutions__"
 
 
 class LessonSubstitutionQuerySet(LessonDataQuerySet):
+    """QuerySet with custom query methods for substitutions."""
+
     _period_path = "lesson_period__"
     _subst_path = ""
 
     def affected_lessons(self):
         """ Return all lessons which are affected by selected substitutions """
-        from .models import Lesson # noaq
+        from .models import Lesson  # noaq
 
         return Lesson.objects.filter(lesson_periods__substitutions__in=self)
 
@@ -265,6 +298,11 @@ class LessonSubstitutionQuerySet(LessonDataQuerySet):
 
 
 class DateRangeQuerySet(models.QuerySet):
+    """QuerySet with custom query methods for models with date and period ranges.
+
+    Filterable fields: date_start, date_end, period_from, period_to
+    """
+
     def within_dates(self, start: date, end: date):
         """ Filter for all events within a date range. """
 
@@ -286,28 +324,41 @@ class DateRangeQuerySet(models.QuerySet):
         now = when or datetime.now()
 
         return self.on_day(now.date()).filter(
-            period_from__time_start__lte=now.time(),
-            period_to__time_end__gte=now.time()
+            period_from__time_start__lte=now.time(), period_to__time_end__gte=now.time()
         )
 
 
 class AbsenceQuerySet(DateRangeQuerySet):
+    """QuerySet with custom query methods for absences."""
+
     def absent_teachers(self):
-        return Person.objects.filter(absences__in=self).annotate(absences_count=Count("absences"))
+        return Person.objects.filter(absences__in=self).annotate(
+            absences_count=Count("absences")
+        )
 
     def absent_groups(self):
-        return Group.objects.filter(absences__in=self).annotate(absences_count=Count("absences"))
+        return Group.objects.filter(absences__in=self).annotate(
+            absences_count=Count("absences")
+        )
 
     def absent_rooms(self):
-        return Person.objects.filter(absences__in=self).annotate(absences_count=Count("absences"))
+        return Person.objects.filter(absences__in=self).annotate(
+            absences_count=Count("absences")
+        )
 
 
 class HolidayQuerySet(DateRangeQuerySet):
+    """QuerySet with custom query methods for holidays."""
+
     pass
 
 
 class SupervisionQuerySet(models.QuerySet, WeekQuerySetMixin):
+    """QuerySet with custom query methods for supervisions."""
+
     def filter_by_weekday(self, weekday: int):
+        """Filter supervisions by weekday."""
+
         self.filter(
             Q(break_item__before_period__weekday=weekday)
             | Q(break_item__after_period__weekday=weekday)
@@ -324,13 +375,16 @@ class SupervisionQuerySet(models.QuerySet, WeekQuerySetMixin):
 
             dates = [week[w] for w in range(0, 7)]
 
-            return self.filter(Q(substitutions__teacher=teacher, substitutions__date__in=dates) | Q(teacher=teacher))
+            return self.filter(
+                Q(substitutions__teacher=teacher, substitutions__date__in=dates)
+                | Q(teacher=teacher)
+            )
 
         return self
 
 
 class TimetableQuerySet(models.QuerySet):
-    """ Common filters
+    """Common query set methods for objects in timetables.
 
      Models need following fields:
      - groups
@@ -370,7 +424,11 @@ class TimetableQuerySet(models.QuerySet):
         else:
             return self.filter(room=room)
 
-    def filter_from_type(self, type_: TimetableType, pk: int) -> Optional[models.QuerySet]:
+    def filter_from_type(
+        self, type_: TimetableType, pk: int
+    ) -> Optional[models.QuerySet]:
+        """Filter data for a group, teacher or room by provided type."""
+
         if type_ == TimetableType.GROUP:
             return self.filter_group(pk)
         elif type_ == TimetableType.TEACHER:
@@ -381,6 +439,8 @@ class TimetableQuerySet(models.QuerySet):
             return None
 
     def filter_from_person(self, person: Person) -> Optional[models.QuerySet]:
+        """Filter data by person."""
+
         type_ = person.timetable_type
 
         if type_ == TimetableType.TEACHER:
@@ -399,6 +459,8 @@ class TimetableQuerySet(models.QuerySet):
 
 
 class EventQuerySet(DateRangeQuerySet, TimetableQuerySet):
+    """QuerySet with custom query methods for events."""
+
     def annotate_day(self, day: date):
         """ Annotate all events in the QuerySet with the provided date. """
 
@@ -406,9 +468,13 @@ class EventQuerySet(DateRangeQuerySet, TimetableQuerySet):
 
 
 class ExtraLessonQuerySet(TimetableQuerySet):
+    """QuerySet with custom query methods for extra lessons."""
+
     _multiple_rooms = False
 
     def within_dates(self, start: date, end: date):
+        """Filter all extra lessons within a specific time range."""
+
         week_start = CalendarWeek.from_date(start)
         week_end = CalendarWeek.from_date(end)
 
@@ -419,11 +485,18 @@ class ExtraLessonQuerySet(TimetableQuerySet):
             period__weekday__lte=end.weekday(),
         )
 
-    def on_day(self, day:date):
+    def on_day(self, day: date):
+        """Filter all extra lessons on a day."""
+
         return self.within_dates(day, day)
 
 
 class GroupPropertiesMixin:
+    """Mixin for common group properties.
+
+    Needed field: `groups`
+    """
+
     @property
     def group_names(self, sep: Optional[str] = ", ") -> str:
         return sep.join([group.short_name for group in self.groups.all()])
@@ -431,7 +504,11 @@ class GroupPropertiesMixin:
     @property
     def groups_to_show(self) -> models.QuerySet:
         groups = self.groups.all()
-        if groups.count() == 1 and groups[0].parent_groups.all() and get_site_preferences()["chronos__use_parent_groups"]:
+        if (
+            groups.count() == 1
+            and groups[0].parent_groups.all()
+            and get_site_preferences()["chronos__use_parent_groups"]
+        ):
             return groups[0].parent_groups.all()
         else:
             return groups
@@ -442,6 +519,11 @@ class GroupPropertiesMixin:
 
 
 class TeacherPropertiesMixin:
+    """Mixin for common teacher properties.
+
+    Needed field: `teacher`
+    """
+
     @property
     def teacher_names(self, sep: Optional[str] = ", ") -> str:
         return sep.join([teacher.full_name for teacher in self.teachers.all()])
diff --git a/aleksis/apps/chronos/menus.py b/aleksis/apps/chronos/menus.py
index 592e6df90319d6165615e8ab3551101ce6e0fbbd..4a07e4e87d46782bd43782725e6a54b549c3393c 100644
--- a/aleksis/apps/chronos/menus.py
+++ b/aleksis/apps/chronos/menus.py
@@ -17,7 +17,10 @@ MENUS = {
                     "url": "my_timetable",
                     "icon": "person",
                     "validators": [
-                        ("aleksis.core.util.predicates.permission_validator", "chronos.view_my_timetable"),
+                        (
+                            "aleksis.core.util.predicates.permission_validator",
+                            "chronos.view_my_timetable",
+                        ),
                     ],
                 },
                 {
@@ -25,7 +28,10 @@ MENUS = {
                     "url": "all_timetables",
                     "icon": "grid_on",
                     "validators": [
-                        ("aleksis.core.util.predicates.permission_validator", "chronos.view_timetable_overview"),
+                        (
+                            "aleksis.core.util.predicates.permission_validator",
+                            "chronos.view_timetable_overview",
+                        ),
                     ],
                 },
                 {
@@ -33,7 +39,10 @@ MENUS = {
                     "url": "lessons_day",
                     "icon": "calendar_today",
                     "validators": [
-                        ("aleksis.core.util.predicates.permission_validator", "chronos.view_lessons_day"),
+                        (
+                            "aleksis.core.util.predicates.permission_validator",
+                            "chronos.view_lessons_day",
+                        ),
                     ],
                 },
                 {
@@ -41,7 +50,10 @@ MENUS = {
                     "url": "substitutions",
                     "icon": "update",
                     "validators": [
-                        ("aleksis.core.util.predicates.permission_validator", "chronos.view_substitutions"),
+                        (
+                            "aleksis.core.util.predicates.permission_validator",
+                            "chronos.view_substitutions",
+                        ),
                     ],
                 },
             ],
diff --git a/aleksis/apps/chronos/migrations/0005_remove_school_related.py b/aleksis/apps/chronos/migrations/0005_remove_school_related.py
index f2ebba33ac96bb24ffe4e866919ebf8cb3edfe2a..7d3ba123a3c1f91565b61957f29594e705007b1c 100644
--- a/aleksis/apps/chronos/migrations/0005_remove_school_related.py
+++ b/aleksis/apps/chronos/migrations/0005_remove_school_related.py
@@ -6,69 +6,53 @@ from django.db import migrations, models
 class Migration(migrations.Migration):
 
     dependencies = [
-        ('core', '0008_rename_fields_notification_activity'),
-        ('chronos', '0004_room_name_not_unique'),
+        ("core", "0008_rename_fields_notification_activity"),
+        ("chronos", "0004_room_name_not_unique"),
     ]
 
     operations = [
-        migrations.RemoveField(
-            model_name='lesson',
-            name='school',
-        ),
-        migrations.RemoveField(
-            model_name='lessonperiod',
-            name='school',
-        ),
+        migrations.RemoveField(model_name="lesson", name="school",),
+        migrations.RemoveField(model_name="lessonperiod", name="school",),
         migrations.AlterField(
-            model_name='lesson',
-            name='teachers',
-            field=models.ManyToManyField(related_name='lessons_as_teacher', to='core.Person'),
+            model_name="lesson",
+            name="teachers",
+            field=models.ManyToManyField(
+                related_name="lessons_as_teacher", to="core.Person"
+            ),
         ),
         migrations.AlterField(
-            model_name='room',
-            name='short_name',
-            field=models.CharField(max_length=10, unique=True, verbose_name='Short name, e.g. room number'),
+            model_name="room",
+            name="short_name",
+            field=models.CharField(
+                max_length=10, unique=True, verbose_name="Short name, e.g. room number"
+            ),
         ),
         migrations.AlterField(
-            model_name='subject',
-            name='abbrev',
-            field=models.CharField(max_length=10, unique=True, verbose_name='Abbreviation of subject in timetable'),
+            model_name="subject",
+            name="abbrev",
+            field=models.CharField(
+                max_length=10,
+                unique=True,
+                verbose_name="Abbreviation of subject in timetable",
+            ),
         ),
         migrations.AlterField(
-            model_name='subject',
-            name='name',
-            field=models.CharField(max_length=30, unique=True, verbose_name='Long name of subject'),
-        ),
-        migrations.AlterUniqueTogether(
-            name='lessonsubstitution',
-            unique_together={('lesson_period', 'week')},
-        ),
-        migrations.AlterUniqueTogether(
-            name='room',
-            unique_together=set(),
+            model_name="subject",
+            name="name",
+            field=models.CharField(
+                max_length=30, unique=True, verbose_name="Long name of subject"
+            ),
         ),
         migrations.AlterUniqueTogether(
-            name='subject',
-            unique_together=set(),
+            name="lessonsubstitution", unique_together={("lesson_period", "week")},
         ),
+        migrations.AlterUniqueTogether(name="room", unique_together=set(),),
+        migrations.AlterUniqueTogether(name="subject", unique_together=set(),),
         migrations.AlterUniqueTogether(
-            name='timeperiod',
-            unique_together={('weekday', 'period')},
-        ),
-        migrations.RemoveField(
-            model_name='lessonsubstitution',
-            name='school',
-        ),
-        migrations.RemoveField(
-            model_name='room',
-            name='school',
-        ),
-        migrations.RemoveField(
-            model_name='subject',
-            name='school',
-        ),
-        migrations.RemoveField(
-            model_name='timeperiod',
-            name='school',
+            name="timeperiod", unique_together={("weekday", "period")},
         ),
+        migrations.RemoveField(model_name="lessonsubstitution", name="school",),
+        migrations.RemoveField(model_name="room", name="school",),
+        migrations.RemoveField(model_name="subject", name="school",),
+        migrations.RemoveField(model_name="timeperiod", name="school",),
     ]
diff --git a/aleksis/apps/chronos/migrations/0006_extended_data.py b/aleksis/apps/chronos/migrations/0006_extended_data.py
index 71785e3f7b61e8445ce2a5425952a13be3227552..6cbea6402c3a9d5d0dedf97cb40fc11ebe9e5115 100644
--- a/aleksis/apps/chronos/migrations/0006_extended_data.py
+++ b/aleksis/apps/chronos/migrations/0006_extended_data.py
@@ -7,13 +7,15 @@ from django.db import migrations, models
 class Migration(migrations.Migration):
 
     dependencies = [
-        ('chronos', '0005_remove_school_related'),
+        ("chronos", "0005_remove_school_related"),
     ]
 
     operations = [
         migrations.AddField(
-            model_name='lessonperiod',
-            name='extended_data',
-            field=django.contrib.postgres.fields.jsonb.JSONField(default=dict, editable=False),
+            model_name="lessonperiod",
+            name="extended_data",
+            field=django.contrib.postgres.fields.jsonb.JSONField(
+                default=dict, editable=False
+            ),
         ),
     ]
diff --git a/aleksis/apps/chronos/migrations/0007_advanced_models_from_untis.py b/aleksis/apps/chronos/migrations/0007_advanced_models_from_untis.py
index 87b517a8beb8d3ee4dd097981ed53f95cc2b4ad4..ccefed73c6c2cb1be14b4aa9acf87f447da97b08 100644
--- a/aleksis/apps/chronos/migrations/0007_advanced_models_from_untis.py
+++ b/aleksis/apps/chronos/migrations/0007_advanced_models_from_untis.py
@@ -9,272 +9,545 @@ import django.db.models.deletion
 class Migration(migrations.Migration):
 
     dependencies = [
-        ('chronos', '0006_extended_data'),
+        ("chronos", "0006_extended_data"),
     ]
 
     operations = [
         migrations.CreateModel(
-            name='Absence',
+            name="Absence",
             fields=[
-                ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
-                ('extended_data', django.contrib.postgres.fields.jsonb.JSONField(default=dict, editable=False)),
-                ('date_start', models.DateField(null=True, verbose_name='Effective start date of absence')),
-                ('date_end', models.DateField(null=True, verbose_name='Effective end date of absence')),
-                ('comment', models.TextField(verbose_name='Comment', null=True, blank=True)),
+                (
+                    "id",
+                    models.AutoField(
+                        auto_created=True,
+                        primary_key=True,
+                        serialize=False,
+                        verbose_name="ID",
+                    ),
+                ),
+                (
+                    "extended_data",
+                    django.contrib.postgres.fields.jsonb.JSONField(
+                        default=dict, editable=False
+                    ),
+                ),
+                (
+                    "date_start",
+                    models.DateField(
+                        null=True, verbose_name="Effective start date of absence"
+                    ),
+                ),
+                (
+                    "date_end",
+                    models.DateField(
+                        null=True, verbose_name="Effective end date of absence"
+                    ),
+                ),
+                (
+                    "comment",
+                    models.TextField(verbose_name="Comment", null=True, blank=True),
+                ),
             ],
             options={
-                'verbose_name': 'Absence',
-                'verbose_name_plural': 'Absences',
-                'ordering': ['date_start'],
+                "verbose_name": "Absence",
+                "verbose_name_plural": "Absences",
+                "ordering": ["date_start"],
             },
         ),
         migrations.CreateModel(
-            name='AbsenceReason',
+            name="AbsenceReason",
             fields=[
-                ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
-                ('extended_data', django.contrib.postgres.fields.jsonb.JSONField(default=dict, editable=False)),
-                ('title', models.CharField(max_length=50, verbose_name='Title')),
-                ('description', models.TextField(verbose_name='Description', null=True, blank=True)),
+                (
+                    "id",
+                    models.AutoField(
+                        auto_created=True,
+                        primary_key=True,
+                        serialize=False,
+                        verbose_name="ID",
+                    ),
+                ),
+                (
+                    "extended_data",
+                    django.contrib.postgres.fields.jsonb.JSONField(
+                        default=dict, editable=False
+                    ),
+                ),
+                ("title", models.CharField(max_length=50, verbose_name="Title")),
+                (
+                    "description",
+                    models.TextField(verbose_name="Description", null=True, blank=True),
+                ),
             ],
             options={
-                'verbose_name': 'Absence reason',
-                'verbose_name_plural': 'Absence reasons',
+                "verbose_name": "Absence reason",
+                "verbose_name_plural": "Absence reasons",
             },
         ),
         migrations.CreateModel(
-            name='Event',
+            name="Event",
             fields=[
-                ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
-                ('extended_data', django.contrib.postgres.fields.jsonb.JSONField(default=dict, editable=False)),
-                ('title', models.CharField(max_length=50, verbose_name='Title')),
-                ('date_start', models.DateField(null=True, verbose_name='Effective start date of event')),
-                ('date_end', models.DateField(null=True, verbose_name='Effective end date of event')),
+                (
+                    "id",
+                    models.AutoField(
+                        auto_created=True,
+                        primary_key=True,
+                        serialize=False,
+                        verbose_name="ID",
+                    ),
+                ),
+                (
+                    "extended_data",
+                    django.contrib.postgres.fields.jsonb.JSONField(
+                        default=dict, editable=False
+                    ),
+                ),
+                ("title", models.CharField(max_length=50, verbose_name="Title")),
+                (
+                    "date_start",
+                    models.DateField(
+                        null=True, verbose_name="Effective start date of event"
+                    ),
+                ),
+                (
+                    "date_end",
+                    models.DateField(
+                        null=True, verbose_name="Effective end date of event"
+                    ),
+                ),
             ],
             options={
-                'verbose_name': 'Events',
-                'verbose_name_plural': 'Events',
-                'ordering': ['date_start'],
+                "verbose_name": "Events",
+                "verbose_name_plural": "Events",
+                "ordering": ["date_start"],
             },
         ),
         migrations.CreateModel(
-            name='Exam',
+            name="Exam",
             fields=[
-                ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
-                ('extended_data', django.contrib.postgres.fields.jsonb.JSONField(default=dict, editable=False)),
-                ('date', models.DateField(null=True, verbose_name='Date of exam')),
-                ('title', models.CharField(max_length=50, verbose_name='Title')),
-                ('comment', models.TextField(verbose_name='Comment', null=True, blank=True)),
+                (
+                    "id",
+                    models.AutoField(
+                        auto_created=True,
+                        primary_key=True,
+                        serialize=False,
+                        verbose_name="ID",
+                    ),
+                ),
+                (
+                    "extended_data",
+                    django.contrib.postgres.fields.jsonb.JSONField(
+                        default=dict, editable=False
+                    ),
+                ),
+                ("date", models.DateField(null=True, verbose_name="Date of exam")),
+                ("title", models.CharField(max_length=50, verbose_name="Title")),
+                (
+                    "comment",
+                    models.TextField(verbose_name="Comment", null=True, blank=True),
+                ),
             ],
             options={
-                'verbose_name': 'Exam',
-                'verbose_name_plural': 'Exams',
-                'ordering': ['date'],
+                "verbose_name": "Exam",
+                "verbose_name_plural": "Exams",
+                "ordering": ["date"],
             },
         ),
         migrations.CreateModel(
-            name='Holiday',
+            name="Holiday",
             fields=[
-                ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
-                ('extended_data', django.contrib.postgres.fields.jsonb.JSONField(default=dict, editable=False)),
-                ('title', models.CharField(max_length=50, verbose_name='Title of the holidays')),
-                ('date_start', models.DateField(null=True, verbose_name='Effective start date of holidays')),
-                ('date_end', models.DateField(null=True, verbose_name='Effective end date of holidays')),
-                ('comments', models.TextField(verbose_name='Comments', null=True, blank=True)),
+                (
+                    "id",
+                    models.AutoField(
+                        auto_created=True,
+                        primary_key=True,
+                        serialize=False,
+                        verbose_name="ID",
+                    ),
+                ),
+                (
+                    "extended_data",
+                    django.contrib.postgres.fields.jsonb.JSONField(
+                        default=dict, editable=False
+                    ),
+                ),
+                (
+                    "title",
+                    models.CharField(
+                        max_length=50, verbose_name="Title of the holidays"
+                    ),
+                ),
+                (
+                    "date_start",
+                    models.DateField(
+                        null=True, verbose_name="Effective start date of holidays"
+                    ),
+                ),
+                (
+                    "date_end",
+                    models.DateField(
+                        null=True, verbose_name="Effective end date of holidays"
+                    ),
+                ),
+                (
+                    "comments",
+                    models.TextField(verbose_name="Comments", null=True, blank=True),
+                ),
             ],
             options={
-                'verbose_name': 'Holiday',
-                'verbose_name_plural': 'Holidays',
-                'ordering': ['date_start'],
+                "verbose_name": "Holiday",
+                "verbose_name_plural": "Holidays",
+                "ordering": ["date_start"],
             },
         ),
         migrations.CreateModel(
-            name='SupervisionArea',
+            name="SupervisionArea",
             fields=[
-                ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
-                ('extended_data', django.contrib.postgres.fields.jsonb.JSONField(default=dict, editable=False)),
-                ('short_name', models.CharField(max_length=10, verbose_name='Short name')),
-                ('name', models.CharField(max_length=50, verbose_name='Long name')),
-                ('colour_fg', colorfield.fields.ColorField(default='#000000', max_length=18)),
-                ('colour_bg', colorfield.fields.ColorField(default='#FFFFFF', max_length=18)),
+                (
+                    "id",
+                    models.AutoField(
+                        auto_created=True,
+                        primary_key=True,
+                        serialize=False,
+                        verbose_name="ID",
+                    ),
+                ),
+                (
+                    "extended_data",
+                    django.contrib.postgres.fields.jsonb.JSONField(
+                        default=dict, editable=False
+                    ),
+                ),
+                (
+                    "short_name",
+                    models.CharField(max_length=10, verbose_name="Short name"),
+                ),
+                ("name", models.CharField(max_length=50, verbose_name="Long name")),
+                (
+                    "colour_fg",
+                    colorfield.fields.ColorField(default="#000000", max_length=18),
+                ),
+                (
+                    "colour_bg",
+                    colorfield.fields.ColorField(default="#FFFFFF", max_length=18),
+                ),
             ],
             options={
-                'verbose_name': 'Supervision areas',
-                'verbose_name_plural': 'Supervision areas',
-                'ordering': ['name'],
+                "verbose_name": "Supervision areas",
+                "verbose_name_plural": "Supervision areas",
+                "ordering": ["name"],
             },
         ),
         migrations.CreateModel(
-            name='Break',
+            name="Break",
             fields=[
-                ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
-                ('extended_data', django.contrib.postgres.fields.jsonb.JSONField(default=dict, editable=False)),
-                ('short_name', models.CharField(max_length=10, verbose_name='Short name')),
-                ('name', models.CharField(max_length=50, verbose_name='Long name')),
-                ('weekday', models.PositiveSmallIntegerField(
-                    choices=[(0, 'Montag'), (1, 'Dienstag'), (2, 'Mittwoch'), (3, 'Donnerstag'), (4, 'Freitag'),
-                             (5, 'Samstag'), (6, 'Sonntag')], verbose_name='Week day')),
-                ('time_start', models.TimeField(verbose_name='Start time')),
-                ('time_end', models.TimeField(verbose_name='End time')),
+                (
+                    "id",
+                    models.AutoField(
+                        auto_created=True,
+                        primary_key=True,
+                        serialize=False,
+                        verbose_name="ID",
+                    ),
+                ),
+                (
+                    "extended_data",
+                    django.contrib.postgres.fields.jsonb.JSONField(
+                        default=dict, editable=False
+                    ),
+                ),
+                (
+                    "short_name",
+                    models.CharField(max_length=10, verbose_name="Short name"),
+                ),
+                ("name", models.CharField(max_length=50, verbose_name="Long name")),
+                (
+                    "weekday",
+                    models.PositiveSmallIntegerField(
+                        choices=[
+                            (0, "Montag"),
+                            (1, "Dienstag"),
+                            (2, "Mittwoch"),
+                            (3, "Donnerstag"),
+                            (4, "Freitag"),
+                            (5, "Samstag"),
+                            (6, "Sonntag"),
+                        ],
+                        verbose_name="Week day",
+                    ),
+                ),
+                ("time_start", models.TimeField(verbose_name="Start time")),
+                ("time_end", models.TimeField(verbose_name="End time")),
             ],
             options={
-                'verbose_name': 'Break',
-                'verbose_name_plural': 'Breaks',
-                'ordering': ['weekday', 'time_start'],
+                "verbose_name": "Break",
+                "verbose_name_plural": "Breaks",
+                "ordering": ["weekday", "time_start"],
             },
         ),
         migrations.CreateModel(
-            name='Supervision',
+            name="Supervision",
             fields=[
-                ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
-                ('extended_data', django.contrib.postgres.fields.jsonb.JSONField(default=dict, editable=False)),
+                (
+                    "id",
+                    models.AutoField(
+                        auto_created=True,
+                        primary_key=True,
+                        serialize=False,
+                        verbose_name="ID",
+                    ),
+                ),
+                (
+                    "extended_data",
+                    django.contrib.postgres.fields.jsonb.JSONField(
+                        default=dict, editable=False
+                    ),
+                ),
             ],
             options={
-                'verbose_name': 'Supervision',
-                'verbose_name_plural': 'Supervisions',
-                'ordering': ['area', 'break_item'],
+                "verbose_name": "Supervision",
+                "verbose_name_plural": "Supervisions",
+                "ordering": ["area", "break_item"],
             },
         ),
         migrations.CreateModel(
-            name='SupervisionSubstitution',
+            name="SupervisionSubstitution",
             fields=[
-                ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
-                ('extended_data', django.contrib.postgres.fields.jsonb.JSONField(default=dict, editable=False)),
-                ('date', models.DateField(verbose_name='Date')),
+                (
+                    "id",
+                    models.AutoField(
+                        auto_created=True,
+                        primary_key=True,
+                        serialize=False,
+                        verbose_name="ID",
+                    ),
+                ),
+                (
+                    "extended_data",
+                    django.contrib.postgres.fields.jsonb.JSONField(
+                        default=dict, editable=False
+                    ),
+                ),
+                ("date", models.DateField(verbose_name="Date")),
             ],
             options={
-                'verbose_name': 'Supervision substitution',
-                'verbose_name_plural': 'Supervision substitutions',
-                'ordering': ['date', 'supervision'],
+                "verbose_name": "Supervision substitution",
+                "verbose_name_plural": "Supervision substitutions",
+                "ordering": ["date", "supervision"],
             },
         ),
         migrations.AddIndex(
-            model_name='holiday',
-            index=models.Index(fields=['date_start', 'date_end'], name='chronos_hol_date_st_a47004_idx'),
+            model_name="holiday",
+            index=models.Index(
+                fields=["date_start", "date_end"], name="chronos_hol_date_st_a47004_idx"
+            ),
         ),
         migrations.AddField(
-            model_name='exam',
-            name='lesson',
-            field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='exams', to='chronos.Lesson'),
+            model_name="exam",
+            name="lesson",
+            field=models.ForeignKey(
+                on_delete=django.db.models.deletion.CASCADE,
+                related_name="exams",
+                to="chronos.Lesson",
+            ),
         ),
         migrations.AddField(
-            model_name='exam',
-            name='period_from',
-            field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, related_name='+', to='chronos.TimePeriod', verbose_name='Effective start period of exam'),
+            model_name="exam",
+            name="period_from",
+            field=models.ForeignKey(
+                null=True,
+                on_delete=django.db.models.deletion.CASCADE,
+                related_name="+",
+                to="chronos.TimePeriod",
+                verbose_name="Effective start period of exam",
+            ),
         ),
         migrations.AddField(
-            model_name='exam',
-            name='period_to',
-            field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, related_name='+', to='chronos.TimePeriod', verbose_name='Effective end period of exam'),
+            model_name="exam",
+            name="period_to",
+            field=models.ForeignKey(
+                null=True,
+                on_delete=django.db.models.deletion.CASCADE,
+                related_name="+",
+                to="chronos.TimePeriod",
+                verbose_name="Effective end period of exam",
+            ),
         ),
         migrations.AddField(
-            model_name='event',
-            name='absence_reason',
-            field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='absence_reason', to='chronos.AbsenceReason', verbose_name='Absence reason'),
+            model_name="event",
+            name="absence_reason",
+            field=models.ForeignKey(
+                on_delete=django.db.models.deletion.CASCADE,
+                related_name="absence_reason",
+                to="chronos.AbsenceReason",
+                verbose_name="Absence reason",
+            ),
         ),
         migrations.AddField(
-            model_name='event',
-            name='teachers',
-            field=models.ManyToManyField(related_name='events', to='core.Person', verbose_name='Teachers'),
+            model_name="event",
+            name="teachers",
+            field=models.ManyToManyField(
+                related_name="events", to="core.Person", verbose_name="Teachers"
+            ),
         ),
         migrations.AddField(
-            model_name='event',
-            name='period_from',
-            field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='+', to='chronos.TimePeriod', verbose_name='Effective start period of event'),
+            model_name="event",
+            name="period_from",
+            field=models.ForeignKey(
+                on_delete=django.db.models.deletion.CASCADE,
+                related_name="+",
+                to="chronos.TimePeriod",
+                verbose_name="Effective start period of event",
+            ),
         ),
         migrations.AddField(
-            model_name='event',
-            name='period_to',
-            field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='+', to='chronos.TimePeriod', verbose_name='Effective end period of event'),
+            model_name="event",
+            name="period_to",
+            field=models.ForeignKey(
+                on_delete=django.db.models.deletion.CASCADE,
+                related_name="+",
+                to="chronos.TimePeriod",
+                verbose_name="Effective end period of event",
+            ),
         ),
         migrations.AddField(
-            model_name='event',
-            name='rooms',
-            field=models.ManyToManyField(related_name='events', to='chronos.Room', verbose_name='Rooms'),
+            model_name="event",
+            name="rooms",
+            field=models.ManyToManyField(
+                related_name="events", to="chronos.Room", verbose_name="Rooms"
+            ),
         ),
         migrations.AddField(
-            model_name='absence',
-            name='period_from',
-            field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, related_name='+', to='chronos.TimePeriod', verbose_name='Effective start period of absence'),
+            model_name="absence",
+            name="period_from",
+            field=models.ForeignKey(
+                null=True,
+                on_delete=django.db.models.deletion.CASCADE,
+                related_name="+",
+                to="chronos.TimePeriod",
+                verbose_name="Effective start period of absence",
+            ),
         ),
         migrations.AddField(
-            model_name='absence',
-            name='period_to',
-            field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, related_name='+', to='chronos.TimePeriod', verbose_name='Effective end period of absence'),
+            model_name="absence",
+            name="period_to",
+            field=models.ForeignKey(
+                null=True,
+                on_delete=django.db.models.deletion.CASCADE,
+                related_name="+",
+                to="chronos.TimePeriod",
+                verbose_name="Effective end period of absence",
+            ),
         ),
         migrations.AddField(
-            model_name='absence',
-            name='person',
-            field=models.ManyToManyField(related_name='absences', to='core.Person'),
+            model_name="absence",
+            name="person",
+            field=models.ManyToManyField(related_name="absences", to="core.Person"),
         ),
         migrations.AddField(
-            model_name='absence',
-            name='reason',
-            field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='absences', to='chronos.AbsenceReason'),
+            model_name="absence",
+            name="reason",
+            field=models.ForeignKey(
+                on_delete=django.db.models.deletion.CASCADE,
+                related_name="absences",
+                to="chronos.AbsenceReason",
+            ),
         ),
         migrations.AddIndex(
-            model_name='exam',
-            index=models.Index(fields=['date'], name='chronos_exa_date_5ba442_idx'),
+            model_name="exam",
+            index=models.Index(fields=["date"], name="chronos_exa_date_5ba442_idx"),
         ),
         migrations.AddIndex(
-            model_name='event',
-            index=models.Index(fields=['period_from', 'period_to', 'date_start', 'date_end'], name='chronos_eve_periodf_56eb18_idx'),
+            model_name="event",
+            index=models.Index(
+                fields=["period_from", "period_to", "date_start", "date_end"],
+                name="chronos_eve_periodf_56eb18_idx",
+            ),
         ),
         migrations.AddIndex(
-            model_name='absence',
-            index=models.Index(fields=['date_start', 'date_end'], name='chronos_abs_date_st_337ff5_idx'),
+            model_name="absence",
+            index=models.Index(
+                fields=["date_start", "date_end"], name="chronos_abs_date_st_337ff5_idx"
+            ),
         ),
         migrations.AddField(
-            model_name='lessonsubstitution',
-            name='cancelled_for_teachers',
-            field=models.BooleanField(default=False, verbose_name='Cancelled for teachers?'),
+            model_name="lessonsubstitution",
+            name="cancelled_for_teachers",
+            field=models.BooleanField(
+                default=False, verbose_name="Cancelled for teachers?"
+            ),
         ),
         migrations.AddField(
-            model_name='lessonsubstitution',
-            name='comment',
-            field=models.TextField(blank=True, null=True, verbose_name='Comment'),
+            model_name="lessonsubstitution",
+            name="comment",
+            field=models.TextField(blank=True, null=True, verbose_name="Comment"),
         ),
         migrations.AlterField(
-            model_name='lessonsubstitution',
-            name='cancelled',
-            field=models.BooleanField(default=False, verbose_name='Cancelled?'),
+            model_name="lessonsubstitution",
+            name="cancelled",
+            field=models.BooleanField(default=False, verbose_name="Cancelled?"),
         ),
         migrations.AddField(
-            model_name='event',
-            name='groups',
-            field=models.ManyToManyField(related_name='events', to='core.Group', verbose_name='Groups'),
+            model_name="event",
+            name="groups",
+            field=models.ManyToManyField(
+                related_name="events", to="core.Group", verbose_name="Groups"
+            ),
         ),
         migrations.AddField(
-            model_name='supervisionsubstitution',
-            name='supervision',
-            field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='substitutions',
-                                    to='chronos.Supervision', verbose_name='Supervision'),
+            model_name="supervisionsubstitution",
+            name="supervision",
+            field=models.ForeignKey(
+                on_delete=django.db.models.deletion.CASCADE,
+                related_name="substitutions",
+                to="chronos.Supervision",
+                verbose_name="Supervision",
+            ),
         ),
         migrations.AddField(
-            model_name='supervisionsubstitution',
-            name='teacher',
-            field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE,
-                                    related_name='substituted_supervisions', to='core.Person', verbose_name='Teacher'),
+            model_name="supervisionsubstitution",
+            name="teacher",
+            field=models.ForeignKey(
+                on_delete=django.db.models.deletion.CASCADE,
+                related_name="substituted_supervisions",
+                to="core.Person",
+                verbose_name="Teacher",
+            ),
         ),
         migrations.AddField(
-            model_name='supervision',
-            name='area',
-            field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='supervisions',
-                                    to='chronos.SupervisionArea', verbose_name='Supervision area'),
+            model_name="supervision",
+            name="area",
+            field=models.ForeignKey(
+                on_delete=django.db.models.deletion.CASCADE,
+                related_name="supervisions",
+                to="chronos.SupervisionArea",
+                verbose_name="Supervision area",
+            ),
         ),
         migrations.AddField(
-            model_name='supervision',
-            name='break_item',
-            field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='supervisions',
-                                    to='chronos.Break', verbose_name='Break'),
+            model_name="supervision",
+            name="break_item",
+            field=models.ForeignKey(
+                on_delete=django.db.models.deletion.CASCADE,
+                related_name="supervisions",
+                to="chronos.Break",
+                verbose_name="Break",
+            ),
         ),
         migrations.AddField(
-            model_name='supervision',
-            name='teacher',
-            field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='supervisions',
-                                    to='core.Person', verbose_name='Teacher'),
+            model_name="supervision",
+            name="teacher",
+            field=models.ForeignKey(
+                on_delete=django.db.models.deletion.CASCADE,
+                related_name="supervisions",
+                to="core.Person",
+                verbose_name="Teacher",
+            ),
         ),
         migrations.AddIndex(
-            model_name='break',
-            index=models.Index(fields=['weekday', 'time_start', 'time_end'], name='chronos_bre_weekday_165338_idx'),
+            model_name="break",
+            index=models.Index(
+                fields=["weekday", "time_start", "time_end"],
+                name="chronos_bre_weekday_165338_idx",
+            ),
         ),
     ]
diff --git a/aleksis/apps/chronos/migrations/0008_break_period.py b/aleksis/apps/chronos/migrations/0008_break_period.py
index 740fd91b9d598255b02d92f095b86d0b12b04f17..971432ab9282bd68a8a4159d8f9306073d5c9ab0 100644
--- a/aleksis/apps/chronos/migrations/0008_break_period.py
+++ b/aleksis/apps/chronos/migrations/0008_break_period.py
@@ -7,38 +7,45 @@ import django.db.models.deletion
 class Migration(migrations.Migration):
 
     dependencies = [
-        ('chronos', '0007_advanced_models_from_untis'),
+        ("chronos", "0007_advanced_models_from_untis"),
     ]
 
     operations = [
         migrations.RemoveIndex(
-            model_name='break',
-            name='chronos_bre_weekday_165338_idx',
-        ),
-        migrations.RemoveField(
-            model_name='break',
-            name='time_end',
-        ),
-        migrations.RemoveField(
-            model_name='break',
-            name='time_start',
-        ),
-        migrations.RemoveField(
-            model_name='break',
-            name='weekday',
+            model_name="break", name="chronos_bre_weekday_165338_idx",
         ),
+        migrations.RemoveField(model_name="break", name="time_end",),
+        migrations.RemoveField(model_name="break", name="time_start",),
+        migrations.RemoveField(model_name="break", name="weekday",),
         migrations.AddField(
-            model_name='break',
-            name='after_period',
-            field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='break_after', to='chronos.TimePeriod', verbose_name='Effective start of break'),
+            model_name="break",
+            name="after_period",
+            field=models.ForeignKey(
+                blank=True,
+                null=True,
+                on_delete=django.db.models.deletion.CASCADE,
+                related_name="break_after",
+                to="chronos.TimePeriod",
+                verbose_name="Effective start of break",
+            ),
         ),
         migrations.AddField(
-            model_name='break',
-            name='before_period',
-            field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='break_before', to='chronos.TimePeriod', verbose_name='Effective end of break'),
+            model_name="break",
+            name="before_period",
+            field=models.ForeignKey(
+                blank=True,
+                null=True,
+                on_delete=django.db.models.deletion.CASCADE,
+                related_name="break_before",
+                to="chronos.TimePeriod",
+                verbose_name="Effective end of break",
+            ),
         ),
         migrations.AddIndex(
-            model_name='break',
-            index=models.Index(fields=['after_period', 'before_period'], name='chronos_bre_after_p_0f28d3_idx'),
+            model_name="break",
+            index=models.Index(
+                fields=["after_period", "before_period"],
+                name="chronos_bre_after_p_0f28d3_idx",
+            ),
         ),
     ]
diff --git a/aleksis/apps/chronos/migrations/0009_extended_data.py b/aleksis/apps/chronos/migrations/0009_extended_data.py
index 6c8ff26421436d260ca295103a02e85e6960dd67..ed65071f8878776a9e767ab09a9c49e069c4a06a 100644
--- a/aleksis/apps/chronos/migrations/0009_extended_data.py
+++ b/aleksis/apps/chronos/migrations/0009_extended_data.py
@@ -7,33 +7,43 @@ from django.db import migrations, models
 class Migration(migrations.Migration):
 
     dependencies = [
-        ('chronos', '0008_break_period'),
+        ("chronos", "0008_break_period"),
     ]
 
     operations = [
         migrations.AddField(
-            model_name='lesson',
-            name='extended_data',
-            field=django.contrib.postgres.fields.jsonb.JSONField(default=dict, editable=False),
+            model_name="lesson",
+            name="extended_data",
+            field=django.contrib.postgres.fields.jsonb.JSONField(
+                default=dict, editable=False
+            ),
         ),
         migrations.AddField(
-            model_name='lessonsubstitution',
-            name='extended_data',
-            field=django.contrib.postgres.fields.jsonb.JSONField(default=dict, editable=False),
+            model_name="lessonsubstitution",
+            name="extended_data",
+            field=django.contrib.postgres.fields.jsonb.JSONField(
+                default=dict, editable=False
+            ),
         ),
         migrations.AddField(
-            model_name='room',
-            name='extended_data',
-            field=django.contrib.postgres.fields.jsonb.JSONField(default=dict, editable=False),
+            model_name="room",
+            name="extended_data",
+            field=django.contrib.postgres.fields.jsonb.JSONField(
+                default=dict, editable=False
+            ),
         ),
         migrations.AddField(
-            model_name='subject',
-            name='extended_data',
-            field=django.contrib.postgres.fields.jsonb.JSONField(default=dict, editable=False),
+            model_name="subject",
+            name="extended_data",
+            field=django.contrib.postgres.fields.jsonb.JSONField(
+                default=dict, editable=False
+            ),
         ),
         migrations.AddField(
-            model_name='timeperiod',
-            name='extended_data',
-            field=django.contrib.postgres.fields.jsonb.JSONField(default=dict, editable=False),
+            model_name="timeperiod",
+            name="extended_data",
+            field=django.contrib.postgres.fields.jsonb.JSONField(
+                default=dict, editable=False
+            ),
         ),
     ]
diff --git a/aleksis/apps/chronos/migrations/0010_absence_reason_name.py b/aleksis/apps/chronos/migrations/0010_absence_reason_name.py
index 7c57327a2e19c8ff2865d14fabcd65acd317146f..95f05e8b63035b6b8b1f9b5b8fcba620647efb01 100644
--- a/aleksis/apps/chronos/migrations/0010_absence_reason_name.py
+++ b/aleksis/apps/chronos/migrations/0010_absence_reason_name.py
@@ -7,27 +7,29 @@ from django.db.models import F
 class Migration(migrations.Migration):
 
     dependencies = [
-        ('chronos', '0009_extended_data'),
+        ("chronos", "0009_extended_data"),
     ]
 
     operations = [
         migrations.AddField(
-            model_name='absencereason',
-            name='name',
-            field=models.CharField(default=F("description"), blank=True, max_length=255, null=True, verbose_name='Name'),
+            model_name="absencereason",
+            name="name",
+            field=models.CharField(
+                default=F("description"),
+                blank=True,
+                max_length=255,
+                null=True,
+                verbose_name="Name",
+            ),
         ),
         migrations.AddField(
-            model_name='absencereason',
-            name='short_name',
-            field=models.CharField(default=F("title"), max_length=255, verbose_name='Short name'),
+            model_name="absencereason",
+            name="short_name",
+            field=models.CharField(
+                default=F("title"), max_length=255, verbose_name="Short name"
+            ),
             preserve_default=False,
         ),
-        migrations.RemoveField(
-            model_name='absencereason',
-            name='description',
-        ),
-        migrations.RemoveField(
-            model_name='absencereason',
-            name='title',
-        ),
+        migrations.RemoveField(model_name="absencereason", name="description",),
+        migrations.RemoveField(model_name="absencereason", name="title",),
     ]
diff --git a/aleksis/apps/chronos/migrations/0011_absence_for_groups_and_rooms.py b/aleksis/apps/chronos/migrations/0011_absence_for_groups_and_rooms.py
index 008a723af7c68f96dbff9f8b149fb3c36305028e..cf7df94cb62f676c1e25188e086bf9460483dcd3 100644
--- a/aleksis/apps/chronos/migrations/0011_absence_for_groups_and_rooms.py
+++ b/aleksis/apps/chronos/migrations/0011_absence_for_groups_and_rooms.py
@@ -7,27 +7,42 @@ import django.db.models.deletion
 class Migration(migrations.Migration):
 
     dependencies = [
-        ('chronos', '0010_absence_reason_name'),
+        ("chronos", "0010_absence_reason_name"),
     ]
 
     operations = [
-        migrations.RemoveField(
-            model_name='absence',
-            name='person',
-        ),
+        migrations.RemoveField(model_name="absence", name="person",),
         migrations.AddField(
-            model_name='absence',
-            name='group',
-            field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='absences', to='core.Group'),
+            model_name="absence",
+            name="group",
+            field=models.ForeignKey(
+                blank=True,
+                null=True,
+                on_delete=django.db.models.deletion.CASCADE,
+                related_name="absences",
+                to="core.Group",
+            ),
         ),
         migrations.AddField(
-            model_name='absence',
-            name='room',
-            field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='absences', to='chronos.Room'),
+            model_name="absence",
+            name="room",
+            field=models.ForeignKey(
+                blank=True,
+                null=True,
+                on_delete=django.db.models.deletion.CASCADE,
+                related_name="absences",
+                to="chronos.Room",
+            ),
         ),
         migrations.AddField(
-            model_name='absence',
-            name='teacher',
-            field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='absences', to='core.Person'),
+            model_name="absence",
+            name="teacher",
+            field=models.ForeignKey(
+                blank=True,
+                null=True,
+                on_delete=django.db.models.deletion.CASCADE,
+                related_name="absences",
+                to="core.Person",
+            ),
         ),
     ]
diff --git a/aleksis/apps/chronos/migrations/0012_event_remove_absence_reason.py b/aleksis/apps/chronos/migrations/0012_event_remove_absence_reason.py
index 4e6611963aab5f79179b0489ca4bd41d25bd4433..5bdaaee5e09b316ff0531c041a7ee79d392fe432 100644
--- a/aleksis/apps/chronos/migrations/0012_event_remove_absence_reason.py
+++ b/aleksis/apps/chronos/migrations/0012_event_remove_absence_reason.py
@@ -6,12 +6,9 @@ from django.db import migrations, models
 class Migration(migrations.Migration):
 
     dependencies = [
-        ('chronos', '0011_absence_for_groups_and_rooms'),
+        ("chronos", "0011_absence_for_groups_and_rooms"),
     ]
 
     operations = [
-        migrations.RemoveField(
-            model_name='event',
-            name='absence_reason',
-        ),
+        migrations.RemoveField(model_name="event", name="absence_reason",),
     ]
diff --git a/aleksis/apps/chronos/migrations/0013_event_title_optional.py b/aleksis/apps/chronos/migrations/0013_event_title_optional.py
index 4bc05c39f8307ca056d03102c8709c018a432f63..6f18311e5450a155dcab6c38a4efffeab878c3e1 100644
--- a/aleksis/apps/chronos/migrations/0013_event_title_optional.py
+++ b/aleksis/apps/chronos/migrations/0013_event_title_optional.py
@@ -6,13 +6,15 @@ from django.db import migrations, models
 class Migration(migrations.Migration):
 
     dependencies = [
-        ('chronos', '0012_event_remove_absence_reason'),
+        ("chronos", "0012_event_remove_absence_reason"),
     ]
 
     operations = [
         migrations.AlterField(
-            model_name='event',
-            name='title',
-            field=models.CharField(blank=True, max_length=255, null=True, verbose_name='Title'),
+            model_name="event",
+            name="title",
+            field=models.CharField(
+                blank=True, max_length=255, null=True, verbose_name="Title"
+            ),
         ),
     ]
diff --git a/aleksis/apps/chronos/migrations/0014_extra_lesson.py b/aleksis/apps/chronos/migrations/0014_extra_lesson.py
index b0610a5ed76763d087d33ddbdb74581732bc9e78..d3a497d90560ff263a5e15d01e1e25d017432e19 100644
--- a/aleksis/apps/chronos/migrations/0014_extra_lesson.py
+++ b/aleksis/apps/chronos/migrations/0014_extra_lesson.py
@@ -9,45 +9,88 @@ import django.db.models.deletion
 class Migration(migrations.Migration):
 
     dependencies = [
-        ('chronos', '0013_event_title_optional'),
+        ("chronos", "0013_event_title_optional"),
     ]
 
     operations = [
         migrations.CreateModel(
-            name='ExtraLesson',
+            name="ExtraLesson",
             fields=[
-                ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
-                ('extended_data', django.contrib.postgres.fields.jsonb.JSONField(default=dict, editable=False)),
-                ('week', models.IntegerField(default=calendarweek.calendarweek.CalendarWeek.current_week, verbose_name='Week')),
-                ('comment', models.CharField(blank=True, max_length=255, null=True, verbose_name='Comment')),
+                (
+                    "id",
+                    models.AutoField(
+                        auto_created=True,
+                        primary_key=True,
+                        serialize=False,
+                        verbose_name="ID",
+                    ),
+                ),
+                (
+                    "extended_data",
+                    django.contrib.postgres.fields.jsonb.JSONField(
+                        default=dict, editable=False
+                    ),
+                ),
+                (
+                    "week",
+                    models.IntegerField(
+                        default=calendarweek.calendarweek.CalendarWeek.current_week,
+                        verbose_name="Week",
+                    ),
+                ),
+                (
+                    "comment",
+                    models.CharField(
+                        blank=True, max_length=255, null=True, verbose_name="Comment"
+                    ),
+                ),
             ],
-            options={
-                'abstract': False,
-            },
+            options={"abstract": False,},
         ),
         migrations.AddField(
-            model_name='extralesson',
-            name='groups',
-            field=models.ManyToManyField(related_name='extra_lessons', to='core.Group', verbose_name='Groups'),
+            model_name="extralesson",
+            name="groups",
+            field=models.ManyToManyField(
+                related_name="extra_lessons", to="core.Group", verbose_name="Groups"
+            ),
         ),
         migrations.AddField(
-            model_name='extralesson',
-            name='period',
-            field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='extra_lessons', to='chronos.TimePeriod'),
+            model_name="extralesson",
+            name="period",
+            field=models.ForeignKey(
+                on_delete=django.db.models.deletion.CASCADE,
+                related_name="extra_lessons",
+                to="chronos.TimePeriod",
+            ),
         ),
         migrations.AddField(
-            model_name='extralesson',
-            name='room',
-            field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, related_name='extra_lessons', to='chronos.Room', verbose_name='Room'),
+            model_name="extralesson",
+            name="room",
+            field=models.ForeignKey(
+                null=True,
+                on_delete=django.db.models.deletion.CASCADE,
+                related_name="extra_lessons",
+                to="chronos.Room",
+                verbose_name="Room",
+            ),
         ),
         migrations.AddField(
-            model_name='extralesson',
-            name='subject',
-            field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='extra_lessons', to='chronos.Subject', verbose_name='Subject'),
+            model_name="extralesson",
+            name="subject",
+            field=models.ForeignKey(
+                on_delete=django.db.models.deletion.CASCADE,
+                related_name="extra_lessons",
+                to="chronos.Subject",
+                verbose_name="Subject",
+            ),
         ),
         migrations.AddField(
-            model_name='extralesson',
-            name='teachers',
-            field=models.ManyToManyField(related_name='extra_lessons_as_teacher', to='core.Person', verbose_name='Teachers'),
+            model_name="extralesson",
+            name="teachers",
+            field=models.ManyToManyField(
+                related_name="extra_lessons_as_teacher",
+                to="core.Person",
+                verbose_name="Teachers",
+            ),
         ),
     ]
diff --git a/aleksis/apps/chronos/migrations/0015_rename_abbrev_to_short_name.py b/aleksis/apps/chronos/migrations/0015_rename_abbrev_to_short_name.py
index 5d79636e326cd65b552559aeb71422ae1bd0d2aa..4912dd0d5118f7938d2f2fb8346cd2f2d079bf38 100644
--- a/aleksis/apps/chronos/migrations/0015_rename_abbrev_to_short_name.py
+++ b/aleksis/apps/chronos/migrations/0015_rename_abbrev_to_short_name.py
@@ -7,17 +7,14 @@ from django.db.models import F
 class Migration(migrations.Migration):
 
     dependencies = [
-        ('chronos', '0014_extra_lesson'),
+        ("chronos", "0014_extra_lesson"),
     ]
 
     operations = [
         migrations.RenameField(
-            model_name='subject',
-            old_name='abbrev',
-            new_name='short_name',
+            model_name="subject", old_name="abbrev", new_name="short_name",
         ),
         migrations.AlterModelOptions(
-            name='subject',
-            options={'ordering': ['name', 'short_name']},
+            name="subject", options={"ordering": ["name", "short_name"]},
         ),
     ]
diff --git a/aleksis/apps/chronos/migrations/0016_add_globalpermissions.py b/aleksis/apps/chronos/migrations/0016_add_globalpermissions.py
index 785e2782c95b53321ee9940f289564ad2b495065..b43b2bf1061cb9f45143c87c67c07c0ee8fc907c 100644
--- a/aleksis/apps/chronos/migrations/0016_add_globalpermissions.py
+++ b/aleksis/apps/chronos/migrations/0016_add_globalpermissions.py
@@ -7,19 +7,36 @@ from django.db import migrations, models
 class Migration(migrations.Migration):
 
     dependencies = [
-        ('chronos', '0015_rename_abbrev_to_short_name'),
+        ("chronos", "0015_rename_abbrev_to_short_name"),
     ]
 
     operations = [
         migrations.CreateModel(
-            name='GlobalPermissions',
+            name="GlobalPermissions",
             fields=[
-                ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
-                ('extended_data', django.contrib.postgres.fields.jsonb.JSONField(default=dict, editable=False)),
+                (
+                    "id",
+                    models.AutoField(
+                        auto_created=True,
+                        primary_key=True,
+                        serialize=False,
+                        verbose_name="ID",
+                    ),
+                ),
+                (
+                    "extended_data",
+                    django.contrib.postgres.fields.jsonb.JSONField(
+                        default=dict, editable=False
+                    ),
+                ),
             ],
             options={
-                'permissions': (('view_all_timetables', 'Can view all timetables'), ('view_timetable_overview', 'Can view timetable overview'), ('view_lessons_day', 'Can view all lessons per day')),
-                'managed': False,
+                "permissions": (
+                    ("view_all_timetables", "Can view all timetables"),
+                    ("view_timetable_overview", "Can view timetable overview"),
+                    ("view_lessons_day", "Can view all lessons per day"),
+                ),
+                "managed": False,
             },
         ),
     ]
diff --git a/aleksis/apps/chronos/model_extensions.py b/aleksis/apps/chronos/model_extensions.py
index 1607bd241868ee6c9c147ab22a90e1ed4a03647e..2877278e87361654e6c40fa48d4c80e145b25f39 100644
--- a/aleksis/apps/chronos/model_extensions.py
+++ b/aleksis/apps/chronos/model_extensions.py
@@ -88,8 +88,13 @@ def lesson_periods_as_teacher(self):
 
 
 def for_timetables(cls):
+    """Return all announcements that should be shown in timetable views."""
     return cls.objects.filter(show_in_timetables=True)
 
 
 Announcement.class_method(for_timetables)
-Announcement.field(show_in_timetables=BooleanField(verbose_name=_("Show announcement in timetable views?")))
+Announcement.field(
+    show_in_timetables=BooleanField(
+        verbose_name=_("Show announcement in timetable views?")
+    )
+)
diff --git a/aleksis/apps/chronos/models.py b/aleksis/apps/chronos/models.py
index 556f3a1f4b40a4ea7c6e5a94757fe9d74c058b8a..595ba172eb3b2bf458cd83b372fd606df33c5dde 100644
--- a/aleksis/apps/chronos/models.py
+++ b/aleksis/apps/chronos/models.py
@@ -3,9 +3,19 @@ from __future__ import annotations
 from datetime import date, datetime, timedelta, time
 from typing import Dict, Optional, Tuple, Union
 
-from aleksis.apps.chronos.managers import GroupPropertiesMixin, TeacherPropertiesMixin, LessonSubstitutionManager, \
-    LessonSubstitutionQuerySet, LessonPeriodManager, LessonPeriodQuerySet, AbsenceQuerySet, HolidayQuerySet, \
-    SupervisionQuerySet, EventQuerySet, ExtraLessonQuerySet
+from aleksis.apps.chronos.managers import (
+    GroupPropertiesMixin,
+    TeacherPropertiesMixin,
+    LessonSubstitutionManager,
+    LessonSubstitutionQuerySet,
+    LessonPeriodManager,
+    LessonPeriodQuerySet,
+    AbsenceQuerySet,
+    HolidayQuerySet,
+    SupervisionQuerySet,
+    EventQuerySet,
+    ExtraLessonQuerySet,
+)
 from django.core.exceptions import ValidationError
 from django.db import models
 from django.db.models import Max, Min, Q
@@ -33,17 +43,16 @@ class TimePeriod(ExtensibleModel):
     WEEKDAY_CHOICES = list(enumerate(i18n_day_names_lazy()))
     WEEKDAY_CHOICES_SHORT = list(enumerate(i18n_day_abbrs_lazy()))
 
-    weekday = models.PositiveSmallIntegerField(verbose_name=_("Week day"), choices=WEEKDAY_CHOICES)
+    weekday = models.PositiveSmallIntegerField(
+        verbose_name=_("Week day"), choices=WEEKDAY_CHOICES
+    )
     period = models.PositiveSmallIntegerField(verbose_name=_("Number of period"))
 
     time_start = models.TimeField(verbose_name=_("Start time"))
     time_end = models.TimeField(verbose_name=_("End time"))
 
     def __str__(self) -> str:
-        return "{}, {}.".format(
-            self.get_weekday_display(),
-            self.period,
-        )
+        return "{}, {}.".format(self.get_weekday_display(), self.period,)
 
     @classmethod
     def get_times_dict(cls) -> Dict[int, Tuple[datetime, datetime]]:
@@ -68,7 +77,9 @@ class TimePeriod(ExtensibleModel):
         return wanted_week[self.weekday]
 
     @classmethod
-    def get_next_relevant_day(cls, day: Optional[date] = None, time: Optional[time] = None, prev: bool = False) -> date:
+    def get_next_relevant_day(
+        cls, day: Optional[date] = None, time: Optional[time] = None, prev: bool = False
+    ) -> date:
         """ Returns next (previous) day with lessons depending on date and time """
 
         if day is None:
@@ -109,11 +120,15 @@ class TimePeriod(ExtensibleModel):
 
     @classproperty
     def period_min(cls) -> int:
-        return cls.objects.aggregate(period__min=Coalesce(Min("period"), 1)).get("period__min")
+        return cls.objects.aggregate(period__min=Coalesce(Min("period"), 1)).get(
+            "period__min"
+        )
 
     @classproperty
     def period_max(cls) -> int:
-        return cls.objects.aggregate(period__max=Coalesce(Max("period"), 7)).get("period__max")
+        return cls.objects.aggregate(period__max=Coalesce(Max("period"), 7)).get(
+            "period__max"
+        )
 
     @classproperty
     def time_min(cls) -> Optional[time]:
@@ -125,11 +140,15 @@ class TimePeriod(ExtensibleModel):
 
     @classproperty
     def weekday_min(cls) -> int:
-        return cls.objects.aggregate(weekday__min=Coalesce(Min("weekday"), 0)).get("weekday__min")
+        return cls.objects.aggregate(weekday__min=Coalesce(Min("weekday"), 0)).get(
+            "weekday__min"
+        )
 
     @classproperty
     def weekday_max(cls) -> int:
-        return cls.objects.aggregate(weekday__max=Coalesce(Max("weekday"), 6)).get("weekday__max")
+        return cls.objects.aggregate(weekday__max=Coalesce(Max("weekday"), 6)).get(
+            "weekday__max"
+        )
 
     class Meta:
         unique_together = [["weekday", "period"]]
@@ -140,7 +159,9 @@ class TimePeriod(ExtensibleModel):
 
 
 class Subject(ExtensibleModel):
-    short_name = models.CharField(verbose_name=_("Short name"), max_length=255, unique=True)
+    short_name = models.CharField(
+        verbose_name=_("Short name"), max_length=255, unique=True
+    )
     name = models.CharField(verbose_name=_("Long name"), max_length=255, unique=True)
 
     colour_fg = ColorField(verbose_name=_("Foreground colour"), blank=True)
@@ -156,7 +177,9 @@ class Subject(ExtensibleModel):
 
 
 class Room(ExtensibleModel):
-    short_name = models.CharField(verbose_name=_("Short name"), max_length=255, unique=True)
+    short_name = models.CharField(
+        verbose_name=_("Short name"), max_length=255, unique=True
+    )
     name = models.CharField(verbose_name=_("Long name"), max_length=255)
 
     def __str__(self) -> str:
@@ -168,13 +191,25 @@ class Room(ExtensibleModel):
         verbose_name_plural = _("Rooms")
 
 
-
-
 class Lesson(ExtensibleModel, GroupPropertiesMixin, TeacherPropertiesMixin):
-    subject = models.ForeignKey("Subject", on_delete=models.CASCADE, related_name="lessons", verbose_name=_("Subject"))
-    teachers = models.ManyToManyField("core.Person", related_name="lessons_as_teacher", verbose_name=_("Teachers"))
-    periods = models.ManyToManyField("TimePeriod", related_name="lessons", through="LessonPeriod", verbose_name=_("Periods"))
-    groups = models.ManyToManyField("core.Group", related_name="lessons", verbose_name=_("Groups"))
+    subject = models.ForeignKey(
+        "Subject",
+        on_delete=models.CASCADE,
+        related_name="lessons",
+        verbose_name=_("Subject"),
+    )
+    teachers = models.ManyToManyField(
+        "core.Person", related_name="lessons_as_teacher", verbose_name=_("Teachers")
+    )
+    periods = models.ManyToManyField(
+        "TimePeriod",
+        related_name="lessons",
+        through="LessonPeriod",
+        verbose_name=_("Periods"),
+    )
+    groups = models.ManyToManyField(
+        "core.Group", related_name="lessons", verbose_name=_("Groups")
+    )
 
     date_start = models.DateField(verbose_name=_("Start date"), null=True)
     date_end = models.DateField(verbose_name=_("End date"), null=True)
@@ -188,9 +223,7 @@ class Lesson(ExtensibleModel, GroupPropertiesMixin, TeacherPropertiesMixin):
 
     def __str__(self):
         return "{}, {}, {}".format(
-            format_m2m(self.groups),
-            self.subject.short_name,
-            format_m2m(self.teachers),
+            format_m2m(self.groups), self.subject.short_name, format_m2m(self.teachers),
         )
 
     class Meta:
@@ -203,9 +236,13 @@ class Lesson(ExtensibleModel, GroupPropertiesMixin, TeacherPropertiesMixin):
 class LessonSubstitution(ExtensibleModel):
     objects = LessonSubstitutionManager.from_queryset(LessonSubstitutionQuerySet)()
 
-    week = models.IntegerField(verbose_name=_("Week"), default=CalendarWeek.current_week)
+    week = models.IntegerField(
+        verbose_name=_("Week"), default=CalendarWeek.current_week
+    )
 
-    lesson_period = models.ForeignKey("LessonPeriod", models.CASCADE, "substitutions", verbose_name=_("Lesson period"))
+    lesson_period = models.ForeignKey(
+        "LessonPeriod", models.CASCADE, "substitutions", verbose_name=_("Lesson period")
+    )
 
     subject = models.ForeignKey(
         "Subject",
@@ -216,18 +253,27 @@ class LessonSubstitution(ExtensibleModel):
         verbose_name=_("Subject"),
     )
     teachers = models.ManyToManyField(
-        "core.Person", related_name="lesson_substitutions", blank=True, verbose_name=_("Teachers")
+        "core.Person",
+        related_name="lesson_substitutions",
+        blank=True,
+        verbose_name=_("Teachers"),
+    )
+    room = models.ForeignKey(
+        "Room", models.CASCADE, null=True, blank=True, verbose_name=_("Room")
     )
-    room = models.ForeignKey("Room", models.CASCADE, null=True, blank=True, verbose_name=_("Room"))
 
     cancelled = models.BooleanField(default=False, verbose_name=_("Cancelled?"))
-    cancelled_for_teachers = models.BooleanField(default=False, verbose_name=_("Cancelled for teachers?"))
+    cancelled_for_teachers = models.BooleanField(
+        default=False, verbose_name=_("Cancelled for teachers?")
+    )
 
     comment = models.TextField(verbose_name=_("Comment"), blank=True, null=True)
 
     def clean(self) -> None:
         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.")
+            )
 
     @property
     def date(self):
@@ -260,10 +306,26 @@ class LessonPeriod(ExtensibleModel):
 
     objects = LessonPeriodManager.from_queryset(LessonPeriodQuerySet)()
 
-    lesson = models.ForeignKey("Lesson", models.CASCADE, related_name="lesson_periods", verbose_name=_("Lesson"))
-    period = models.ForeignKey("TimePeriod", models.CASCADE, related_name="lesson_periods", verbose_name=_("Time period"))
+    lesson = models.ForeignKey(
+        "Lesson",
+        models.CASCADE,
+        related_name="lesson_periods",
+        verbose_name=_("Lesson"),
+    )
+    period = models.ForeignKey(
+        "TimePeriod",
+        models.CASCADE,
+        related_name="lesson_periods",
+        verbose_name=_("Time period"),
+    )
 
-    room = models.ForeignKey("Room", models.CASCADE, null=True, related_name="lesson_periods", verbose_name=_("Room"))
+    room = models.ForeignKey(
+        "Room",
+        models.CASCADE,
+        null=True,
+        related_name="lesson_periods",
+        verbose_name=_("Room"),
+    )
 
     def get_substitution(self, week: Optional[int] = None) -> LessonSubstitution:
         wanted_week = week or getattr(self, "_week", None) or CalendarWeek().week
@@ -301,13 +363,15 @@ class LessonPeriod(ExtensibleModel):
         return self.lesson.groups
 
     def __str__(self) -> str:
-        return "{}, {}".format(
-            str(self.period),
-            str(self.lesson)
-        )
+        return "{}, {}".format(str(self.period), str(self.lesson))
 
     class Meta:
-        ordering = ["lesson__date_start", "period__weekday", "period__period", "lesson__subject"]
+        ordering = [
+            "lesson__date_start",
+            "period__weekday",
+            "period__period",
+            "lesson__subject",
+        ]
         indexes = [models.Index(fields=["lesson", "period"])]
         verbose_name = _("Lesson period")
         verbose_name_plural = _("Lesson periods")
@@ -317,11 +381,13 @@ class TimetableWidget(DashboardWidget):
     template = "chronos/widget.html"
 
     def get_context(self):
-        from aleksis.apps.chronos.util.build import build_timetable # noqa
+        from aleksis.apps.chronos.util.build import build_timetable  # noqa
 
         request = get_request()
         context = {"has_plan": True}
-        wanted_day = TimePeriod.get_next_relevant_day(timezone.now().date(), datetime.now().time())
+        wanted_day = TimePeriod.get_next_relevant_day(
+            timezone.now().date(), datetime.now().time()
+        )
 
         if has_person(request.user):
             person = request.user.person
@@ -345,9 +411,7 @@ class TimetableWidget(DashboardWidget):
 
         return context
 
-    media = Media(css={
-        "all": ("css/chronos/timetable.css",)
-    })
+    media = Media(css={"all": ("css/chronos/timetable.css",)})
 
     class Meta:
         proxy = True
@@ -357,7 +421,9 @@ class TimetableWidget(DashboardWidget):
 
 class AbsenceReason(ExtensibleModel):
     short_name = models.CharField(verbose_name=_("Short name"), max_length=255)
-    name = models.CharField(verbose_name=_("Name"), blank=True, null=True, max_length=255)
+    name = models.CharField(
+        verbose_name=_("Name"), blank=True, null=True, max_length=255
+    )
 
     def __str__(self):
         if self.name:
@@ -373,16 +439,56 @@ class AbsenceReason(ExtensibleModel):
 class Absence(ExtensibleModel):
     objects = models.Manager.from_queryset(AbsenceQuerySet)()
 
-    reason = models.ForeignKey("AbsenceReason", on_delete=models.SET_NULL, related_name="absences", blank=True, null=True, verbose_name=_("Absence reason"))
+    reason = models.ForeignKey(
+        "AbsenceReason",
+        on_delete=models.SET_NULL,
+        related_name="absences",
+        blank=True,
+        null=True,
+        verbose_name=_("Absence reason"),
+    )
 
-    teacher = models.ForeignKey("core.Person", on_delete=models.CASCADE, related_name="absences", null=True, blank=True, verbose_name=_("Teacher"))
-    group = models.ForeignKey("core.Group", on_delete=models.CASCADE, related_name="absences", null=True, blank=True, verbose_name=_("Group"))
-    room = models.ForeignKey("Room", on_delete=models.CASCADE, related_name="absences", null=True, blank=True, verbose_name=_("Room"))
+    teacher = models.ForeignKey(
+        "core.Person",
+        on_delete=models.CASCADE,
+        related_name="absences",
+        null=True,
+        blank=True,
+        verbose_name=_("Teacher"),
+    )
+    group = models.ForeignKey(
+        "core.Group",
+        on_delete=models.CASCADE,
+        related_name="absences",
+        null=True,
+        blank=True,
+        verbose_name=_("Group"),
+    )
+    room = models.ForeignKey(
+        "Room",
+        on_delete=models.CASCADE,
+        related_name="absences",
+        null=True,
+        blank=True,
+        verbose_name=_("Room"),
+    )
 
     date_start = models.DateField(verbose_name=_("Start date"), null=True)
     date_end = models.DateField(verbose_name=_("End date"), null=True)
-    period_from = models.ForeignKey("TimePeriod", on_delete=models.CASCADE, verbose_name=_("Start period"), null=True, related_name="+")
-    period_to = models.ForeignKey("TimePeriod", on_delete=models.CASCADE, verbose_name=_("End period"), null=True, related_name="+")
+    period_from = models.ForeignKey(
+        "TimePeriod",
+        on_delete=models.CASCADE,
+        verbose_name=_("Start period"),
+        null=True,
+        related_name="+",
+    )
+    period_to = models.ForeignKey(
+        "TimePeriod",
+        on_delete=models.CASCADE,
+        verbose_name=_("End period"),
+        null=True,
+        related_name="+",
+    )
     comment = models.TextField(verbose_name=_("Comment"), blank=True, null=True)
 
     def __str__(self):
@@ -403,11 +509,28 @@ class Absence(ExtensibleModel):
 
 
 class Exam(ExtensibleModel):
-    lesson = models.ForeignKey("Lesson", on_delete=models.CASCADE, related_name="exams", verbose_name=_("Lesson"))
+    lesson = models.ForeignKey(
+        "Lesson",
+        on_delete=models.CASCADE,
+        related_name="exams",
+        verbose_name=_("Lesson"),
+    )
 
     date = models.DateField(verbose_name=_("Date of exam"), null=True)
-    period_from = models.ForeignKey("TimePeriod", on_delete=models.CASCADE, verbose_name=_("Start period"), null=True, related_name="+")
-    period_to = models.ForeignKey("TimePeriod", on_delete=models.CASCADE, verbose_name=_("End period"), null=True, related_name="+")
+    period_from = models.ForeignKey(
+        "TimePeriod",
+        on_delete=models.CASCADE,
+        verbose_name=_("Start period"),
+        null=True,
+        related_name="+",
+    )
+    period_to = models.ForeignKey(
+        "TimePeriod",
+        on_delete=models.CASCADE,
+        verbose_name=_("End period"),
+        null=True,
+        related_name="+",
+    )
 
     title = models.CharField(verbose_name=_("Title"), max_length=255)
     comment = models.TextField(verbose_name=_("Comment"), blank=True, null=True)
@@ -476,12 +599,22 @@ class Break(ExtensibleModel):
     short_name = models.CharField(verbose_name=_("Short name"), max_length=255)
     name = models.CharField(verbose_name=_("Long name"), max_length=255)
 
-    after_period = models.ForeignKey("TimePeriod", on_delete=models.CASCADE,
-                                    verbose_name=_("Time period after break starts"),
-                                    related_name="break_after", blank=True, null=True)
-    before_period = models.ForeignKey("TimePeriod", on_delete=models.CASCADE,
-                                  verbose_name=_("Time period before break ends"),
-                                  related_name="break_before", blank=True, null=True)
+    after_period = models.ForeignKey(
+        "TimePeriod",
+        on_delete=models.CASCADE,
+        verbose_name=_("Time period after break starts"),
+        related_name="break_after",
+        blank=True,
+        null=True,
+    )
+    before_period = models.ForeignKey(
+        "TimePeriod",
+        on_delete=models.CASCADE,
+        verbose_name=_("Time period before break ends"),
+        related_name="break_before",
+        blank=True,
+        null=True,
+    )
 
     @property
     def weekday(self):
@@ -536,9 +669,21 @@ class Break(ExtensibleModel):
 class Supervision(ExtensibleModel):
     objects = models.Manager.from_queryset(SupervisionQuerySet)()
 
-    area = models.ForeignKey(SupervisionArea, models.CASCADE, verbose_name=_("Supervision area"), related_name="supervisions")
-    break_item = models.ForeignKey(Break, models.CASCADE, verbose_name=_("Break"), related_name="supervisions")
-    teacher = models.ForeignKey("core.Person", models.CASCADE, related_name="supervisions", verbose_name=_("Teacher"))
+    area = models.ForeignKey(
+        SupervisionArea,
+        models.CASCADE,
+        verbose_name=_("Supervision area"),
+        related_name="supervisions",
+    )
+    break_item = models.ForeignKey(
+        Break, models.CASCADE, verbose_name=_("Break"), related_name="supervisions"
+    )
+    teacher = models.ForeignKey(
+        "core.Person",
+        models.CASCADE,
+        related_name="supervisions",
+        verbose_name=_("Teacher"),
+    )
 
     def get_substitution(
         self, week: Optional[int] = None
@@ -563,14 +708,24 @@ class Supervision(ExtensibleModel):
 
     class Meta:
         ordering = ["area", "break_item"]
-        verbose_name= _("Supervision")
+        verbose_name = _("Supervision")
         verbose_name_plural = _("Supervisions")
 
 
 class SupervisionSubstitution(ExtensibleModel):
     date = models.DateField(verbose_name=_("Date"))
-    supervision = models.ForeignKey(Supervision, models.CASCADE, verbose_name=_("Supervision"), related_name="substitutions")
-    teacher = models.ForeignKey("core.Person", models.CASCADE, related_name="substituted_supervisions", verbose_name=_("Teacher"))
+    supervision = models.ForeignKey(
+        Supervision,
+        models.CASCADE,
+        verbose_name=_("Supervision"),
+        related_name="substitutions",
+    )
+    teacher = models.ForeignKey(
+        "core.Person",
+        models.CASCADE,
+        related_name="substituted_supervisions",
+        verbose_name=_("Teacher"),
+    )
 
     @property
     def teachers(self):
@@ -590,17 +745,35 @@ class Event(ExtensibleModel, GroupPropertiesMixin, TeacherPropertiesMixin):
 
     objects = models.Manager.from_queryset(EventQuerySet)()
 
-    title = models.CharField(verbose_name=_("Title"), max_length=255, blank=True, null=True)
+    title = models.CharField(
+        verbose_name=_("Title"), max_length=255, blank=True, null=True
+    )
 
     date_start = models.DateField(verbose_name=_("Start date"), null=True)
     date_end = models.DateField(verbose_name=_("End date"), null=True)
 
-    period_from = models.ForeignKey("TimePeriod", on_delete=models.CASCADE, verbose_name=_("Start time period"), related_name="+")
-    period_to = models.ForeignKey("TimePeriod", on_delete=models.CASCADE, verbose_name=_("End time period"), related_name="+")
+    period_from = models.ForeignKey(
+        "TimePeriod",
+        on_delete=models.CASCADE,
+        verbose_name=_("Start time period"),
+        related_name="+",
+    )
+    period_to = models.ForeignKey(
+        "TimePeriod",
+        on_delete=models.CASCADE,
+        verbose_name=_("End time period"),
+        related_name="+",
+    )
 
-    groups = models.ManyToManyField("core.Group", related_name="events", verbose_name=_("Groups"))
-    rooms = models.ManyToManyField("Room", related_name="events", verbose_name=_("Rooms"))
-    teachers = models.ManyToManyField("core.Person", related_name="events", verbose_name=_("Teachers"))
+    groups = models.ManyToManyField(
+        "core.Group", related_name="events", verbose_name=_("Groups")
+    )
+    rooms = models.ManyToManyField(
+        "Room", related_name="events", verbose_name=_("Rooms")
+    )
+    teachers = models.ManyToManyField(
+        "core.Person", related_name="events", verbose_name=_("Teachers")
+    )
 
     def __str__(self):
         if self.title:
@@ -626,7 +799,9 @@ class Event(ExtensibleModel, GroupPropertiesMixin, TeacherPropertiesMixin):
 
     class Meta:
         ordering = ["date_start"]
-        indexes = [models.Index(fields=["period_from", "period_to", "date_start", "date_end"])]
+        indexes = [
+            models.Index(fields=["period_from", "period_to", "date_start", "date_end"])
+        ]
         verbose_name = _("Event")
         verbose_name_plural = _("Events")
 
@@ -636,15 +811,41 @@ class ExtraLesson(ExtensibleModel, GroupPropertiesMixin):
 
     objects = models.Manager.from_queryset(ExtraLessonQuerySet)()
 
-    week = models.IntegerField(verbose_name=_("Week"), default=CalendarWeek.current_week)
-    period = models.ForeignKey("TimePeriod", models.CASCADE, related_name="extra_lessons", verbose_name=_("Time period"))
+    week = models.IntegerField(
+        verbose_name=_("Week"), default=CalendarWeek.current_week
+    )
+    period = models.ForeignKey(
+        "TimePeriod",
+        models.CASCADE,
+        related_name="extra_lessons",
+        verbose_name=_("Time period"),
+    )
 
-    subject = models.ForeignKey("Subject", on_delete=models.CASCADE, related_name="extra_lessons", verbose_name=_("Subject"))
-    groups = models.ManyToManyField("core.Group", related_name="extra_lessons", verbose_name=_("Groups"))
-    teachers = models.ManyToManyField("core.Person", related_name="extra_lessons_as_teacher", verbose_name=_("Teachers"))
-    room = models.ForeignKey("Room", models.CASCADE, null=True, related_name="extra_lessons", verbose_name=_("Room"))
+    subject = models.ForeignKey(
+        "Subject",
+        on_delete=models.CASCADE,
+        related_name="extra_lessons",
+        verbose_name=_("Subject"),
+    )
+    groups = models.ManyToManyField(
+        "core.Group", related_name="extra_lessons", verbose_name=_("Groups")
+    )
+    teachers = models.ManyToManyField(
+        "core.Person",
+        related_name="extra_lessons_as_teacher",
+        verbose_name=_("Teachers"),
+    )
+    room = models.ForeignKey(
+        "Room",
+        models.CASCADE,
+        null=True,
+        related_name="extra_lessons",
+        verbose_name=_("Room"),
+    )
 
-    comment = models.CharField(verbose_name=_("Comment"), blank=True, null=True, max_length=255)
+    comment = models.CharField(
+        verbose_name=_("Comment"), blank=True, null=True, max_length=255
+    )
 
     def __str__(self):
         return "{}, {}, {}".format(self.week, self.period, self.subject)
diff --git a/aleksis/apps/chronos/preferences.py b/aleksis/apps/chronos/preferences.py
index 69b435a47a964809a02a351f5cb0068dd3c04b33..c0a4294db7d17bb65b9b483a5ea38940d632a0cf 100644
--- a/aleksis/apps/chronos/preferences.py
+++ b/aleksis/apps/chronos/preferences.py
@@ -3,7 +3,10 @@ from django.utils.translation import gettext as _
 from dynamic_preferences.preferences import Section
 from dynamic_preferences.types import BooleanPreference, IntegerPreference
 
-from aleksis.core.registries import site_preferences_registry, person_preferences_registry
+from aleksis.core.registries import (
+    site_preferences_registry,
+    person_preferences_registry,
+)
 
 chronos = Section("chronos", verbose_name=_("Chronos"))
 
@@ -50,10 +53,11 @@ class SubstitutionsPrintNumberOfDays(IntegerPreference):
     default = 2
     verbose_name = _("Number of days shown on substitutions print view")
 
+
 @site_preferences_registry.register
 class SubstitutionsShowHeaderBox(BooleanPreference):
     section = chronos
     name = "substitutions_show_header_box"
     default = True
     verbose_name = _("Show header box in substitution views")
-    help_text =  _("The header box shows affected teachers/groups.")
+    help_text = _("The header box shows affected teachers/groups.")
diff --git a/aleksis/apps/chronos/rules.py b/aleksis/apps/chronos/rules.py
index ddc398cde95665f12e47e4dfe52f5f7c88d4177a..dc0369b08e6121c6be0187baad3471ca98573094 100644
--- a/aleksis/apps/chronos/rules.py
+++ b/aleksis/apps/chronos/rules.py
@@ -7,12 +7,12 @@ from aleksis.core.util.predicates import (
     has_object_perm,
 )
 from .models import LessonSubstitution
-from .util.predicates import (
-    has_timetable_perm
-)
+from .util.predicates import has_timetable_perm
 
 # View timetable overview
-view_timetable_overview_predicate = has_person & has_global_perm("chronos.view_timetable_overview")
+view_timetable_overview_predicate = has_person & has_global_perm(
+    "chronos.view_timetable_overview"
+)
 add_perm("chronos.view_timetable_overview", view_timetable_overview_predicate)
 
 # View my timetable
@@ -30,18 +30,21 @@ add_perm("chronos.view_lessons_day", view_lessons_day_predicate)
 
 # Edit substition
 edit_substitution_predicate = has_person & (
-    has_global_perm("chronos.change_lessonsubstitution") | has_object_perm("chronos.change_lessonsubstitution")
+    has_global_perm("chronos.change_lessonsubstitution")
+    | has_object_perm("chronos.change_lessonsubstitution")
 )
 add_perm("chronos.edit_substitution", edit_substitution_predicate)
 
 # Delete substitution
 delete_substitution_predicate = has_person & (
-    has_global_perm("chronos.delete_lessonsubstitution") | has_object_perm("chronos.delete_lessonsubstitution")
+    has_global_perm("chronos.delete_lessonsubstitution")
+    | has_object_perm("chronos.delete_lessonsubstitution")
 )
 add_perm("chronos.delete_substitution", delete_substitution_predicate)
 
 # View substitutions
 view_substitutions_predicate = has_person & (
-    has_global_perm("chronos.view_lessonsubstitution") | has_any_object("chronos.view_lessonsubstitution", LessonSubstitution)
+    has_global_perm("chronos.view_lessonsubstitution")
+    | has_any_object("chronos.view_lessonsubstitution", LessonSubstitution)
 )
 add_perm("chronos.view_substitutions", view_substitutions_predicate)
diff --git a/aleksis/apps/chronos/tables.py b/aleksis/apps/chronos/tables.py
index ed224b4c5470f1b9098ce972595bcd4697aeaf73..92a966fe11a3d866bdd9d160ebba712ab4e3066d 100644
--- a/aleksis/apps/chronos/tables.py
+++ b/aleksis/apps/chronos/tables.py
@@ -13,6 +13,7 @@ from .models import LessonPeriod
 def _css_class_from_lesson_state(
     record: Optional[LessonPeriod] = None, table: Optional[LessonsTable] = None
 ) -> str:
+    """Return CSS class depending on lesson state."""
     if record.get_substitution(record._week):
         if record.get_substitution(record._week).cancelled:
             return "success"
@@ -23,16 +24,25 @@ def _css_class_from_lesson_state(
 
 
 class LessonsTable(tables.Table):
+    """Table for daily lessons and management of substitutions."""
+
     class Meta:
         attrs = {"class": "highlight"}
         row_attrs = {"class": _css_class_from_lesson_state}
 
     period__period = tables.Column(accessor="period__period")
-    lesson__groups = tables.Column(accessor="lesson__group_names", verbose_name=_("Groups"))
-    lesson__teachers = tables.Column(accessor="lesson__teacher_names", verbose_name=_("Teachers"))
+    lesson__groups = tables.Column(
+        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", args=[A("id"), A("_week")], text=_("Substitution"),
-        attrs={"a": {"class": "btn-flat waves-effect waves-orange"}}, verbose_name=_("Manage substitution")
+        "edit_substitution",
+        args=[A("id"), A("_week")],
+        text=_("Substitution"),
+        attrs={"a": {"class": "btn-flat waves-effect waves-orange"}},
+        verbose_name=_("Manage substitution"),
     )
diff --git a/aleksis/apps/chronos/templates/chronos/partials/datepicker.html b/aleksis/apps/chronos/templates/chronos/partials/datepicker.html
index ede1fb06e37da69590468018bec6ca5384bd459f..3897461b9b81d79c003479be753ed5eb23ab1234 100644
--- a/aleksis/apps/chronos/templates/chronos/partials/datepicker.html
+++ b/aleksis/apps/chronos/templates/chronos/partials/datepicker.html
@@ -7,24 +7,24 @@
 {% endif %}
 
 <div class="col s2 no-padding">
-    <a class="waves-effect waves-teal btn-flat btn-flat-medium left" href="{{ url_prev }}">
-        <i class="material-icons center">navigate_before</i>
-    </a>
+  <a class="waves-effect waves-teal btn-flat btn-flat-medium left" href="{{ url_prev }}">
+    <i class="material-icons center">navigate_before</i>
+  </a>
 </div>
 
 {% if display_date_only %}
-    <div class="col s8">
+  <div class="col s8">
     <span class="card-title center-block" id="date">
         {{ day|date:"l" }}, {{ day }}
     </span>
-    </div>
+  </div>
 {% else %}
-    <div class="col s8 no-padding">
-        <input type="text" class="datepicker center-align" id="date">
-    </div>
+  <div class="col s8 no-padding">
+    <input type="text" class="datepicker center-align" id="date">
+  </div>
 {% endif %}
 <div class="col s2 no-padding">
-    <a class="waves-effect waves-teal btn-flat btn-flat-medium right" href="{{ url_next }}">
-        <i class="material-icons center">navigate_next</i>
-    </a>
+  <a class="waves-effect waves-teal btn-flat btn-flat-medium right" href="{{ url_next }}">
+    <i class="material-icons center">navigate_next</i>
+  </a>
 </div>
diff --git a/aleksis/apps/chronos/templates/chronos/partials/week_select.html b/aleksis/apps/chronos/templates/chronos/partials/week_select.html
index 6739290f11b07a2b787d3789f912bc284d6f2ec2..f2c8f31cf8653deae4c07dfe082d9bdd0e89d0a1 100644
--- a/aleksis/apps/chronos/templates/chronos/partials/week_select.html
+++ b/aleksis/apps/chronos/templates/chronos/partials/week_select.html
@@ -11,7 +11,8 @@
     <select id="calendar-week-1">
       {% for week in weeks %}
         <option value="{{ week.week }}" {% if week == wanted_week %}
-                selected {% endif %}>{% trans "CW" %} {{ week.week }} ({{ week.0|date:"SHORT_DATE_FORMAT" }}–{{ week.6|date:"SHORT_DATE_FORMAT" }})
+                selected {% endif %}>{% trans "CW" %} {{ week.week }}
+          ({{ week.0|date:"SHORT_DATE_FORMAT" }}–{{ week.6|date:"SHORT_DATE_FORMAT" }})
         </option>
       {% endfor %}
     </select>
@@ -21,7 +22,8 @@
     <select id="calendar-week-2">
       {% for week in weeks %}
         <option value="{{ week.week }}" {% if week == wanted_week %}
-                selected {% endif %}>{% trans "CW" %} {{ week.week }} ({{ week.0|date:"j.n." }}–{{ week.6|date:"SHORT_DATE_FORMAT" }})
+                selected {% endif %}>{% trans "CW" %} {{ week.week }}
+          ({{ week.0|date:"j.n." }}–{{ week.6|date:"SHORT_DATE_FORMAT" }})
         </option>
       {% endfor %}
     </select>
diff --git a/aleksis/apps/chronos/templates/chronos/substitutions.html b/aleksis/apps/chronos/templates/chronos/substitutions.html
index 3f5d70b3c8f29bbebb68fa7593b73a5f2291c7c0..b5f8551d2566191b4fe1a2755e9a72d386e77974 100644
--- a/aleksis/apps/chronos/templates/chronos/substitutions.html
+++ b/aleksis/apps/chronos/templates/chronos/substitutions.html
@@ -64,7 +64,7 @@
           {% include "chronos/partials/subs/groups.html" with type=item.type el=item.el %}
         </td>
         <td>
-         {% include "chronos/partials/subs/period.html" with type=item.type el=item.el %}
+          {% include "chronos/partials/subs/period.html" with type=item.type el=item.el %}
         </td>
         <td>
           {% include "chronos/partials/subs/teachers.html" with type=item.type el=item.el %}
diff --git a/aleksis/apps/chronos/templatetags/common.py b/aleksis/apps/chronos/templatetags/common.py
index 2a33c1b04b09d620cfb07fee9fb945f5dc61067b..cfdec15c8a19e0e2281b89490ce9e133000e741c 100644
--- a/aleksis/apps/chronos/templatetags/common.py
+++ b/aleksis/apps/chronos/templatetags/common.py
@@ -4,7 +4,6 @@ register = template.Library()
 
 
 class SetVarNode(template.Node):
-
     def __init__(self, var_name, var_value):
         self.var_name = var_name
         self.var_value = var_value
@@ -19,13 +18,15 @@ class SetVarNode(template.Node):
         return u""
 
 
-@register.tag(name='set')
+@register.tag(name="set")
 def set_var(parser, token):
     """
     {% set some_var = '123' %}
     """
     parts = token.split_contents()
     if len(parts) < 4:
-        raise template.TemplateSyntaxError("'set' tag must be of the form: {% set <var_name> = <var_value> %}")
+        raise template.TemplateSyntaxError(
+            "'set' tag must be of the form: {% set <var_name> = <var_value> %}"
+        )
 
     return SetVarNode(parts[1], parts[3])
diff --git a/aleksis/apps/chronos/templatetags/week_helpers.py b/aleksis/apps/chronos/templatetags/week_helpers.py
index acfb3a3a8859cc50c9e250520d9bcb32f3764ebe..054d3f9e2c3feac25e5067db9df324124a771f88 100644
--- a/aleksis/apps/chronos/templatetags/week_helpers.py
+++ b/aleksis/apps/chronos/templatetags/week_helpers.py
@@ -4,7 +4,11 @@ from typing import Optional, Union
 from django import template
 from django.db.models.query import QuerySet
 
-from aleksis.apps.chronos.util.date import CalendarWeek, week_period_to_date, week_weekday_to_date
+from aleksis.apps.chronos.util.date import (
+    CalendarWeek,
+    week_period_to_date,
+    week_weekday_to_date,
+)
 
 register = template.Library()
 
diff --git a/aleksis/apps/chronos/urls.py b/aleksis/apps/chronos/urls.py
index d5311f492f6592dfb66f4fa5b03b48fc76126188..3d68c3a18451570254e2d5323221bd12a31352ba 100644
--- a/aleksis/apps/chronos/urls.py
+++ b/aleksis/apps/chronos/urls.py
@@ -5,12 +5,28 @@ from . import views
 urlpatterns = [
     path("", views.all_timetables, name="all_timetables"),
     path("timetable/my/", views.my_timetable, name="my_timetable"),
-    path("timetable/my/<int:year>/<int:month>/<int:day>/", views.my_timetable, name="my_timetable_by_date"),
+    path(
+        "timetable/my/<int:year>/<int:month>/<int:day>/",
+        views.my_timetable,
+        name="my_timetable_by_date",
+    ),
     path("timetable/<str:type_>/<int:pk>/", views.timetable, name="timetable"),
-    path("timetable/<str:type_>/<int:pk>/<int:year>/<int:week>/", views.timetable, name="timetable_by_week"),
-    path("timetable/<str:type_>/<int:pk>/<str:regular>/", views.timetable, name="timetable_regular"),
+    path(
+        "timetable/<str:type_>/<int:pk>/<int:year>/<int:week>/",
+        views.timetable,
+        name="timetable_by_week",
+    ),
+    path(
+        "timetable/<str:type_>/<int:pk>/<str:regular>/",
+        views.timetable,
+        name="timetable_regular",
+    ),
     path("lessons/", views.lessons_day, name="lessons_day"),
-    path("lessons/<int:year>/<int:month>/<int:day>/", views.lessons_day, name="lessons_day_by_date"),
+    path(
+        "lessons/<int:year>/<int:month>/<int:day>/",
+        views.lessons_day,
+        name="lessons_day_by_date",
+    ),
     path(
         "lessons/<int:id_>/<int:week>/substition/",
         views.edit_substitution,
@@ -22,7 +38,21 @@ urlpatterns = [
         name="delete_substitution",
     ),
     path("substitutions/", views.substitutions, name="substitutions"),
-    path("substitutions/print/", views.substitutions, {"is_print": True}, name="substitutions_print"),
-    path("substitutions/<int:year>/<int:month>/<int:day>/", views.substitutions, name="substitutions_by_date"),
-    path("substitutions/<int:year>/<int:month>/<int:day>/print/", views.substitutions, {"is_print": True}, name="substitutions_print_by_date"),
+    path(
+        "substitutions/print/",
+        views.substitutions,
+        {"is_print": True},
+        name="substitutions_print",
+    ),
+    path(
+        "substitutions/<int:year>/<int:month>/<int:day>/",
+        views.substitutions,
+        name="substitutions_by_date",
+    ),
+    path(
+        "substitutions/<int:year>/<int:month>/<int:day>/print/",
+        views.substitutions,
+        {"is_print": True},
+        name="substitutions_print_by_date",
+    ),
 ]
diff --git a/aleksis/apps/chronos/util/build.py b/aleksis/apps/chronos/util/build.py
index 9fbc4ce5a629993983ca3adabc57c3d19f39864e..a73798e99ffaddad4b29d4abbaf22846d3bde184 100644
--- a/aleksis/apps/chronos/util/build.py
+++ b/aleksis/apps/chronos/util/build.py
@@ -21,7 +21,9 @@ ExtraLesson = apps.get_model("chronos", "ExtraLesson")
 
 
 def build_timetable(
-    type_: Union[TimetableType, str], obj: Union[int, Person], date_ref: Union[CalendarWeek, date]
+    type_: Union[TimetableType, str],
+    obj: Union[int, Person],
+    date_ref: Union[CalendarWeek, date],
 ):
     needed_breaks = []
 
@@ -59,7 +61,9 @@ def build_timetable(
     if is_person:
         extra_lessons = ExtraLesson.objects.on_day(date_ref).filter_from_person(obj)
     else:
-        extra_lessons = ExtraLesson.objects.filter(week=date_ref.week).filter_from_type(type_, obj)
+        extra_lessons = ExtraLesson.objects.filter(week=date_ref.week).filter_from_type(
+            type_, obj
+        )
 
     # Sort lesson periods in a dict
     extra_lessons_per_period = extra_lessons.group_by_periods(is_person=is_person)
@@ -108,7 +112,7 @@ def build_timetable(
                 # If not end day, use max period
                 period_to = TimePeriod.period_max
 
-            for period in range(period_from, period_to +1):
+            for period in range(period_from, period_to + 1):
                 if period not in events_per_period:
                     events_per_period[period] = [] if is_person else {}
 
@@ -126,7 +130,9 @@ def build_timetable(
             week = CalendarWeek.from_date(date_ref)
         else:
             week = date_ref
-        supervisions = Supervision.objects.all().annotate_week(week).filter_by_teacher(obj)
+        supervisions = (
+            Supervision.objects.all().annotate_week(week).filter_by_teacher(obj)
+        )
 
         if is_person:
             supervisions.filter_by_weekday(date_ref.weekday())
@@ -280,7 +286,7 @@ def build_substitutions_list(wanted_day: date) -> List[dict]:
             "type": "supervision_substitution",
             "sort_a": "Z.{}".format(super_sub.teacher),
             "sort_b": "{}".format(super_sub.supervision.break_item.after_period_number),
-            "el": super_sub
+            "el": super_sub,
         }
         rows.append(row)
 
diff --git a/aleksis/apps/chronos/util/format.py b/aleksis/apps/chronos/util/format.py
index c85668bff93f274fdf27fc33ec0f07d064eaa901..dbf704c22ff01a413cbfc0f8df538c4386faac07 100644
--- a/aleksis/apps/chronos/util/format.py
+++ b/aleksis/apps/chronos/util/format.py
@@ -1,9 +1,13 @@
+from datetime import date
+
 from django.utils.formats import date_format
 
 
-def format_m2m(f, attr: str = "short_name"):
+def format_m2m(f, attr: str = "short_name") -> str:
+    """Join a attribute of all elements of a ManyToManyField."""
     return ", ".join([getattr(x, attr) for x in f.all()])
 
 
-def format_date_period(date, period):
-    return "{}, {}.".format(date_format(date), period.period)
+def format_date_period(day: date, period: "TimePeriod") -> str:
+    """Format date and time period."""
+    return "{}, {}.".format(date_format(day), period.period)
diff --git a/aleksis/apps/chronos/views.py b/aleksis/apps/chronos/views.py
index 72821e75ad47d8f112cb507a7f3f7c0d43a66959..532d0ed953445f5b4c3ffb84c074a7c1597bb6ab 100644
--- a/aleksis/apps/chronos/views.py
+++ b/aleksis/apps/chronos/views.py
@@ -64,7 +64,9 @@ def my_timetable(
         wanted_day = timezone.datetime(year=year, month=month, day=day).date()
         wanted_day = TimePeriod.get_next_relevant_day(wanted_day)
     else:
-        wanted_day = TimePeriod.get_next_relevant_day(timezone.now().date(), datetime.now().time())
+        wanted_day = TimePeriod.get_next_relevant_day(
+            timezone.now().date(), datetime.now().time()
+        )
 
     if has_person(request.user):
         person = request.user.person
@@ -86,7 +88,9 @@ def my_timetable(
         context["day"] = wanted_day
         context["periods"] = TimePeriod.get_times_dict()
         context["smart"] = True
-        context["announcements"] = Announcement.for_timetables().on_date(wanted_day).for_person(person)
+        context["announcements"] = (
+            Announcement.for_timetables().on_date(wanted_day).for_person(person)
+        )
 
         context["url_prev"], context["url_next"] = TimePeriod.get_prev_next_by_day(
             wanted_day, "my_timetable_by_date"
@@ -148,7 +152,9 @@ def timetable(
 
     # Build lists with weekdays and corresponding dates (long and short variant)
     context["weekdays"] = build_weekdays(TimePeriod.WEEKDAY_CHOICES, wanted_week)
-    context["weekdays_short"] = build_weekdays(TimePeriod.WEEKDAY_CHOICES_SHORT, wanted_week)
+    context["weekdays_short"] = build_weekdays(
+        TimePeriod.WEEKDAY_CHOICES_SHORT, wanted_week
+    )
 
     context["weeks"] = get_weeks_for_year(year=wanted_week.year)
     context["week"] = wanted_week
@@ -158,13 +164,15 @@ def timetable(
     context["smart"] = is_smart
     context["week_select"] = {
         "year": wanted_week.year,
-        "dest": reverse("timetable", args=[type_, pk])
+        "dest": reverse("timetable", args=[type_, pk]),
     }
 
     if is_smart:
         start = wanted_week[TimePeriod.weekday_min]
         stop = wanted_week[TimePeriod.weekday_max]
-        context["announcements"] = Announcement.for_timetables().relevant_for(el).within_days(start, stop)
+        context["announcements"] = (
+            Announcement.for_timetables().relevant_for(el).within_days(start, stop)
+        )
 
     week_prev = wanted_week - 1
     week_next = wanted_week + 1
@@ -193,7 +201,9 @@ def lessons_day(
         wanted_day = timezone.datetime(year=year, month=month, day=day).date()
         wanted_day = TimePeriod.get_next_relevant_day(wanted_day)
     else:
-        wanted_day = TimePeriod.get_next_relevant_day(timezone.now().date(), datetime.now().time())
+        wanted_day = TimePeriod.get_next_relevant_day(
+            timezone.now().date(), datetime.now().time()
+        )
 
     # Get lessons
     lesson_periods = LessonPeriod.objects.on_day(wanted_day)
@@ -208,7 +218,7 @@ def lessons_day(
 
     context["datepicker"] = {
         "date": date_unix(wanted_day),
-        "dest": reverse("lessons_day")
+        "dest": reverse("lessons_day"),
     }
 
     context["url_prev"], context["url_next"] = TimePeriod.get_prev_next_by_day(
@@ -257,8 +267,7 @@ def edit_substitution(request: HttpRequest, id_: int, week: int) -> HttpResponse
 
             date = wanted_week[lesson_period.period.weekday]
             return redirect(
-                "lessons_day_by_date",
-                year=date.year, month=date.month, day=date.day
+                "lessons_day_by_date", year=date.year, month=date.month, day=date.day
             )
 
     context["edit_substitution_form"] = edit_substitution_form
@@ -281,8 +290,7 @@ def delete_substitution(request: HttpRequest, id_: int, week: int) -> HttpRespon
 
     date = wanted_week[lesson_period.period.weekday]
     return redirect(
-        "lessons_day_by_date",
-        year=date.year, month=date.month, day=date.day
+        "lessons_day_by_date", year=date.year, month=date.month, day=date.day
     )
 
 
@@ -301,7 +309,9 @@ def substitutions(
         wanted_day = timezone.datetime(year=year, month=month, day=day).date()
         wanted_day = TimePeriod.get_next_relevant_day(wanted_day)
     else:
-        wanted_day = TimePeriod.get_next_relevant_day(timezone.now().date(), datetime.now().time())
+        wanted_day = TimePeriod.get_next_relevant_day(
+            timezone.now().date(), datetime.now().time()
+        )
 
     day_number = get_site_preferences()["chronos__substitutions_print_number_of_days"]
     day_contexts = {}
@@ -318,11 +328,14 @@ def substitutions(
         subs = build_substitutions_list(day)
         day_contexts[day]["substitutions"] = subs
 
-        day_contexts[day]["announcements"] = Announcement.for_timetables().on_date(day).filter(show_in_timetables=True)
+        day_contexts[day]["announcements"] = (
+            Announcement.for_timetables().on_date(day).filter(show_in_timetables=True)
+        )
 
         if get_site_preferences()["chronos__substitutions_show_header_box"]:
-            subs = LessonSubstitution.objects.on_day(day).order_by("lesson_period__lesson__groups",
-                                                                   "lesson_period__period")
+            subs = LessonSubstitution.objects.on_day(day).order_by(
+                "lesson_period__lesson__groups", "lesson_period__period"
+            )
             absences = Absence.objects.on_day(day)
             day_contexts[day]["absent_teachers"] = absences.absent_teachers()
             day_contexts[day]["absent_groups"] = absences.absent_groups()
diff --git a/poetry.lock b/poetry.lock
index 5f2a2c53785d3a0d8a95cacbc0d598716780f600..9aea96d739f672441b972ef118dcf71921827a6c 100644
--- a/poetry.lock
+++ b/poetry.lock
@@ -14,12 +14,16 @@ calendarweek = "^0.4.3"
 colour = "^0.1.5"
 django-any-js = "^1.0"
 django-bleach = "^0.6.1"
+django-cache-memoize = "^0.1.6"
 django-ckeditor = "^5.8.0"
 django-colorfield = "^0.2.1"
 django-dbbackup = "^3.3.0"
 django-debug-toolbar = "^2.0"
+django-dynamic-preferences = "rev develop"
 django-easy-audit = "^1.2rc1"
+django-favicon-plus-reloaded = "^1.0.1"
 django-filter = "^2.2.0"
+django-guardian = "^2.2.0"
 django-hattori = "^0.2"
 django-haystack = "3.0b1"
 django-image-cropping = "^1.2"
@@ -29,12 +33,12 @@ django-js-reverse = "^0.9.1"
 django-jsonstore = "^0.4.1"
 django-maintenance-mode = "^0.14.0"
 django-material = "^1.6.0"
-django-memoize = "^2.2.1"
 django-menu-generator = "^1.0.4"
 django-middleware-global-request = "^0.1.2"
 django-otp = "0.7.5"
 django-polymorphic = "^2.1.2"
 django-pwa = "^1.0.8"
+django-reversion = "^3.0.7"
 django-sass-processor = "^0.8"
 django-settings-context-processor = "^0.2"
 django-tables2 = "^2.1"
@@ -49,19 +53,16 @@ license-expression = "^1.2"
 psycopg2 = "^2.8"
 python-memcached = "^1.59"
 requests = "^2.22"
+rules = "^2.2"
 spdx-license-list = "^0.4.0"
 
-[package.dependencies.django-constance]
-extras = ["database"]
-version = "^2.6.0"
-
 [package.dependencies.django-phonenumber-field]
 extras = ["phonenumbers"]
 version = ">=3.0, <5.0"
 
 [package.dependencies.django-two-factor-auth]
-extras = ["YubiKey", "phonenumbers", "Call", "SMS"]
-version = "^1.10.0"
+extras = ["yubikey", "phonenumbers", "call", "sms"]
+version = "^1.11.0"
 
 [package.dependencies.dynaconf]
 extras = ["yaml", "toml", "ini"]
@@ -119,9 +120,10 @@ description = "An easy safelist-based HTML-sanitizing tool."
 name = "bleach"
 optional = false
 python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
-version = "3.1.4"
+version = "3.1.5"
 
 [package.dependencies]
+packaging = "*"
 six = ">=1.9.0"
 webencodings = "*"
 
@@ -166,7 +168,7 @@ description = "Composable command line interface toolkit"
 name = "click"
 optional = false
 python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
-version = "7.1.1"
+version = "7.1.2"
 
 [[package]]
 category = "main"
@@ -261,6 +263,17 @@ version = "2.2.0"
 [package.dependencies]
 Django = ">=1.8"
 
+[[package]]
+category = "main"
+description = "Django utility for a memoization decorator that uses the Django cache framework."
+name = "django-cache-memoize"
+optional = false
+python-versions = "*"
+version = "0.1.6"
+
+[package.extras]
+dev = ["flake8", "tox", "twine", "therapist", "black"]
+
 [[package]]
 category = "main"
 description = "Django admin CKEditor integration."
@@ -280,23 +293,6 @@ optional = false
 python-versions = "*"
 version = "0.2.2"
 
-[[package]]
-category = "main"
-description = "Django live settings with pluggable backends, including Redis."
-name = "django-constance"
-optional = false
-python-versions = "*"
-version = "2.6.0"
-
-[package.dependencies]
-[package.dependencies.django-picklefield]
-optional = true
-version = "*"
-
-[package.extras]
-database = ["django-picklefield"]
-redis = ["redis"]
-
 [[package]]
 category = "main"
 description = "Management commands to help backup and restore a project database and media"
@@ -322,6 +318,23 @@ version = "2.2"
 Django = ">=1.11"
 sqlparse = ">=0.2.0"
 
+[[package]]
+category = "main"
+description = "Dynamic global and instance settings for your django project"
+name = "django-dynamic-preferences"
+optional = false
+python-versions = "*"
+version = "1.8.1"
+
+[package.dependencies]
+django = ">=1.11"
+persisting_theory = ">=0.2.1"
+six = "*"
+
+[package.source]
+reference = "3cb2637e99455260e1988fb969bdce16ba7a9801"
+type = "git"
+url = "https://github.com/EliotBerriot/django-dynamic-preferences"
 [[package]]
 category = "main"
 description = "Yet another Django audit log app, hopefully the simplest one."
@@ -333,6 +346,18 @@ version = "1.2.2b4"
 [package.dependencies]
 beautifulsoup4 = "*"
 
+[[package]]
+category = "main"
+description = "simple Django app which allows you to upload a image and it renders a wide variety for html link tags to display the favicon"
+name = "django-favicon-plus-reloaded"
+optional = false
+python-versions = "*"
+version = "1.0.1"
+
+[package.dependencies]
+django = "*"
+pillow = "*"
+
 [[package]]
 category = "main"
 description = "Django-filter is a reusable Django application for allowing users to filter querysets dynamically."
@@ -355,6 +380,17 @@ version = "2.2"
 [package.dependencies]
 Django = ">=1.11"
 
+[[package]]
+category = "main"
+description = "Implementation of per object permissions for Django."
+name = "django-guardian"
+optional = false
+python-versions = ">=3.5"
+version = "2.2.0"
+
+[package.dependencies]
+Django = ">=2.1"
+
 [[package]]
 category = "main"
 description = "Command to anonymize sensitive data."
@@ -458,17 +494,6 @@ version = "1.6.3"
 [package.dependencies]
 six = "*"
 
-[[package]]
-category = "main"
-description = "An implementation of memoization technique for Django."
-name = "django-memoize"
-optional = false
-python-versions = "*"
-version = "2.3.0"
-
-[package.dependencies]
-django = "*"
-
 [[package]]
 category = "main"
 description = "A straightforward menu generator for Django"
@@ -503,6 +528,19 @@ six = ">=1.10.0"
 [package.extras]
 qrcode = ["qrcode"]
 
+[[package]]
+category = "main"
+description = "A django-otp plugin that verifies YubiKey OTP tokens."
+name = "django-otp-yubikey"
+optional = false
+python-versions = "*"
+version = "0.5.2"
+
+[package.dependencies]
+YubiOTP = ">=0.2.2"
+django-otp = ">=0.5.0"
+six = ">=1.10.0"
+
 [[package]]
 category = "main"
 description = "An international phone number field for django models."
@@ -515,24 +553,14 @@ version = "3.0.1"
 Django = ">=1.11.3"
 babel = "*"
 
+[package.dependencies.phonenumbers]
+optional = true
+version = ">=7.0.2"
+
 [package.extras]
 phonenumbers = ["phonenumbers (>=7.0.2)"]
 phonenumberslite = ["phonenumberslite (>=7.0.2)"]
 
-[[package]]
-category = "main"
-description = "Pickled object field for Django"
-name = "django-picklefield"
-optional = false
-python-versions = "*"
-version = "2.1.1"
-
-[package.dependencies]
-Django = ">=1.11"
-
-[package.extras]
-tests = ["tox"]
-
 [[package]]
 category = "main"
 description = "Seamless polymorphic inheritance for Django models"
@@ -566,6 +594,17 @@ version = "0.6"
 [package.dependencies]
 django = ">=1.11"
 
+[[package]]
+category = "main"
+description = "An extension to the Django web framework that provides version control for model instances."
+name = "django-reversion"
+optional = false
+python-versions = ">=3.6"
+version = "3.0.7"
+
+[package.dependencies]
+django = ">=1.11"
+
 [[package]]
 category = "main"
 description = "SASS processor to compile SCSS files into *.css, while rendering, or offline."
@@ -639,10 +678,18 @@ django-otp = ">=0.6.0,<0.99"
 django-phonenumber-field = ">=1.1.0,<3.99"
 qrcode = ">=4.0.0,<6.99"
 
+[package.dependencies.django-otp-yubikey]
+optional = true
+version = "*"
+
 [package.dependencies.phonenumbers]
 optional = true
 version = ">=7.0.9,<8.99"
 
+[package.dependencies.twilio]
+optional = true
+version = ">=6.0"
+
 [package.extras]
 call = ["twilio (>=6.0)"]
 phonenumbers = ["phonenumbers (>=7.0.9,<8.99)"]
@@ -763,13 +810,33 @@ version = "1.2"
 [package.dependencies]
 "boolean.py" = ">=3.6,<4.0.0"
 
+[[package]]
+category = "main"
+description = "Core utilities for Python packages"
+name = "packaging"
+optional = false
+python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
+version = "20.3"
+
+[package.dependencies]
+pyparsing = ">=2.0.2"
+six = "*"
+
+[[package]]
+category = "main"
+description = "Registries that can autodiscover values accross your project apps"
+name = "persisting-theory"
+optional = false
+python-versions = "*"
+version = "0.2.1"
+
 [[package]]
 category = "main"
 description = "Python version of Google's common library for parsing, formatting, storing and validating international phone numbers."
 name = "phonenumbers"
 optional = false
 python-versions = "*"
-version = "8.12.1"
+version = "8.12.2"
 
 [[package]]
 category = "main"
@@ -777,7 +844,7 @@ description = "Python Imaging Library (Fork)"
 name = "pillow"
 optional = false
 python-versions = ">=3.5"
-version = "7.1.1"
+version = "7.1.2"
 
 [[package]]
 category = "main"
@@ -787,6 +854,35 @@ optional = false
 python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*"
 version = "2.8.5"
 
+[[package]]
+category = "main"
+description = "Cryptographic library for Python"
+name = "pycryptodome"
+optional = false
+python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
+version = "3.9.7"
+
+[[package]]
+category = "main"
+description = "JSON Web Token implementation in Python"
+name = "pyjwt"
+optional = false
+python-versions = "*"
+version = "1.7.1"
+
+[package.extras]
+crypto = ["cryptography (>=1.4)"]
+flake8 = ["flake8", "flake8-import-order", "pep8-naming"]
+test = ["pytest (>=4.0.1,<5.0.0)", "pytest-cov (>=2.6.0,<3.0.0)", "pytest-runner (>=4.2,<5.0.0)"]
+
+[[package]]
+category = "main"
+description = "Python parsing module"
+name = "pyparsing"
+optional = false
+python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*"
+version = "2.4.7"
+
 [[package]]
 category = "main"
 description = "Advanced Python dictionaries with dot notation access"
@@ -837,7 +933,7 @@ description = "World timezone definitions, modern and historical"
 name = "pytz"
 optional = false
 python-versions = "*"
-version = "2019.3"
+version = "2020.1"
 
 [[package]]
 category = "main"
@@ -883,6 +979,14 @@ urllib3 = ">=1.21.1,<1.25.0 || >1.25.0,<1.25.1 || >1.25.1,<1.26"
 security = ["pyOpenSSL (>=0.14)", "cryptography (>=1.3.4)"]
 socks = ["PySocks (>=1.5.6,<1.5.7 || >1.5.7)", "win-inet-pton"]
 
+[[package]]
+category = "main"
+description = "Awesome Django authorization, without the database"
+name = "rules"
+optional = false
+python-versions = "*"
+version = "2.2"
+
 [[package]]
 category = "main"
 description = "Python 2 and 3 compatibility utilities"
@@ -937,11 +1041,28 @@ description = "Fast, Extensible Progress Meter"
 name = "tqdm"
 optional = false
 python-versions = ">=2.6, !=3.0.*, !=3.1.*"
-version = "4.45.0"
+version = "4.46.0"
 
 [package.extras]
 dev = ["py-make (>=0.1.0)", "twine", "argopt", "pydoc-markdown"]
 
+[[package]]
+category = "main"
+description = "Twilio API client and TwiML generator"
+name = "twilio"
+optional = false
+python-versions = "*"
+version = "6.39.0"
+
+[package.dependencies]
+PyJWT = ">=1.4.2"
+pytz = "*"
+six = "*"
+
+[package.dependencies.requests]
+python = ">=3.0"
+version = ">=2.0.0"
+
 [[package]]
 category = "main"
 description = "HTTP library with thread-safe connection pooling, file post, and more."
@@ -963,6 +1084,18 @@ optional = false
 python-versions = "*"
 version = "0.5.1"
 
+[[package]]
+category = "main"
+description = "A library for verifying YubiKey OTP tokens, both locally and through a Yubico web service."
+name = "yubiotp"
+optional = false
+python-versions = "*"
+version = "0.2.2.post1"
+
+[package.dependencies]
+pycryptodome = "*"
+six = "*"
+
 [metadata]
 content-hash = "f2db0ef57bb256e1e7f9ae769792ce2b3288a37dccbae621f568844e2a656d0c"
 python-versions = "^3.7"
@@ -983,8 +1116,8 @@ beautifulsoup4 = [
     {file = "beautifulsoup4-4.9.0.tar.gz", hash = "sha256:594ca51a10d2b3443cbac41214e12dbb2a1cd57e1a7344659849e2e20ba6a8d8"},
 ]
 bleach = [
-    {file = "bleach-3.1.4-py2.py3-none-any.whl", hash = "sha256:cc8da25076a1fe56c3ac63671e2194458e0c4d9c7becfd52ca251650d517903c"},
-    {file = "bleach-3.1.4.tar.gz", hash = "sha256:e78e426105ac07026ba098f04de8abe9b6e3e98b5befbf89b51a5ef0a4292b03"},
+    {file = "bleach-3.1.5-py2.py3-none-any.whl", hash = "sha256:2bce3d8fab545a6528c8fa5d9f9ae8ebc85a56da365c7f85180bfe96a35ef22f"},
+    {file = "bleach-3.1.5.tar.gz", hash = "sha256:3c4c520fdb9db59ef139915a5db79f8b51bc2a7257ea0389f30c846883430a4b"},
 ]
 "boolean.py" = [
     {file = "boolean.py-3.7-py2.py3-none-any.whl", hash = "sha256:82ae181f9c85cb5c893a5a4daba9f24d60b538a7dd27fd0c6752a77eba4fbeff"},
@@ -1003,8 +1136,8 @@ chardet = [
     {file = "chardet-3.0.4.tar.gz", hash = "sha256:84ab92ed1c4d4f16916e05906b6b75a6c0fb5db821cc65e70cbd64a3e2a5eaae"},
 ]
 click = [
-    {file = "click-7.1.1-py2.py3-none-any.whl", hash = "sha256:e345d143d80bf5ee7534056164e5e112ea5e22716bbb1ce727941f4c8b471b9a"},
-    {file = "click-7.1.1.tar.gz", hash = "sha256:8a18b4ea89d8820c5d0c7da8a64b2c324b4dabb695804dbfea19b9be9d88c0cc"},
+    {file = "click-7.1.2-py2.py3-none-any.whl", hash = "sha256:dacca89f4bfadd5de3d7489b7c8a566eee0d3676333fbb50030263894c38c0dc"},
+    {file = "click-7.1.2.tar.gz", hash = "sha256:d2b5255c7c6349bc1bd1e59e08cd12acbbd63ce649f2588755783aa94dfb6b1a"},
 ]
 colorama = [
     {file = "colorama-0.4.3-py2.py3-none-any.whl", hash = "sha256:7d73d2a99753107a36ac6b455ee49046802e59d9d076ef8e47b61499fa29afff"},
@@ -1036,6 +1169,10 @@ django-bulk-update = [
     {file = "django-bulk-update-2.2.0.tar.gz", hash = "sha256:5ab7ce8a65eac26d19143cc189c0f041d5c03b9d1b290ca240dc4f3d6aaeb337"},
     {file = "django_bulk_update-2.2.0-py2.py3-none-any.whl", hash = "sha256:49a403392ae05ea872494d74fb3dfa3515f8df5c07cc277c3dc94724c0ee6985"},
 ]
+django-cache-memoize = [
+    {file = "django-cache-memoize-0.1.6.tar.gz", hash = "sha256:7f271be70b11155929ee8a4a2b5f53c9fb46b9befa1b546caffa3298e6ac8f7d"},
+    {file = "django_cache_memoize-0.1.6-py2.py3-none-any.whl", hash = "sha256:d239e8c37734b0a70b74f94fa33b180b3b0c82c3784beb21209bb4ab64a3e6fb"},
+]
 django-ckeditor = [
     {file = "django-ckeditor-5.9.0.tar.gz", hash = "sha256:e4d112851a72c5bf8b586e1c674d34084cab16d28f2553ad15cc770d1e9639c7"},
     {file = "django_ckeditor-5.9.0-py2.py3-none-any.whl", hash = "sha256:71c3c7bb46b0cbfb9712ef64af0d2a406eab233f44ecd7c42c24bdfa39ae3bde"},
@@ -1044,9 +1181,6 @@ django-colorfield = [
     {file = "django-colorfield-0.2.2.tar.gz", hash = "sha256:49cfce71365de88130e65ced8f2c5c4826b31e9ab0c5f0e721ff13a830b5be76"},
     {file = "django_colorfield-0.2.2-py2-none-any.whl", hash = "sha256:ecb8af68f35028e35f973ddb687c2dcae86d028c6da1b72580c0d3fae915d3b7"},
 ]
-django-constance = [
-    {file = "django-constance-2.6.0.tar.gz", hash = "sha256:12d827f9d5552ee39884fb6fb356f231f32b1ab8958acc715e3d1a6ecf913653"},
-]
 django-dbbackup = [
     {file = "django-dbbackup-3.3.0.tar.gz", hash = "sha256:bb109735cae98b64ad084e5b461b7aca2d7b39992f10c9ed9435e3ebb6fb76c8"},
 ]
@@ -1054,10 +1188,15 @@ django-debug-toolbar = [
     {file = "django-debug-toolbar-2.2.tar.gz", hash = "sha256:eabbefe89881bbe4ca7c980ff102e3c35c8e8ad6eb725041f538988f2f39a943"},
     {file = "django_debug_toolbar-2.2-py3-none-any.whl", hash = "sha256:ff94725e7aae74b133d0599b9bf89bd4eb8f5d2c964106e61d11750228c8774c"},
 ]
+django-dynamic-preferences = []
 django-easy-audit = [
     {file = "django-easy-audit-1.2.2b4.tar.gz", hash = "sha256:eac94b76882c6ad3fdb76d15f4f4ea281dc61e0897e92a457e058b87ed21ff68"},
     {file = "django_easy_audit-1.2.2b4-py3-none-any.whl", hash = "sha256:49ef3beea7bf439b349daa66d5e3d7624a7c9005d3bfd51f54d15dd5dcfaa202"},
 ]
+django-favicon-plus-reloaded = [
+    {file = "django-favicon-plus-reloaded-1.0.1.tar.gz", hash = "sha256:ee48b9a86ee20e5285216dc76c69cc6e8bcdcb1ea83c2585785dff7a4eebfc7b"},
+    {file = "django_favicon_plus_reloaded-1.0.1-py3-none-any.whl", hash = "sha256:d796bb994aa648a1d9c8276674a4de4e6af3ef2583dcd4d45495b4d3df85e283"},
+]
 django-filter = [
     {file = "django-filter-2.2.0.tar.gz", hash = "sha256:c3deb57f0dd7ff94d7dce52a047516822013e2b441bed472b722a317658cfd14"},
     {file = "django_filter-2.2.0-py3-none-any.whl", hash = "sha256:558c727bce3ffa89c4a7a0b13bc8976745d63e5fd576b3a9a851650ef11c401b"},
@@ -1066,6 +1205,10 @@ django-formtools = [
     {file = "django-formtools-2.2.tar.gz", hash = "sha256:c5272c03c1cd51b2375abf7397a199a3148a9fbbf2f100e186467a84025d13b2"},
     {file = "django_formtools-2.2-py2.py3-none-any.whl", hash = "sha256:304fa777b8ef9e0693ce7833f885cb89ba46b0e46fc23b01176900a93f46742f"},
 ]
+django-guardian = [
+    {file = "django-guardian-2.2.0.tar.gz", hash = "sha256:8cacf49ebcc1e545f0a8997971eec0fe109f5ed31fc2a569a7bf5615453696e2"},
+    {file = "django_guardian-2.2.0-py3-none-any.whl", hash = "sha256:ac81e88372fdf1795d84ba065550e739b42e9c6d07cdf201cf5bbf9efa7f396c"},
+]
 django-hattori = [
     {file = "django-hattori-0.2.1.tar.gz", hash = "sha256:6953d40881317252f19f62c4e7fe8058924b852c7498bc42beb7bc4d268c252c"},
     {file = "django_hattori-0.2.1-py2.py3-none-any.whl", hash = "sha256:e529ed7af8fc34a0169c797c477672b687a205a56f3f5206f90c260acb83b7ac"},
@@ -1103,9 +1246,6 @@ django-material = [
     {file = "django-material-1.6.3.tar.gz", hash = "sha256:f8758afe1beabc16a3c54f5437c7fea15946b7d068eedd89c97d57a363793950"},
     {file = "django_material-1.6.3-py2.py3-none-any.whl", hash = "sha256:502dc88c2f61f190fdc401666e83b47da00cbda98477af6ed8b7d43944ce6407"},
 ]
-django-memoize = [
-    {file = "django-memoize-2.3.0.tar.gz", hash = "sha256:85decffbef7d38ffc569dc96527f598e6677bbc01ce29adf722b051da7efd4be"},
-]
 django-menu-generator = [
     {file = "django-menu-generator-1.0.4.tar.gz", hash = "sha256:ce71a5055c16933c8aff64fb36c21e5cf8b6d505733aceed1252f8b99369a378"},
 ]
@@ -1116,14 +1256,14 @@ django-otp = [
     {file = "django-otp-0.7.5.tar.gz", hash = "sha256:1f16c2b93fe484706ff16ac6f5e64ecc73dd240318c333e0560384ba548d3837"},
     {file = "django_otp-0.7.5-py2.py3-none-any.whl", hash = "sha256:cd4975539be478417033561e9832a1a69a583189f680e92a649f412c661f90aa"},
 ]
+django-otp-yubikey = [
+    {file = "django-otp-yubikey-0.5.2.tar.gz", hash = "sha256:f0b1881562fb42ee9f12c28d284cbdb90d1f0383f2d53a595373b080a19bc261"},
+    {file = "django_otp_yubikey-0.5.2-py2.py3-none-any.whl", hash = "sha256:26b12c763b37e99b95b8b8a54d06d8d54c3774eb26133a452f54558033de732b"},
+]
 django-phonenumber-field = [
     {file = "django-phonenumber-field-3.0.1.tar.gz", hash = "sha256:794ebbc3068a7af75aa72a80cb0cec67e714ff8409a965968040f1fd210b2d97"},
     {file = "django_phonenumber_field-3.0.1-py3-none-any.whl", hash = "sha256:1ab19f723928582fed412bd9844221fa4ff466276d8526b8b4a9913ee1487c5e"},
 ]
-django-picklefield = [
-    {file = "django-picklefield-2.1.1.tar.gz", hash = "sha256:67a5e156343e3b032cac2f65565f0faa81635a99c7da74b0f07a0f5db467b646"},
-    {file = "django_picklefield-2.1.1-py2.py3-none-any.whl", hash = "sha256:e03cb181b7161af38ad6b573af127e4fe9b7cc2c455b42c1ec43eaad525ade0a"},
-]
 django-polymorphic = [
     {file = "django-polymorphic-2.1.2.tar.gz", hash = "sha256:6e08a76c91066635ccb7ef3ebbe9a0ad149febae6b30be2579716ec16d3c6461"},
     {file = "django_polymorphic-2.1.2-py2.py3-none-any.whl", hash = "sha256:0a25058e95e5e99fe0beeabb8f4734effe242d7b5b77dca416fba9fd3062da6a"},
@@ -1135,6 +1275,10 @@ django-pwa = [
 django-render-block = [
     {file = "django_render_block-0.6-py2.py3-none-any.whl", hash = "sha256:95c7dc9610378a10e0c4a10d8364ec7307210889afccd6a67a6aaa0fd599bd4d"},
 ]
+django-reversion = [
+    {file = "django-reversion-3.0.7.tar.gz", hash = "sha256:72fc53580a6b538f0cfff10f27f42333f67d79c406399289c94ec5a193cfb3e1"},
+    {file = "django_reversion-3.0.7-py3-none-any.whl", hash = "sha256:ecab4703ecc0871dc325c3e100139def84eb153622df3413fbcd9de7d3503c78"},
+]
 django-sass-processor = [
     {file = "django-sass-processor-0.8.tar.gz", hash = "sha256:e039551994feaaba6fcf880412b25a772dd313162a34cbb4289814988cfae340"},
 ]
@@ -1190,6 +1334,7 @@ libsass = [
     {file = "libsass-0.19.4-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:81a013a4c2a614927fd1ef7a386eddabbba695cbb02defe8f31cf495106e974c"},
     {file = "libsass-0.19.4-cp35-cp35m-win32.whl", hash = "sha256:fcb7ab4dc81889e5fc99cafbc2017bc76996f9992fc6b175f7a80edac61d71df"},
     {file = "libsass-0.19.4-cp35-cp35m-win_amd64.whl", hash = "sha256:fc5f8336750f76f1bfae82f7e9e89ae71438d26fc4597e3ab4c05ca8fcd41d8a"},
+    {file = "libsass-0.19.4-cp36-cp36m-macosx_10_14_x86_64.whl", hash = "sha256:53f87116e7441827878bd79bbad8debac23e1930423f61ab8d837ec4a4c36e0c"},
     {file = "libsass-0.19.4-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:9b59afa0d755089c4165516400a39a289b796b5612eeef5736ab7a1ebf96a67c"},
     {file = "libsass-0.19.4-cp36-cp36m-win32.whl", hash = "sha256:c93df526eeef90b1ea4799c1d33b6cd5aea3e9f4633738fb95c1287c13e6b404"},
     {file = "libsass-0.19.4-cp36-cp36m-win_amd64.whl", hash = "sha256:0fd8b4337b3b101c6e6afda9112cc0dc4bacb9133b59d75d65968c7317aa3272"},
@@ -1197,6 +1342,7 @@ libsass = [
     {file = "libsass-0.19.4-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:338e9ae066bf1fde874e335324d5355c52d2081d978b4f74fc59536564b35b08"},
     {file = "libsass-0.19.4-cp37-cp37m-win32.whl", hash = "sha256:e318f06f06847ff49b1f8d086ac9ebce1e63404f7ea329adab92f4f16ba0e00e"},
     {file = "libsass-0.19.4-cp37-cp37m-win_amd64.whl", hash = "sha256:a7e685466448c9b1bf98243339793978f654a1151eb5c975f09b83c7a226f4c1"},
+    {file = "libsass-0.19.4-cp38-cp38-macosx_10_14_x86_64.whl", hash = "sha256:0fb4399f7bbecab7b181f2c2d82c3a0ba2916bf9169714b96e425355a5b23b9f"},
     {file = "libsass-0.19.4-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:6a51393d75f6e3c812785b0fa0b7d67c54258c28011921f204643b55f7355ec0"},
     {file = "libsass-0.19.4.tar.gz", hash = "sha256:8b5b6d1a7c4ea1d954e0982b04474cc076286493f6af2d0a13c2e950fbe0be95"},
 ]
@@ -1204,34 +1350,41 @@ license-expression = [
     {file = "license-expression-1.2.tar.gz", hash = "sha256:7960e1dfdf20d127e75ead931476f2b5c7556df05b117a73880b22ade17d1abc"},
     {file = "license_expression-1.2-py2.py3-none-any.whl", hash = "sha256:6d97906380cecfc758a77f6d38c6760f2afade7e83d2b8295e234fe21f486fb8"},
 ]
+packaging = [
+    {file = "packaging-20.3-py2.py3-none-any.whl", hash = "sha256:82f77b9bee21c1bafbf35a84905d604d5d1223801d639cf3ed140bd651c08752"},
+    {file = "packaging-20.3.tar.gz", hash = "sha256:3c292b474fda1671ec57d46d739d072bfd495a4f51ad01a055121d81e952b7a3"},
+]
+persisting-theory = [
+    {file = "persisting-theory-0.2.1.tar.gz", hash = "sha256:00ff7dcc8f481ff75c770ca5797d968e8725b6df1f77fe0cf7d20fa1e5790c0a"},
+]
 phonenumbers = [
-    {file = "phonenumbers-8.12.1-py2.py3-none-any.whl", hash = "sha256:bebf881ef0e775b93062fbd107bf164b5baef877a7b8f702e93a9a5d24ae4065"},
-    {file = "phonenumbers-8.12.1.tar.gz", hash = "sha256:59ae9cb25fb03027c9f2bf5584098e699be7eca12c443838b83752956be15cda"},
+    {file = "phonenumbers-8.12.2-py2.py3-none-any.whl", hash = "sha256:eedbace07295109ce98b13b9bd1ac22dd43c1e90a3f0854c557c2298493fc731"},
+    {file = "phonenumbers-8.12.2.tar.gz", hash = "sha256:61adadab01adaac571b04ddbe50f981c488ef00cfd51eef7e040ef4765871b00"},
 ]
 pillow = [
-    {file = "Pillow-7.1.1-cp35-cp35m-macosx_10_10_intel.whl", hash = "sha256:b7453750cf911785009423789d2e4e5393aae9cbb8b3f471dab854b85a26cb89"},
-    {file = "Pillow-7.1.1-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:4510c6b33277970b1af83c987277f9a08ec2b02cc20ac0f9234e4026136bb137"},
-    {file = "Pillow-7.1.1-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:b99b2607b6cd58396f363b448cbe71d3c35e28f03e442ab00806463439629c2c"},
-    {file = "Pillow-7.1.1-cp35-cp35m-win32.whl", hash = "sha256:cd47793f7bc9285a88c2b5551d3f16a2ddd005789614a34c5f4a598c2a162383"},
-    {file = "Pillow-7.1.1-cp35-cp35m-win_amd64.whl", hash = "sha256:04a10558320eba9137d6a78ca6fc8f4a5801f1b971152938851dc4629d903579"},
-    {file = "Pillow-7.1.1-cp36-cp36m-macosx_10_10_x86_64.whl", hash = "sha256:50a10b048f4dd81c092adad99fa5f7ba941edaf2f9590510109ac2a15e706695"},
-    {file = "Pillow-7.1.1-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:721c04d3c77c38086f1f95d1cd8df87f2f9a505a780acf8575912b3206479da1"},
-    {file = "Pillow-7.1.1-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:a5dc9f28c0239ec2742d4273bd85b2aa84655be2564db7ad1eb8f64b1efcdc4c"},
-    {file = "Pillow-7.1.1-cp36-cp36m-win32.whl", hash = "sha256:d6bf085f6f9ec6a1724c187083b37b58a8048f86036d42d21802ed5d1fae4853"},
-    {file = "Pillow-7.1.1-cp36-cp36m-win_amd64.whl", hash = "sha256:251e5618125ec12ac800265d7048f5857a8f8f1979db9ea3e11382e159d17f68"},
-    {file = "Pillow-7.1.1-cp37-cp37m-macosx_10_10_x86_64.whl", hash = "sha256:433bbc2469a2351bea53666d97bb1eb30f0d56461735be02ea6b27654569f80f"},
-    {file = "Pillow-7.1.1-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:eb84e7e5b07ff3725ab05977ac56d5eeb0c510795aeb48e8b691491be3c5745b"},
-    {file = "Pillow-7.1.1-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:3713386d1e9e79cea1c5e6aaac042841d7eef838cc577a3ca153c8bedf570287"},
-    {file = "Pillow-7.1.1-cp37-cp37m-win32.whl", hash = "sha256:291bad7097b06d648222b769bbfcd61e40d0abdfe10df686d20ede36eb8162b6"},
-    {file = "Pillow-7.1.1-cp37-cp37m-win_amd64.whl", hash = "sha256:6c1924ed7dbc6ad0636907693bbbdd3fdae1d73072963e71f5644b864bb10b4d"},
-    {file = "Pillow-7.1.1-cp38-cp38-macosx_10_10_x86_64.whl", hash = "sha256:670e58d3643971f4afd79191abd21623761c2ebe61db1c2cb4797d817c4ba1a7"},
-    {file = "Pillow-7.1.1-cp38-cp38-manylinux1_i686.whl", hash = "sha256:8d5799243050c2833c2662b824dfb16aa98e408d2092805edea4300a408490e7"},
-    {file = "Pillow-7.1.1-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:da737ab273f4d60ae552f82ad83f7cbd0e173ca30ca20b160f708c92742ee212"},
-    {file = "Pillow-7.1.1-cp38-cp38-win32.whl", hash = "sha256:b2f3e8cc52ecd259b94ca880fea0d15f4ebc6da2cd3db515389bb878d800270f"},
-    {file = "Pillow-7.1.1-cp38-cp38-win_amd64.whl", hash = "sha256:2f0b52a08d175f10c8ea36685115681a484c55d24d0933f9fd911e4111c04144"},
-    {file = "Pillow-7.1.1-pp373-pypy36_pp73-win32.whl", hash = "sha256:90cd441a1638ae176eab4d8b6b94ab4ec24b212ed4c3fbee2a6e74672481d4f8"},
-    {file = "Pillow-7.1.1-py3.8-macosx-10.9-x86_64.egg", hash = "sha256:5eef904c82b5f8e4256e8d420c971357da2884c0b812ba4efa15a7ad2ec66247"},
-    {file = "Pillow-7.1.1.tar.gz", hash = "sha256:0f89ddc77cf421b8cd34ae852309501458942bf370831b4a9b406156b599a14e"},
+    {file = "Pillow-7.1.2-cp35-cp35m-macosx_10_10_intel.whl", hash = "sha256:ae2b270f9a0b8822b98655cb3a59cdb1bd54a34807c6c56b76dd2e786c3b7db3"},
+    {file = "Pillow-7.1.2-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:d23e2aa9b969cf9c26edfb4b56307792b8b374202810bd949effd1c6e11ebd6d"},
+    {file = "Pillow-7.1.2-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:b532bcc2f008e96fd9241177ec580829dee817b090532f43e54074ecffdcd97f"},
+    {file = "Pillow-7.1.2-cp35-cp35m-win32.whl", hash = "sha256:12e4bad6bddd8546a2f9771485c7e3d2b546b458ae8ff79621214119ac244523"},
+    {file = "Pillow-7.1.2-cp35-cp35m-win_amd64.whl", hash = "sha256:9744350687459234867cbebfe9df8f35ef9e1538f3e729adbd8fde0761adb705"},
+    {file = "Pillow-7.1.2-cp36-cp36m-macosx_10_10_x86_64.whl", hash = "sha256:f54be399340aa602066adb63a86a6a5d4f395adfdd9da2b9a0162ea808c7b276"},
+    {file = "Pillow-7.1.2-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:1f694e28c169655c50bb89a3fa07f3b854d71eb47f50783621de813979ba87f3"},
+    {file = "Pillow-7.1.2-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:f784aad988f12c80aacfa5b381ec21fd3f38f851720f652b9f33facc5101cf4d"},
+    {file = "Pillow-7.1.2-cp36-cp36m-win32.whl", hash = "sha256:b37bb3bd35edf53125b0ff257822afa6962649995cbdfde2791ddb62b239f891"},
+    {file = "Pillow-7.1.2-cp36-cp36m-win_amd64.whl", hash = "sha256:b67a6c47ed963c709ed24566daa3f95a18f07d3831334da570c71da53d97d088"},
+    {file = "Pillow-7.1.2-cp37-cp37m-macosx_10_10_x86_64.whl", hash = "sha256:eaa83729eab9c60884f362ada982d3a06beaa6cc8b084cf9f76cae7739481dfa"},
+    {file = "Pillow-7.1.2-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:f46e0e024346e1474083c729d50de909974237c72daca05393ee32389dabe457"},
+    {file = "Pillow-7.1.2-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:0e2a3bceb0fd4e0cb17192ae506d5f082b309ffe5fc370a5667959c9b2f85fa3"},
+    {file = "Pillow-7.1.2-cp37-cp37m-win32.whl", hash = "sha256:ccc9ad2460eb5bee5642eaf75a0438d7f8887d484490d5117b98edd7f33118b7"},
+    {file = "Pillow-7.1.2-cp37-cp37m-win_amd64.whl", hash = "sha256:b943e71c2065ade6fef223358e56c167fc6ce31c50bc7a02dd5c17ee4338e8ac"},
+    {file = "Pillow-7.1.2-cp38-cp38-macosx_10_10_x86_64.whl", hash = "sha256:04766c4930c174b46fd72d450674612ab44cca977ebbcc2dde722c6933290107"},
+    {file = "Pillow-7.1.2-cp38-cp38-manylinux1_i686.whl", hash = "sha256:f455efb7a98557412dc6f8e463c1faf1f1911ec2432059fa3e582b6000fc90e2"},
+    {file = "Pillow-7.1.2-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:ee94fce8d003ac9fd206496f2707efe9eadcb278d94c271f129ab36aa7181344"},
+    {file = "Pillow-7.1.2-cp38-cp38-win32.whl", hash = "sha256:4b02b9c27fad2054932e89f39703646d0c543f21d3cc5b8e05434215121c28cd"},
+    {file = "Pillow-7.1.2-cp38-cp38-win_amd64.whl", hash = "sha256:3d25dd8d688f7318dca6d8cd4f962a360ee40346c15893ae3b95c061cdbc4079"},
+    {file = "Pillow-7.1.2-pp373-pypy36_pp73-win32.whl", hash = "sha256:0f01e63c34f0e1e2580cc0b24e86a5ccbbfa8830909a52ee17624c4193224cd9"},
+    {file = "Pillow-7.1.2-py3.8-macosx-10.9-x86_64.egg", hash = "sha256:70e3e0d99a0dcda66283a185f80697a9b08806963c6149c8e6c5f452b2aa59c0"},
+    {file = "Pillow-7.1.2.tar.gz", hash = "sha256:a0b49960110bc6ff5fead46013bcb8825d101026d466f3a4de3476defe0fb0dd"},
 ]
 psycopg2 = [
     {file = "psycopg2-2.8.5-cp27-cp27m-win32.whl", hash = "sha256:a0984ff49e176062fcdc8a5a2a670c9bb1704a2f69548bce8f8a7bad41c661bf"},
@@ -1248,6 +1401,46 @@ psycopg2 = [
     {file = "psycopg2-2.8.5-cp38-cp38-win_amd64.whl", hash = "sha256:132efc7ee46a763e68a815f4d26223d9c679953cd190f1f218187cb60decf535"},
     {file = "psycopg2-2.8.5.tar.gz", hash = "sha256:f7d46240f7a1ae1dd95aab38bd74f7428d46531f69219954266d669da60c0818"},
 ]
+pycryptodome = [
+    {file = "pycryptodome-3.9.7-cp27-cp27m-macosx_10_6_intel.whl", hash = "sha256:0e10f352ccbbcb5bb2dc4ecaf106564e65702a717d72ab260f9ac4c19753cfc2"},
+    {file = "pycryptodome-3.9.7-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:9c739b7795ccf2ef1fdad8d44e539a39ad300ee6786e804ea7f0c6a786eb5343"},
+    {file = "pycryptodome-3.9.7-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:9977086e0f93adb326379897437373871b80501e1d176fec63c7f46fb300c862"},
+    {file = "pycryptodome-3.9.7-cp27-cp27m-win32.whl", hash = "sha256:83295a3fb5cf50c48631eb5b440cb5e9832d8c14d81d1d45f4497b67a9987de8"},
+    {file = "pycryptodome-3.9.7-cp27-cp27m-win_amd64.whl", hash = "sha256:b1e332587b3b195542e77681389c296e1837ca01240399d88803a075447d3557"},
+    {file = "pycryptodome-3.9.7-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:9378c309aec1f8cd8bad361ed0816a440151b97a2a3f6ffdaba1d1a1fb76873a"},
+    {file = "pycryptodome-3.9.7-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:d4f94368ce2d65873a87ad867eb3bf63f4ba81eb97a9ee66d38c2b71ce5a7439"},
+    {file = "pycryptodome-3.9.7-cp34-cp34m-manylinux1_i686.whl", hash = "sha256:f655addaaaa9974108d4808f4150652589cada96074c87115c52e575bfcd87d5"},
+    {file = "pycryptodome-3.9.7-cp34-cp34m-manylinux1_x86_64.whl", hash = "sha256:9a94fca11fdc161460bd8659c15b6adef45c1b20da86402256eaf3addfaab324"},
+    {file = "pycryptodome-3.9.7-cp35-cp35m-macosx_10_6_intel.whl", hash = "sha256:ea83bcd9d6c03248ebd46e71ac313858e0afd5aa2fa81478c0e653242f3eb476"},
+    {file = "pycryptodome-3.9.7-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:07024fc364869eae8d6ac0d316e089956e6aeffe42dbdcf44fe1320d96becf7f"},
+    {file = "pycryptodome-3.9.7-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:426c188c83c10df71f053e04b4003b1437bae5cb37606440e498b00f160d71d0"},
+    {file = "pycryptodome-3.9.7-cp35-cp35m-win32.whl", hash = "sha256:d61b012baa8c2b659e9890011358455c0019a4108536b811602d2f638c40802a"},
+    {file = "pycryptodome-3.9.7-cp35-cp35m-win_amd64.whl", hash = "sha256:1f4752186298caf2e9ff5354f2e694d607ca7342aa313a62005235d46e28cf04"},
+    {file = "pycryptodome-3.9.7-cp36-cp36m-macosx_10_6_intel.whl", hash = "sha256:767ad0fb5d23efc36a4d5c2fc608ac603f3de028909bcf59abc943e0d0bc5a36"},
+    {file = "pycryptodome-3.9.7-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:2fbc472e0b567318fe2052281d5a8c0ae70099b446679815f655e9fbc18c3a65"},
+    {file = "pycryptodome-3.9.7-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:9230fcb5d948c3fb40049bace4d33c5d254f8232c2c0bba05d2570aea3ba4520"},
+    {file = "pycryptodome-3.9.7-cp36-cp36m-win32.whl", hash = "sha256:8f06556a8f7ea7b1e42eff39726bb0dca1c251205debae64e6eebea3cd7b438a"},
+    {file = "pycryptodome-3.9.7-cp36-cp36m-win_amd64.whl", hash = "sha256:d6e1bc5c94873bec742afe2dfadce0d20445b18e75c47afc0c115b19e5dd38dd"},
+    {file = "pycryptodome-3.9.7-cp37-cp37m-macosx_10_6_intel.whl", hash = "sha256:3ec3dc2f80f71fd0c955ce48b81bfaf8914c6f63a41a738f28885a1c4892968a"},
+    {file = "pycryptodome-3.9.7-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:cff31f5a8977534f255f729d5d2467526f2b10563a30bbdade92223e0bf264bd"},
+    {file = "pycryptodome-3.9.7-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:ed5761b37615a1f222c5345bbf45272ae2cf8c7dff88a4f53a1e9f977cbb6d95"},
+    {file = "pycryptodome-3.9.7-cp37-cp37m-win32.whl", hash = "sha256:f011cd0062e54658b7086a76f8cf0f4222812acc66e219e196ea2d0a8849d0ed"},
+    {file = "pycryptodome-3.9.7-cp37-cp37m-win_amd64.whl", hash = "sha256:626c0a1d4d83ec6303f970a17158114f75c3ba1736f7f2983f7b40a265861bd8"},
+    {file = "pycryptodome-3.9.7-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:8be56bde3312e022d9d1d6afa124556460ad5c844c2fc63642f6af723c098d35"},
+    {file = "pycryptodome-3.9.7-cp38-cp38-manylinux1_i686.whl", hash = "sha256:c818dc1f3eace93ee50c2b6b5c2becf7c418fa5dd1ba6fc0ef7db279ea21d5e4"},
+    {file = "pycryptodome-3.9.7-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:09b6d6bcc01a4eb1a2b4deeff5aa602a108ec5aed8ac75ae554f97d1d7f0a5ad"},
+    {file = "pycryptodome-3.9.7-cp38-cp38-win32.whl", hash = "sha256:7ac729d9091ed5478af2b4a4f44f5335a98febbc008af619e4569a59fe503e40"},
+    {file = "pycryptodome-3.9.7-cp38-cp38-win_amd64.whl", hash = "sha256:c109a26a21f21f695d369ff9b87f5d43e0d6c768d8384e10bc74142bed2e092e"},
+    {file = "pycryptodome-3.9.7.tar.gz", hash = "sha256:f1add21b6d179179b3c177c33d18a2186a09cc0d3af41ff5ed3f377360b869f2"},
+]
+pyjwt = [
+    {file = "PyJWT-1.7.1-py2.py3-none-any.whl", hash = "sha256:5c6eca3c2940464d106b99ba83b00c6add741c9becaec087fb7ccdefea71350e"},
+    {file = "PyJWT-1.7.1.tar.gz", hash = "sha256:8d59a976fb773f3e6a39c85636357c4f0e242707394cadadd9814f5cbaa20e96"},
+]
+pyparsing = [
+    {file = "pyparsing-2.4.7-py2.py3-none-any.whl", hash = "sha256:ef9d7589ef3c200abe66653d3f1ab1033c3c419ae9b9bdb1240a85b024efc88b"},
+    {file = "pyparsing-2.4.7.tar.gz", hash = "sha256:c203ec8783bf771a155b207279b9bccb8dea02d8f0c9e5f8ead507bc3246ecc1"},
+]
 python-box = [
     {file = "python-box-3.4.6.tar.gz", hash = "sha256:694a7555e3ff9fbbce734bbaef3aad92b8e4ed0659d3ed04d56b6a0a0eff26a9"},
     {file = "python_box-3.4.6-py2.py3-none-any.whl", hash = "sha256:a71d3dc9dbaa34c8597d3517c89a8041bd62fa875f23c0f3dad55e1958e3ce10"},
@@ -1265,8 +1458,8 @@ python-memcached = [
     {file = "python_memcached-1.59-py2.py3-none-any.whl", hash = "sha256:4dac64916871bd3550263323fc2ce18e1e439080a2d5670c594cf3118d99b594"},
 ]
 pytz = [
-    {file = "pytz-2019.3-py2.py3-none-any.whl", hash = "sha256:1c557d7d0e871de1f5ccd5833f60fb2550652da6be2693c1e02300743d21500d"},
-    {file = "pytz-2019.3.tar.gz", hash = "sha256:b02c06db6cf09c12dd25137e563b31700d3b80fcc4ad23abb7a315f2789819be"},
+    {file = "pytz-2020.1-py2.py3-none-any.whl", hash = "sha256:a494d53b6d39c3c6e44c3bec237336e14305e4f29bbf800b599253057fbb79ed"},
+    {file = "pytz-2020.1.tar.gz", hash = "sha256:c35965d010ce31b23eeb663ed3cc8c906275d6be1a34393a1d73a41febf4a048"},
 ]
 pyyaml = [
     {file = "PyYAML-5.3.1-cp27-cp27m-win32.whl", hash = "sha256:74809a57b329d6cc0fdccee6318f44b9b8649961fa73144a98735b0aaf029f1f"},
@@ -1289,6 +1482,9 @@ requests = [
     {file = "requests-2.23.0-py2.py3-none-any.whl", hash = "sha256:43999036bfa82904b6af1d99e4882b560e5e2c68e5c4b0aa03b655f3d7d73fee"},
     {file = "requests-2.23.0.tar.gz", hash = "sha256:b3f43d496c6daba4493e7c431722aeb7dbc6288f52a6e04e7b6023b0247817e6"},
 ]
+rules = [
+    {file = "rules-2.2.tar.gz", hash = "sha256:9bae429f9d4f91a375402990da1541f9e093b0ac077221d57124d06eeeca4405"},
+]
 six = [
     {file = "six-1.14.0-py2.py3-none-any.whl", hash = "sha256:8f3cd2e254d8f793e7f3d6d9df77b92252b52637291d0f0da013c76ea2724b6c"},
     {file = "six-1.14.0.tar.gz", hash = "sha256:236bdbdce46e6e6a3d61a337c0f8b763ca1e8717c03b369e87a7ec7ce1319c0a"},
@@ -1315,8 +1511,11 @@ toml = [
     {file = "toml-0.10.0.tar.gz", hash = "sha256:229f81c57791a41d65e399fc06bf0848bab550a9dfd5ed66df18ce5f05e73d5c"},
 ]
 tqdm = [
-    {file = "tqdm-4.45.0-py2.py3-none-any.whl", hash = "sha256:ea9e3fd6bd9a37e8783d75bfc4c1faf3c6813da6bd1c3e776488b41ec683af94"},
-    {file = "tqdm-4.45.0.tar.gz", hash = "sha256:00339634a22c10a7a22476ee946bbde2dbe48d042ded784e4d88e0236eca5d81"},
+    {file = "tqdm-4.46.0-py2.py3-none-any.whl", hash = "sha256:acdafb20f51637ca3954150d0405ff1a7edde0ff19e38fb99a80a66210d2a28f"},
+    {file = "tqdm-4.46.0.tar.gz", hash = "sha256:4733c4a10d0f2a4d098d801464bdaf5240c7dadd2a7fde4ee93b0a0efd9fb25e"},
+]
+twilio = [
+    {file = "twilio-6.39.0.tar.gz", hash = "sha256:7ef6ad19251fee6a41f1184e97b4fcb62f4a8c0e6f4b78797e40e9c92aed006d"},
 ]
 urllib3 = [
     {file = "urllib3-1.25.9-py2.py3-none-any.whl", hash = "sha256:88206b0eb87e6d677d424843ac5209e3fb9d0190d0ee169599165ec25e9d9115"},
@@ -1326,3 +1525,7 @@ webencodings = [
     {file = "webencodings-0.5.1-py2.py3-none-any.whl", hash = "sha256:a0af1213f3c2226497a97e2b3aa01a7e4bee4f403f95be16fc9acd2947514a78"},
     {file = "webencodings-0.5.1.tar.gz", hash = "sha256:b36a1c245f2d304965eb4e0a82848379241dc04b865afcc4aab16748587e1923"},
 ]
+yubiotp = [
+    {file = "YubiOTP-0.2.2.post1-py2.py3-none-any.whl", hash = "sha256:7e281801b24678f4bda855ce8ab975a7688a912f5a6cb22b6c2b16263a93cbd2"},
+    {file = "YubiOTP-0.2.2.post1.tar.gz", hash = "sha256:de83b1560226e38b5923f6ab919f962c8c2abb7c722104cb45b2b6db2ac86e40"},
+]