diff --git a/aleksis/apps/chronos/models.py b/aleksis/apps/chronos/models.py
index 293b62ba8f886a0689d79121421de7e346bb9473..0f9c5f9e409b53f15bab8b1e4c4674e4076ab88b 100644
--- a/aleksis/apps/chronos/models.py
+++ b/aleksis/apps/chronos/models.py
@@ -1,13 +1,15 @@
 from __future__ import annotations
 
-from datetime import date, datetime, timedelta
+from datetime import date, datetime, timedelta, time
 from typing import Dict, Optional, Tuple, Union
 
 from django.core import validators
 from django.core.exceptions import ValidationError
 from django.db import models
-from django.db.models import F, Q
+from django.db.models import F, Max, Min, Q
+from django.db.models.functions import Coalesce
 from django.http.request import QueryDict
+from django.utils.decorators import classproperty
 from django.utils.translation import ugettext_lazy as _
 
 from calendarweek.django import CalendarWeek, i18n_day_names_lazy, i18n_day_abbrs_lazy
@@ -244,6 +246,30 @@ class TimePeriod(models.Model):
 
         return wanted_week[self.weekday]
 
+    @classproperty
+    def period_min(cls) -> int:
+        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")
+
+    @classproperty
+    def time_min(cls) -> Optional[time]:
+        return cls.objects.aggregate(Min("time_start")).get("time_start__min")
+
+    @classproperty
+    def time_max(cls) -> Optional[time]:
+        return cls.objects.aggregate(Max("time_start")).get("time_start__max")
+
+    @classproperty
+    def weekday_min(cls) -> int:
+        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")
+
     class Meta:
         unique_together = [["weekday", "period"]]
         ordering = ["weekday", "period"]
diff --git a/aleksis/apps/chronos/util/min_max.py b/aleksis/apps/chronos/util/min_max.py
deleted file mode 100644
index 4d5dd421443f36b3b2f01ce0a2ef74b6504a0b5e..0000000000000000000000000000000000000000
--- a/aleksis/apps/chronos/util/min_max.py
+++ /dev/null
@@ -1,19 +0,0 @@
-from django.db.models import Min, Max
-
-from aleksis.apps.chronos.models import TimePeriod
-
-# Determine overall first and last day and period
-min_max = TimePeriod.objects.aggregate(
-    Min("period"), Max("period"), Min("weekday"), Max("weekday"), Min("time_start"), Max("time_end")
-)
-
-period_min = min_max.get("period__min", 1)
-period_max = min_max.get("period__max", 7)
-
-time_min = min_max.get("time_start__min", None)
-time_max = min_max.get("time_end__max", None)
-
-weekday_min_ = min_max.get("weekday__min", 0)
-weekday_max = min_max.get("weekday__max", 6)
-
-
diff --git a/aleksis/apps/chronos/util/prev_next.py b/aleksis/apps/chronos/util/prev_next.py
index c0a844dc40e728a17c98e6289def5bd69c1dd9b9..4f954c3082eb69e5c18afb12896c975ed07135b0 100644
--- a/aleksis/apps/chronos/util/prev_next.py
+++ b/aleksis/apps/chronos/util/prev_next.py
@@ -5,7 +5,7 @@ from calendarweek import CalendarWeek
 from django.urls import reverse
 from django.utils import timezone
 
-from aleksis.apps.chronos.util.min_max import weekday_min_, weekday_max, time_max
+from ..models import TimePeriod
 
 
 def get_next_relevant_day(day: Optional[date] = None, time: Optional[time] = None, prev: bool = False) -> date:
@@ -15,23 +15,23 @@ def get_next_relevant_day(day: Optional[date] = None, time: Optional[time] = Non
         day = timezone.now().date()
 
     if time is not None and not prev:
-        if time > time_max:
+        if time > TimePeriod.time_max:
             day += timedelta(days=1)
 
     cw = CalendarWeek.from_date(day)
 
-    if day.weekday() > weekday_max:
+    if day.weekday() > TimePeriod.weekday_max:
         if prev:
-            day = cw[weekday_max]
+            day = cw[TimePeriod.weekday_max]
         else:
             cw += 1
-            day = cw[weekday_min_]
-    elif day.weekday() < weekday_min_:
+            day = cw[TimePeriod.weekday_min]
+    elif day.weekday() < TimePeriod.weekday_min:
         if prev:
             cw -= 1
-            day = cw[weekday_max]
+            day = cw[TimePeriod.weekday_max]
         else:
-            day = cw[weekday_min_]
+            day = cw[TimePeriod.weekday_min]
 
     return day
 
diff --git a/aleksis/apps/chronos/views.py b/aleksis/apps/chronos/views.py
index cd3acad87d356a9debafe3e30302514473f31baa..44fe5e7f670267dbd4ae8c5093b6420ecf5fcb9b 100644
--- a/aleksis/apps/chronos/views.py
+++ b/aleksis/apps/chronos/views.py
@@ -153,13 +153,13 @@ def timetable(
             ] = [lesson_period]
 
     # Fill in empty lessons
-    for period_num in range(period_min, period_max + 1):
+    for period_num in range(TimePeriod.period_min, TimePeriod.period_max + 1):
         # Fill in empty weekdays
         if period_num not in per_period.keys():
             per_period[period_num] = {}
 
         # Fill in empty lessons on this workday
-        for weekday_num in range(weekday_min_, weekday_max + 1):
+        for weekday_num in range(TimePeriod.weekday_min, TimePeriod.weekday_max + 1):
             if weekday_num not in per_period[period_num].keys():
                 per_period[period_num][weekday_num] = []
 
@@ -169,10 +169,10 @@ def timetable(
     context["lesson_periods"] = OrderedDict(sorted(per_period.items()))
     context["periods"] = TimePeriod.get_times_dict()
     context["weekdays"] = dict(
-        TimePeriod.WEEKDAY_CHOICES[weekday_min_ : weekday_max + 1]
+        TimePeriod.WEEKDAY_CHOICES[TimePeriod.weekday_min : TimePeriod.weekday_max + 1]
     )
     context["weekdays_short"] = dict(
-        TimePeriod.WEEKDAY_CHOICES_SHORT[weekday_min_ : weekday_max + 1]
+        TimePeriod.WEEKDAY_CHOICES_SHORT[TimePeriod.weekday_min : TimePeriod.weekday_max + 1]
     )
     context["weeks"] = get_weeks_for_year(year=wanted_week.year)
     context["week"] = wanted_week
@@ -266,7 +266,8 @@ 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
diff --git a/poetry.lock b/poetry.lock
index 28e6962c2c88ad1487dfccbf777f814cff0d214a..8a2acb4de4976064953cbf323e304874f39bf496 100644
--- a/poetry.lock
+++ b/poetry.lock
@@ -23,11 +23,13 @@ django-image-cropping = "^1.2"
 django-impersonate = "^1.4"
 django-ipware = "^2.1"
 django-js-reverse = "^0.9.1"
+django-jsonstore = "^0.4.1"
 django-maintenance-mode = "^0.14.0"
 django-material = "^1.6.0"
 django-menu-generator = "^1.0.4"
 django-middleware-global-request = "^0.1.2"
-django-pwa = "^1.0.6"
+django-polymorphic = "^2.1.2"
+django-pwa = "rev 67cf917a081df3116968f684ebb28e4c076b2b50"
 django-sass-processor = "^0.8"
 django-settings-context-processor = "^0.2"
 django-tables2 = "^2.1"
@@ -44,7 +46,7 @@ requests = "^2.22"
 
 [package.dependencies.django-constance]
 extras = ["database"]
-version = "rev 590fa02eb30e377da0eda5cc3a84254b839176a7"
+version = "^2.6.0"
 
 [package.dependencies.django-phonenumber-field]
 extras = ["phonenumbers"]
@@ -110,7 +112,7 @@ description = "Utilities for working with calendar weeks in Python and Django"
 name = "calendarweek"
 optional = false
 python-versions = ">=3.7,<4.0"
-version = "0.4.4"
+version = "0.4.5"
 
 [package.extras]
 django = ["Django (>=2.2,<4.0)"]
@@ -167,6 +169,9 @@ optional = false
 python-versions = "*"
 version = "5.0.6"
 
+[package.dependencies]
+six = "*"
+
 [[package]]
 category = "main"
 description = "A high-level Python Web framework that encourages rapid development and clean, pragmatic design."
@@ -235,7 +240,7 @@ description = "Django admin CKEditor integration."
 name = "django-ckeditor"
 optional = false
 python-versions = "*"
-version = "5.8.0"
+version = "5.9.0"
 
 [package.dependencies]
 django-js-asset = ">=1.2.2"
@@ -246,7 +251,7 @@ description = "Django live settings with pluggable backends, including Redis."
 name = "django-constance"
 optional = false
 python-versions = "*"
-version = "2.5.0"
+version = "2.6.0"
 
 [package.dependencies]
 [package.dependencies.django-picklefield]
@@ -257,10 +262,6 @@ version = "*"
 database = ["django-picklefield"]
 redis = ["redis"]
 
-[package.source]
-reference = "590fa02eb30e377da0eda5cc3a84254b839176a7"
-type = "git"
-url = "https://github.com/jazzband/django-constance"
 [[package]]
 category = "main"
 description = "A configurable set of panels that display various debug information about the current request/response."
@@ -279,7 +280,7 @@ description = "Yet another Django audit log app, hopefully the simplest one."
 name = "django-easy-audit"
 optional = false
 python-versions = "*"
-version = "1.2rc1"
+version = "1.2.1rc1"
 
 [package.dependencies]
 beautifulsoup4 = "*"
@@ -367,6 +368,18 @@ version = "0.9.1"
 [package.dependencies]
 Django = ">=1.5"
 
+[[package]]
+category = "main"
+description = "Expose JSONField data as a virtual django model fields."
+name = "django-jsonstore"
+optional = false
+python-versions = "*"
+version = "0.4.1"
+
+[package.dependencies]
+Django = ">=1.11"
+six = "*"
+
 [[package]]
 category = "main"
 description = "django-maintenance-mode shows a 503 error page when maintenance-mode is on."
@@ -459,7 +472,7 @@ description = "Pickled object field for Django"
 name = "django-picklefield"
 optional = false
 python-versions = "*"
-version = "2.0"
+version = "2.1.1"
 
 [package.dependencies]
 Django = ">=1.11"
@@ -467,6 +480,17 @@ Django = ">=1.11"
 [package.extras]
 tests = ["tox"]
 
+[[package]]
+category = "main"
+description = "Seamless polymorphic inheritance for Django models"
+name = "django-polymorphic"
+optional = false
+python-versions = "*"
+version = "2.1.2"
+
+[package.dependencies]
+Django = ">=1.11"
+
 [[package]]
 category = "main"
 description = "A Django app to include a manifest.json and Service Worker instance to enable progressive web app behavior"
@@ -478,6 +502,10 @@ version = "1.0.6"
 [package.dependencies]
 django = ">=1.8"
 
+[package.source]
+reference = "67cf917a081df3116968f684ebb28e4c076b2b50"
+type = "git"
+url = "https://github.com/Natureshadow/django-pwa"
 [[package]]
 category = "main"
 description = "Render a particular block from a template to a string."
@@ -791,7 +819,7 @@ category = "main"
 description = "YAML parser and emitter for Python"
 name = "pyyaml"
 optional = false
-python-versions = "*"
+python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
 version = "5.3"
 
 [[package]]
@@ -876,7 +904,7 @@ description = "Fast, Extensible Progress Meter"
 name = "tqdm"
 optional = false
 python-versions = ">=2.6, !=3.0.*, !=3.1.*"
-version = "4.41.1"
+version = "4.42.0"
 
 [package.extras]
 dev = ["py-make (>=0.1.0)", "twine", "argopt", "pydoc-markdown"]
@@ -887,7 +915,7 @@ description = "Twilio API client and TwiML generator"
 name = "twilio"
 optional = false
 python-versions = "*"
-version = "6.35.2"
+version = "6.35.3"
 
 [package.dependencies]
 PyJWT = ">=1.4.2"
@@ -903,8 +931,8 @@ category = "main"
 description = "HTTP library with thread-safe connection pooling, file post, and more."
 name = "urllib3"
 optional = false
-python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, <4"
-version = "1.25.7"
+python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, <4"
+version = "1.25.8"
 
 [package.extras]
 brotli = ["brotlipy (>=0.6.0)"]
@@ -943,8 +971,8 @@ beautifulsoup4 = [
     {file = "beautifulsoup4-4.8.2.tar.gz", hash = "sha256:05fd825eb01c290877657a56df4c6e4c311b3965bda790c613a3d6fb01a5462a"},
 ]
 calendarweek = [
-    {file = "calendarweek-0.4.4-py3-none-any.whl", hash = "sha256:6510a42015558f140ed6677e79efbb45d8bf87ccded069db4026283eb639a256"},
-    {file = "calendarweek-0.4.4.tar.gz", hash = "sha256:02f092ec54ebe162dc9f3614de6efbf3d7fb35115e8ca5d62e99d65c342f5732"},
+    {file = "calendarweek-0.4.5-py3-none-any.whl", hash = "sha256:b35fcc087073969d017cede62a7295bcd714a1304bcb4c4e2b0f23acb0265fb1"},
+    {file = "calendarweek-0.4.5.tar.gz", hash = "sha256:5b1788ca435022f9348fc81a718974e51dd85d080f9aa3dad717df70a1bc6e1f"},
 ]
 certifi = [
     {file = "certifi-2019.11.28-py2.py3-none-any.whl", hash = "sha256:017c25db2a153ce562900032d5bc68e9f191e44e9a0f762f373977de9df1fbb3"},
@@ -989,17 +1017,19 @@ django-bulk-update = [
     {file = "django_bulk_update-2.2.0-py2.py3-none-any.whl", hash = "sha256:49a403392ae05ea872494d74fb3dfa3515f8df5c07cc277c3dc94724c0ee6985"},
 ]
 django-ckeditor = [
-    {file = "django-ckeditor-5.8.0.tar.gz", hash = "sha256:46fc9c7346ea36183dc0cea350f98704f8b04c4722b7fe4fb18baf8ae20423fb"},
-    {file = "django_ckeditor-5.8.0-py2.py3-none-any.whl", hash = "sha256:a59bab13f4481318f8a048b1b0aef5c7da768a6352dcfb9ba0e77d91fbb9462a"},
+    {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"},
+]
+django-constance = [
+    {file = "django-constance-2.6.0.tar.gz", hash = "sha256:12d827f9d5552ee39884fb6fb356f231f32b1ab8958acc715e3d1a6ecf913653"},
 ]
-django-constance = []
 django-debug-toolbar = [
     {file = "django-debug-toolbar-2.1.tar.gz", hash = "sha256:24c157bc6c0e1648e0a6587511ecb1b007a00a354ce716950bff2de12693e7a8"},
     {file = "django_debug_toolbar-2.1-py3-none-any.whl", hash = "sha256:77cfba1d6e91b9bc3d36dc7dc74a9bb80be351948db5f880f2562a0cbf20b6c5"},
 ]
 django-easy-audit = [
-    {file = "django-easy-audit-1.2rc1.tar.gz", hash = "sha256:80f82fa4006290dcd6589a345e75de1c780de49d38218050eedd9048c54b647d"},
-    {file = "django_easy_audit-1.2rc1-py3-none-any.whl", hash = "sha256:fb9c5ec3e90f0900302448d3648acc11da6d6b3d35d13d77eab917ab8c813d77"},
+    {file = "django-easy-audit-1.2.1rc1.tar.gz", hash = "sha256:a127264dbfef4aac17bfa74439487540ad41e47ff4c067d3d77bfad82fd23bc5"},
+    {file = "django_easy_audit-1.2.1rc1-py3-none-any.whl", hash = "sha256:00e9a9bc063ad73120fe399f2e7bc216af5fc32186dc5eccad09e2c4cd10abc1"},
 ]
 django-filter = [
     {file = "django-filter-2.2.0.tar.gz", hash = "sha256:c3deb57f0dd7ff94d7dce52a047516822013e2b441bed472b722a317658cfd14"},
@@ -1030,6 +1060,9 @@ django-js-reverse = [
     {file = "django-js-reverse-0.9.1.tar.gz", hash = "sha256:2a392d169f44e30b883c30dfcfd917a14167ce8fe196c99d2385b31c90d77aa0"},
     {file = "django_js_reverse-0.9.1-py2.py3-none-any.whl", hash = "sha256:8134c2ab6307c945edfa90671ca65e85d6c1754d48566bdd6464be259cc80c30"},
 ]
+django-jsonstore = [
+    {file = "django-jsonstore-0.4.1.tar.gz", hash = "sha256:d6e42152af3f924e4657c99e80144ba9a6410799256f6134b5a4e9fa4282ec10"},
+]
 django-maintenance-mode = [
     {file = "django-maintenance-mode-0.14.0.tar.gz", hash = "sha256:f3fef1760fdcda5e0bf6c2966aadc77eea6f328580a9c751920daba927281a68"},
     {file = "django_maintenance_mode-0.14.0-py2-none-any.whl", hash = "sha256:b4cc24a469ed10897826a28f05d64e6166a58d130e4940ac124ce198cd4cc778"},
@@ -1057,13 +1090,14 @@ django-phonenumber-field = [
     {file = "django_phonenumber_field-3.0.1-py3-none-any.whl", hash = "sha256:1ab19f723928582fed412bd9844221fa4ff466276d8526b8b4a9913ee1487c5e"},
 ]
 django-picklefield = [
-    {file = "django-picklefield-2.0.tar.gz", hash = "sha256:f1733a8db1b6046c0d7d738e785f9875aa3c198215de11993463a9339aa4ea24"},
-    {file = "django_picklefield-2.0-py2.py3-none-any.whl", hash = "sha256:9052f2dcf4882c683ce87b4356f29b4d014c0dad645b6906baf9f09571f52bc8"},
+    {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-pwa = [
-    {file = "django-pwa-1.0.6.tar.gz", hash = "sha256:b3f1ad0c5241fae4c7505423540de4db93077d7c88416ff6d2af545ffe209f34"},
-    {file = "django_pwa-1.0.6-py3-none-any.whl", hash = "sha256:9306105fcb637ae16fea6527be4b147d45fd53db85efb1d4f61dfea6bf793e56"},
+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"},
 ]
+django-pwa = []
 django-render-block = [
     {file = "django_render_block-0.6-py2.py3-none-any.whl", hash = "sha256:95c7dc9610378a10e0c4a10d8364ec7307210889afccd6a67a6aaa0fd599bd4d"},
 ]
@@ -1276,15 +1310,15 @@ toml = [
     {file = "toml-0.10.0.tar.gz", hash = "sha256:229f81c57791a41d65e399fc06bf0848bab550a9dfd5ed66df18ce5f05e73d5c"},
 ]
 tqdm = [
-    {file = "tqdm-4.41.1-py2.py3-none-any.whl", hash = "sha256:efab950cf7cc1e4d8ee50b2bb9c8e4a89f8307b49e0b2c9cfef3ec4ca26655eb"},
-    {file = "tqdm-4.41.1.tar.gz", hash = "sha256:4789ccbb6fc122b5a6a85d512e4e41fc5acad77216533a6f2b8ce51e0f265c23"},
+    {file = "tqdm-4.42.0-py2.py3-none-any.whl", hash = "sha256:01464d5950e9a07a8e463c2767883d9616c099c6502f6c7ef4e2e11d3065bd35"},
+    {file = "tqdm-4.42.0.tar.gz", hash = "sha256:5865f5fef9d739864ff341ddaa69894173ebacedb1aaafcf014de56343d01d5c"},
 ]
 twilio = [
-    {file = "twilio-6.35.2.tar.gz", hash = "sha256:a086443642c0e1f13c8f8f087b426ca81ec883efbe496d8279180a49bb9287bc"},
+    {file = "twilio-6.35.3.tar.gz", hash = "sha256:4474fa87fde5ea5526e296be782d1b06fc6722f9e4698a503c47b5c2f8b70ea9"},
 ]
 urllib3 = [
-    {file = "urllib3-1.25.7-py2.py3-none-any.whl", hash = "sha256:a8a318824cc77d1fd4b2bec2ded92646630d7fe8619497b142c84a9e6f5a7293"},
-    {file = "urllib3-1.25.7.tar.gz", hash = "sha256:f3c5fd51747d450d4dcf6f923c81f78f811aab8205fda64b0aba34a4e48b0745"},
+    {file = "urllib3-1.25.8-py2.py3-none-any.whl", hash = "sha256:2f3db8b19923a873b3e5256dc9c2dedfa883e33d87c690d9c7913e1f40673cdc"},
+    {file = "urllib3-1.25.8.tar.gz", hash = "sha256:87716c2d2a7121198ebcb7ce7cccf6ce5e9ba539041cfbaeecfb641dc0bf6acc"},
 ]
 yubiotp = [
     {file = "YubiOTP-0.2.2.post1-py2.py3-none-any.whl", hash = "sha256:7e281801b24678f4bda855ce8ab975a7688a912f5a6cb22b6c2b16263a93cbd2"},