diff --git a/README.rst b/README.rst
index e5d0251f38d496da608ba3c99526b31678378caf..664d44feccd825af419d30650fa31a27e97234c2 100644
--- a/README.rst
+++ b/README.rst
@@ -1,38 +1,16 @@
-AlekSIS — All-libre extensible kit for school information systems
-=================================================================
+AlekSIS (School Information System) — Core (Core functionality and app framework)
+=================================================================================
 
-Warning
--------
-
-**This is an alpha version of AlekSIS, the free school information system.
-The AlekSIS team is looking for schools who want to help shape the 2.0
-final release and supports interested schools in operating AlekSIS.**
-
-What AlekSIS is
-----------------
-
-`AlekSIS`_ is a web-based school information system (SIS) which can be used to
-manage and/or publish organisational subjects of educational institutions.
-
-Formerly two separate projects (BiscuIT and SchoolApps), developed by
-`Teckids e.V.`_ and a team of students at `Katharineum zu Lübeck`_, they
-were merged into the AlekSIS project in 2020.
+AlekSIS standard distribution
+-----------------------------
 
-AlekSIS is a platform based on Django, that provides central funstions
-and data structures that can be used by apps that are developed and provided
-seperately. The AlekSIS team also maintains a set of official apps which
-make AlekSIS a fully-featured software solutions for the information
-management needs of schools.
+The AlekSIS standard distribution with information about all official apps
+can be found on `EduGit`_.
 
-By design, the platform can be used by schools to write their own apps for
-specific needs they face, also in coding classes. Students are empowered to
-create real-world applications that bring direct value to their environment.
+Features
+--------
 
-AlekSIS is part of the `schul-frei`_ project as a component in sustainable
-educational networks.
-
-Core features
---------------
+The AlekSIS-Core currently provides the following features:
 
 * For users:
 
@@ -53,24 +31,6 @@ Core features
  * Authentication via LDAP
  * Automatic backup of database, static and media files
 
-Official apps
--------------
-
-+--------------------------------------+---------------------------------------------------------------------------------------------+
-| App name                             | Purpose                                                                                     |
-+======================================+=============================================================================================+
-| `AlekSIS-App-Chronos`_               | The Chronos app provides functionality for digital timetables.                              |
-+--------------------------------------+---------------------------------------------------------------------------------------------+
-| `AlekSIS-App-DashboardFeeds`_        | The DashboardFeeds app provides functionality to add RSS or Atom feeds to dashboard         |
-+--------------------------------------+---------------------------------------------------------------------------------------------+
-| `AlekSIS-App-Hjelp`_                 | The Hjelp app provides functionality for aiding users.                                      |
-+--------------------------------------+---------------------------------------------------------------------------------------------+
-| `AlekSIS-App-LDAP`_                  | The LDAP app provides functionality to import users and groups from LDAP                    |
-+--------------------------------------+---------------------------------------------------------------------------------------------+
-| `AlekSIS-App-Untis`_                 | This app provides import and export functions to interact with Untis, a timetable software. |
-+--------------------------------------+---------------------------------------------------------------------------------------------+
-
-
 Licence
 -------
 
@@ -91,13 +51,6 @@ full licence text or on the `European Union Public Licence`_ website
 https://joinup.ec.europa.eu/collection/eupl/guidelines-users-and-developers
 (including all other official language versions).
 
-.. _AlekSIS: https://aleksis.org/
-.. _Teckids e.V.: https://www.teckids.org/
-.. _Katharineum zu Lübeck: https://www.katharineum.de/
+.. _AlekSIS: https://edugit.org/AlekSIS/Official/AlekSIS
 .. _European Union Public Licence: https://eupl.eu/
-.. _schul-frei: https://schul-frei.org/
-.. _AlekSIS-App-Chronos: https://edugit.org/AlekSIS/official/AlekSIS-App-Chronos
-.. _AlekSIS-App-DashboardFeeds: https://edugit.org/AlekSIS/official/AlekSIS-App-DashboardFeeds
-.. _AlekSIS-App-Hjelp: https://edugit.org/AlekSIS/official/AlekSIS-App-Hjelp
-.. _AlekSIS-App-LDAP: https://edugit.org/AlekSIS/official/AlekSIS-App-LDAP
-.. _AlekSIS-App-Untis: https://edugit.org/AlekSIS/official/AlekSIS-App-Untis
+.. _EduGit: https://edugit.org/AlekSIS/official/AlekSIS
diff --git a/aleksis/core/forms.py b/aleksis/core/forms.py
index 9297980c7cc8bbd494f6124ee914f6d542b1863f..f4599d4d0e13ac02d01fda147331a3997d0f8dd0 100644
--- a/aleksis/core/forms.py
+++ b/aleksis/core/forms.py
@@ -10,7 +10,15 @@ from dynamic_preferences.forms import PreferenceForm
 from material import Fieldset, Layout, Row
 
 from .mixins import ExtensibleForm, SchoolTermRelatedExtensibleForm
-from .models import AdditionalField, Announcement, Group, GroupType, Person, SchoolTerm
+from .models import (
+    AdditionalField,
+    Announcement,
+    DashboardWidget,
+    Group,
+    GroupType,
+    Person,
+    SchoolTerm,
+)
 from .registries import (
     group_preferences_registry,
     person_preferences_registry,
@@ -345,3 +353,20 @@ class SchoolTermForm(ExtensibleForm):
     class Meta:
         model = SchoolTerm
         exclude = []
+
+
+class DashboardWidgetOrderForm(ExtensibleForm):
+    pk = forms.ModelChoiceField(
+        queryset=DashboardWidget.objects.all(),
+        widget=forms.HiddenInput(attrs={"class": "pk-input"}),
+    )
+    order = forms.IntegerField(initial=0, widget=forms.HiddenInput(attrs={"class": "order-input"}))
+
+    class Meta:
+        model = DashboardWidget
+        fields = []
+
+
+DashboardWidgetOrderFormSet = forms.formset_factory(
+    form=DashboardWidgetOrderForm, max_num=0, extra=0
+)
diff --git a/aleksis/core/locale/ar/LC_MESSAGES/django.po b/aleksis/core/locale/ar/LC_MESSAGES/django.po
index 92ad8782664c5f8ec329b96d4054bdc057596d40..a3e781c6b389c7e3c6814fe9491358e6810b1b41 100644
--- a/aleksis/core/locale/ar/LC_MESSAGES/django.po
+++ b/aleksis/core/locale/ar/LC_MESSAGES/django.po
@@ -1506,8 +1506,8 @@ msgstr ""
 msgid ""
 "\n"
 "        To start using a token generator, please use your\n"
-"        smartphone to scan the QR code below. For example, use Google\n"
-"        Authenticator. Then, enter the token generated by the app.\n"
+"        favourite two factor authentication (TOTP) app to scan the QR code below.\n"
+"        Then, enter the token generated by the app.\n"
 "      "
 msgstr ""
 
diff --git a/aleksis/core/locale/de_DE/LC_MESSAGES/django.po b/aleksis/core/locale/de_DE/LC_MESSAGES/django.po
index e59806834f65d4acced6f3adea79b24ccf3b2f3f..6f6d8fc83f732fbd2abdedbb8f4793e2591c1ca1 100644
--- a/aleksis/core/locale/de_DE/LC_MESSAGES/django.po
+++ b/aleksis/core/locale/de_DE/LC_MESSAGES/django.po
@@ -8,7 +8,7 @@ msgstr ""
 "Project-Id-Version: AlekSIS (School Information System) 0.1\n"
 "Report-Msgid-Bugs-To: \n"
 "POT-Creation-Date: 2020-08-02 16:29+0200\n"
-"PO-Revision-Date: 2020-08-02 15:00+0000\n"
+"PO-Revision-Date: 2020-12-19 12:57+0000\n"
 "Last-Translator: Jonathan Weth <teckids@jonathanweth.de>\n"
 "Language-Team: German <https://translate.edugit.org/projects/aleksis/aleksis/"
 "de/>\n"
@@ -17,7 +17,7 @@ msgstr ""
 "Content-Type: text/plain; charset=UTF-8\n"
 "Content-Transfer-Encoding: 8bit\n"
 "Plural-Forms: nplurals=2; plural=n != 1;\n"
-"X-Generator: Weblate 4.0.1\n"
+"X-Generator: Weblate 4.3.2\n"
 
 #: filters.py:37 templates/core/base.html:77 templates/core/group/list.html:20
 #: templates/core/person/list.html:24 templates/search/search.html:7
@@ -412,7 +412,7 @@ msgstr "Kann Kindgruppen zu Gruppen zuordnen"
 
 #: models.py:330
 msgid "Long name"
-msgstr "Langer Name"
+msgstr "Langname"
 
 #: models.py:340 templates/core/group/full.html:65
 msgid "Members"
@@ -1655,14 +1655,14 @@ msgstr ""
 msgid ""
 "\n"
 "        To start using a token generator, please use your\n"
-"        smartphone to scan the QR code below. For example, use Google\n"
-"        Authenticator. Then, enter the token generated by the app.\n"
+"        favourite two factor authentication (TOTP) app to scan the QR code below.\n"
+"        Then, enter the token generated by the app.\n"
 "      "
 msgstr ""
 "\n"
-"        Um mit dem Codegenerator zu starten, benutzen Sie bitte Ihr Smartphone,\n"
-"um diesen QR-Code zu scannen (z. B. den Google Authenticator). Dann geben Sie \n"
-"den in der App angezeigten Code an:\n"
+"        Um mit dem Codegenerator zu starten, benutzen Sie bitte Ihre\n"
+"App für Zwei-Faktor-Authentifizierung (TOTP), um diesen QR-Code zu scannen.\n"
+"Dann geben Sie den in der App angezeigten Code an:\n"
 "      "
 
 #: templates/two_factor/core/setup.html:34
diff --git a/aleksis/core/locale/fr/LC_MESSAGES/django.po b/aleksis/core/locale/fr/LC_MESSAGES/django.po
index d800a36f8aa7db3f7279e2ddfc496e3a4f10ff8f..a8556727d98887123c05abb853a0bd46e829ead4 100644
--- a/aleksis/core/locale/fr/LC_MESSAGES/django.po
+++ b/aleksis/core/locale/fr/LC_MESSAGES/django.po
@@ -1558,8 +1558,8 @@ msgstr ""
 msgid ""
 "\n"
 "        To start using a token generator, please use your\n"
-"        smartphone to scan the QR code below. For example, use Google\n"
-"        Authenticator. Then, enter the token generated by the app.\n"
+"        favourite two factor authentication (TOTP) app to scan the QR code below.\n"
+"        Then, enter the token generated by the app.\n"
 "      "
 msgstr ""
 
diff --git a/aleksis/core/locale/la/LC_MESSAGES/django.po b/aleksis/core/locale/la/LC_MESSAGES/django.po
index 0fe692359d53218a555763890f363f7c9ed9e382..6fa38602c123109a48b5f34b07415b76a666f5a5 100644
--- a/aleksis/core/locale/la/LC_MESSAGES/django.po
+++ b/aleksis/core/locale/la/LC_MESSAGES/django.po
@@ -8,27 +8,26 @@ msgstr ""
 "Project-Id-Version: PACKAGE VERSION\n"
 "Report-Msgid-Bugs-To: \n"
 "POT-Creation-Date: 2020-08-02 16:29+0200\n"
-"PO-Revision-Date: 2020-04-27 13:03+0000\n"
+"PO-Revision-Date: 2020-12-19 12:57+0000\n"
 "Last-Translator: Julian <leuckerj@gmail.com>\n"
-"Language-Team: Latin <https://translate.edugit.org/projects/aleksis/aleksis/la/>\n"
+"Language-Team: Latin <https://translate.edugit.org/projects/aleksis/aleksis/"
+"la/>\n"
 "Language: la\n"
 "MIME-Version: 1.0\n"
 "Content-Type: text/plain; charset=UTF-8\n"
 "Content-Transfer-Encoding: 8bit\n"
 "Plural-Forms: nplurals=2; plural=n != 1;\n"
-"X-Generator: Weblate 4.0.1\n"
+"X-Generator: Weblate 4.3.2\n"
 
 #: filters.py:37 templates/core/base.html:77 templates/core/group/list.html:20
 #: templates/core/person/list.html:24 templates/search/search.html:7
 #: templates/search/search.html:22
 msgid "Search"
-msgstr ""
+msgstr "Quaerere"
 
 #: filters.py:53
-#, fuzzy
-#| msgid "Short name"
 msgid "Search by name"
-msgstr "Breve nomen"
+msgstr "Quaerere cum breve nomine"
 
 #: filters.py:65
 #, fuzzy
@@ -249,7 +248,7 @@ msgstr ""
 
 #: models.py:36
 msgid "Date and time"
-msgstr ""
+msgstr "Dies et hora"
 
 #: models.py:37
 msgid "Decimal number"
@@ -1631,8 +1630,8 @@ msgstr ""
 msgid ""
 "\n"
 "        To start using a token generator, please use your\n"
-"        smartphone to scan the QR code below. For example, use Google\n"
-"        Authenticator. Then, enter the token generated by the app.\n"
+"        favourite two factor authentication (TOTP) app to scan the QR code below.\n"
+"        Then, enter the token generated by the app.\n"
 "      "
 msgstr ""
 
diff --git a/aleksis/core/locale/nb_NO/LC_MESSAGES/django.po b/aleksis/core/locale/nb_NO/LC_MESSAGES/django.po
index 6cfc5ee7e1a2e39ea35727fd66d5bd22d8e8b235..5be24e272186dd1985d749b948562bb13f222bc7 100644
--- a/aleksis/core/locale/nb_NO/LC_MESSAGES/django.po
+++ b/aleksis/core/locale/nb_NO/LC_MESSAGES/django.po
@@ -1505,8 +1505,8 @@ msgstr ""
 msgid ""
 "\n"
 "        To start using a token generator, please use your\n"
-"        smartphone to scan the QR code below. For example, use Google\n"
-"        Authenticator. Then, enter the token generated by the app.\n"
+"        favourite two factor authentication (TOTP) app to scan the QR code below.\n"
+"        Then, enter the token generated by the app.\n"
 "      "
 msgstr ""
 
diff --git a/aleksis/core/locale/tr_TR/LC_MESSAGES/django.po b/aleksis/core/locale/tr_TR/LC_MESSAGES/django.po
index d1efaccfa330d6f6989f9baf39bca163cd3cadf2..d0417b32bd8aa0df398290eacf5a23491641abd2 100644
--- a/aleksis/core/locale/tr_TR/LC_MESSAGES/django.po
+++ b/aleksis/core/locale/tr_TR/LC_MESSAGES/django.po
@@ -1505,8 +1505,8 @@ msgstr ""
 msgid ""
 "\n"
 "        To start using a token generator, please use your\n"
-"        smartphone to scan the QR code below. For example, use Google\n"
-"        Authenticator. Then, enter the token generated by the app.\n"
+"        favourite two factor authentication (TOTP) app to scan the QR code below.\n"
+"        Then, enter the token generated by the app.\n"
 "      "
 msgstr ""
 
diff --git a/aleksis/core/menus.py b/aleksis/core/menus.py
index a690f3b6a72db7e3d8b383cdfc9d39df56c0d4aa..fc9ba63604dabf57e1c4101fc8d7f5700548cc6c 100644
--- a/aleksis/core/menus.py
+++ b/aleksis/core/menus.py
@@ -93,6 +93,17 @@ MENUS = {
                         ),
                     ],
                 },
+                {
+                    "name": _("Dashboard widgets"),
+                    "url": "dashboard_widgets",
+                    "icon": "dashboard",
+                    "validators": [
+                        (
+                            "aleksis.core.util.predicates.permission_validator",
+                            "core.view_dashboardwidget",
+                        ),
+                    ],
+                },
                 {
                     "name": _("Data management"),
                     "url": "data_management",
diff --git a/aleksis/core/migrations/0006_dashboard_widget_size.py b/aleksis/core/migrations/0006_dashboard_widget_size.py
new file mode 100644
index 0000000000000000000000000000000000000000..713ff1792ce87e4124025a34561cd602004ea8c7
--- /dev/null
+++ b/aleksis/core/migrations/0006_dashboard_widget_size.py
@@ -0,0 +1,36 @@
+# Generated by Django 3.1.4 on 2020-12-20 15:55
+
+import django.core.validators
+from django.db import migrations, models
+import django.db.models.deletion
+import django.utils.timezone
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('core', '0005_timestamped_activity_notification'),
+    ]
+
+    operations = [
+        migrations.AddField(
+            model_name='dashboardwidget',
+            name='size_l',
+            field=models.PositiveSmallIntegerField(default=6, help_text='> 992 px, 12 columns', validators=[django.core.validators.MaxValueValidator(12)], verbose_name='Size on desktop devices'),
+        ),
+        migrations.AddField(
+            model_name='dashboardwidget',
+            name='size_m',
+            field=models.PositiveSmallIntegerField(default=12, help_text='> 600 px, 12 columns', validators=[django.core.validators.MaxValueValidator(12)], verbose_name='Size on tablet devices'),
+        ),
+        migrations.AddField(
+            model_name='dashboardwidget',
+            name='size_s',
+            field=models.PositiveSmallIntegerField(default=12, help_text='<= 600 px, 12 columns', validators=[django.core.validators.MaxValueValidator(12)], verbose_name='Size on mobile devices'),
+        ),
+        migrations.AddField(
+            model_name='dashboardwidget',
+            name='size_xl',
+            field=models.PositiveSmallIntegerField(default=4, help_text='> 1200 px>, 12 columns', validators=[django.core.validators.MaxValueValidator(12)], verbose_name='Size on large desktop devices'),
+        ),
+    ]
diff --git a/aleksis/core/migrations/0007_dashboard_widget_order.py b/aleksis/core/migrations/0007_dashboard_widget_order.py
new file mode 100644
index 0000000000000000000000000000000000000000..9387f4ca7c0dbb3307d2a3681f091e4bf631f847
--- /dev/null
+++ b/aleksis/core/migrations/0007_dashboard_widget_order.py
@@ -0,0 +1,35 @@
+# Generated by Django 3.1.4 on 2020-12-21 13:38
+
+import django.contrib.sites.managers
+from django.db import migrations, models
+import django.db.models.deletion
+import django.utils.timezone
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('sites', '0002_alter_domain_unique'),
+        ('core', '0006_dashboard_widget_size'),
+    ]
+
+    operations = [
+        migrations.CreateModel(
+            name='DashboardWidgetOrder',
+            fields=[
+                ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+                ('extended_data', models.JSONField(default=dict, editable=False)),
+                ('order', models.PositiveIntegerField(verbose_name='Order')),
+                ('person', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='core.person', verbose_name='Person')),
+                ('site', models.ForeignKey(default=1, editable=False, on_delete=django.db.models.deletion.CASCADE, to='sites.site')),
+                ('widget', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='core.dashboardwidget', verbose_name='Dashboard widget')),
+            ],
+            options={
+                'verbose_name': 'Dashboard widget order',
+                'verbose_name_plural': 'Dashboard widget orders',
+            },
+            managers=[
+                ('objects', django.contrib.sites.managers.CurrentSiteManager()),
+            ],
+        ),
+    ]
diff --git a/aleksis/core/models.py b/aleksis/core/models.py
index ae13cb588e41550dc747d9aabd9558f207315938..f82ea02ce04e5e6919ca3a39ba899e5080548d82 100644
--- a/aleksis/core/models.py
+++ b/aleksis/core/models.py
@@ -9,6 +9,7 @@ from django.contrib.contenttypes.fields import GenericForeignKey
 from django.contrib.contenttypes.models import ContentType
 from django.contrib.sites.models import Site
 from django.core.exceptions import ValidationError
+from django.core.validators import MaxValueValidator
 from django.db import models, transaction
 from django.db.models import QuerySet
 from django.forms.widgets import Media
@@ -235,6 +236,12 @@ class Person(ExtensibleModel):
                 years -= 1
             return years
 
+    @property
+    def dashboard_widgets(self):
+        return [
+            w.widget for w in DashboardWidgetOrder.objects.filter(person=self).order_by("order")
+        ]
+
     def save(self, *args, **kwargs):
         super().save(*args, **kwargs)
 
@@ -683,6 +690,31 @@ class DashboardWidget(PolymorphicModel, PureDjangoModel):
     title = models.CharField(max_length=150, verbose_name=_("Widget Title"))
     active = models.BooleanField(verbose_name=_("Activate Widget"))
 
+    size_s = models.PositiveSmallIntegerField(
+        verbose_name=_("Size on mobile devices"),
+        help_text=_("<= 600 px, 12 columns"),
+        validators=[MaxValueValidator(12)],
+        default=12,
+    )
+    size_m = models.PositiveSmallIntegerField(
+        verbose_name=_("Size on tablet devices"),
+        help_text=_("> 600 px, 12 columns"),
+        validators=[MaxValueValidator(12)],
+        default=12,
+    )
+    size_l = models.PositiveSmallIntegerField(
+        verbose_name=_("Size on desktop devices"),
+        help_text=_("> 992 px, 12 columns"),
+        validators=[MaxValueValidator(12)],
+        default=6,
+    )
+    size_xl = models.PositiveSmallIntegerField(
+        verbose_name=_("Size on large desktop devices"),
+        help_text=_("> 1200 px>, 12 columns"),
+        validators=[MaxValueValidator(12)],
+        default=4,
+    )
+
     def get_context(self):
         """Get the context dictionary to pass to the widget template."""
         raise NotImplementedError("A widget subclass needs to implement the get_context method.")
@@ -703,6 +735,18 @@ class DashboardWidget(PolymorphicModel, PureDjangoModel):
         verbose_name_plural = _("Dashboard Widgets")
 
 
+class DashboardWidgetOrder(ExtensibleModel):
+    widget = models.ForeignKey(
+        DashboardWidget, on_delete=models.CASCADE, verbose_name=_("Dashboard widget")
+    )
+    person = models.ForeignKey(Person, on_delete=models.CASCADE, verbose_name=_("Person"))
+    order = models.PositiveIntegerField(verbose_name=_("Order"))
+
+    class Meta:
+        verbose_name = _("Dashboard widget order")
+        verbose_name_plural = _("Dashboard widget orders")
+
+
 class CustomMenu(ExtensibleModel):
     """A custom menu to display in the footer."""
 
diff --git a/aleksis/core/rules.py b/aleksis/core/rules.py
index 1ad1099bd441844a5ad4a4baef269174aff1aed0..8ad48fbf18db6cac37270680792aa784a49eb77c 100644
--- a/aleksis/core/rules.py
+++ b/aleksis/core/rules.py
@@ -294,3 +294,15 @@ solve_data_problem_predicate = (
     has_person & view_data_check_results_predicate & has_global_perm("core.solve_data_problem")
 )
 rules.add_perm("core.solve_data_problem", solve_data_problem_predicate)
+
+view_dashboard_widget_predicate = has_person & has_global_perm("core.view_dashboardwidget")
+rules.add_perm("core.view_dashboardwidget", view_dashboard_widget_predicate)
+
+create_dashboard_widget_predicate = has_person & has_global_perm("core.add_dashboardwidget")
+rules.add_perm("core.create_dashboardwidget", create_dashboard_widget_predicate)
+
+edit_dashboard_widget_predicate = has_person & has_global_perm("core.change_dashboardwidget")
+rules.add_perm("core.edit_dashboardwidget", edit_dashboard_widget_predicate)
+
+delete_dashboard_widget_predicate = has_person & has_global_perm("core.delete_dashboardwidget")
+rules.add_perm("core.delete_dashboardwidget", delete_dashboard_widget_predicate)
diff --git a/aleksis/core/settings.py b/aleksis/core/settings.py
index cd92f28e4a3ef71bcb6220711db196452641d3cc..e8870dc656e05c566ab1e83131d7fc4c70e0b9d5 100644
--- a/aleksis/core/settings.py
+++ b/aleksis/core/settings.py
@@ -44,6 +44,23 @@ DEBUG_TOOLBAR_CONFIG = {
     "SHOW_TOOLBAR_CALLBACK": "aleksis.core.util.core_helpers.dt_show_toolbar",
 }
 
+DEBUG_TOOLBAR_PANELS = [
+    "debug_toolbar.panels.versions.VersionsPanel",
+    "debug_toolbar.panels.timer.TimerPanel",
+    "debug_toolbar.panels.settings.SettingsPanel",
+    "debug_toolbar.panels.headers.HeadersPanel",
+    "debug_toolbar.panels.request.RequestPanel",
+    "debug_toolbar.panels.sql.SQLPanel",
+    "debug_toolbar.panels.cache.CachePanel",
+    "debug_toolbar.panels.staticfiles.StaticFilesPanel",
+    "debug_toolbar.panels.templates.TemplatesPanel",
+    "debug_toolbar.panels.signals.SignalsPanel",
+    "debug_toolbar.panels.logging.LoggingPanel",
+    "debug_toolbar.panels.redirects.RedirectsPanel",
+    "debug_toolbar.panels.profiling.ProfilingPanel",
+]
+
+
 ALLOWED_HOSTS = _settings.get("http.allowed_hosts", [])
 
 # Application definition
@@ -99,6 +116,7 @@ INSTALLED_APPS = [
     "colorfield",
     "django_bleach",
     "favicon",
+    "django_filters",
 ]
 
 merge_app_settings("INSTALLED_APPS", INSTALLED_APPS, True)
@@ -183,6 +201,11 @@ if _settings.get("caching.memcached.enabled", False):
             "LOCATION": _settings.get("caching.memcached.address", "127.0.0.1:11211"),
         }
     }
+    INSTALLED_APPS.append("cachalot")
+    DEBUG_TOOLBAR_PANELS.append("cachalot.panels.CachalotPanel")
+    CACHALOT_TIMEOUT = _settings.get("caching.cachalot.timeout", None)
+    CACHALOT_DATABASES = set(["default"])
+    SILENCED_SYSTEM_CHECKS.append("cachalot.W001")
 
 # Password validation
 # https://docs.djangoproject.com/en/2.1/ref/settings/#auth-password-validators
@@ -329,6 +352,8 @@ YARN_INSTALLED_APPS = [
     "select2",
     "select2-materialize",
     "paper-css",
+    "jquery-sortablejs",
+    "sortablejs",
 ]
 
 merge_app_settings("YARN_INSTALLED_APPS", YARN_INSTALLED_APPS, True)
@@ -352,6 +377,8 @@ ANY_JS = {
         "css_url": JS_URL + "/select2-materialize/select2-materialize.css",
         "js_url": JS_URL + "/select2-materialize/index.js",
     },
+    "sortablejs": {"js_url": JS_URL + "/sortablejs/dist/sortable.umd.js"},
+    "jquery-sortablejs": {"js_url": JS_URL + "/jquery-sortablejs/jquery-sortable.js"},
 }
 
 merge_app_settings("ANY_JS", ANY_JS, True)
diff --git a/aleksis/core/static/js/edit_dashboard.js b/aleksis/core/static/js/edit_dashboard.js
new file mode 100644
index 0000000000000000000000000000000000000000..0cc90de60305497a682d219b121e77550d273d1d
--- /dev/null
+++ b/aleksis/core/static/js/edit_dashboard.js
@@ -0,0 +1,22 @@
+function refreshOrder() {
+    $(".order-input").val(0);
+    $("#widgets > .col").each(function (index) {
+        const order = (index + 1) * 10;
+        let pk = $(this).attr("data-pk");
+        let sel = $("#order-form input[value=" + pk + "].pk-input").next();
+        sel.val(order);
+    })
+}
+
+$(document).ready(function () {
+    $('#not-used-widgets').sortable({
+        group: 'widgets',
+        animation: 150,
+        onEnd: refreshOrder
+    });
+    $('#widgets').sortable({
+        group: 'widgets',
+        animation: 150,
+        onEnd: refreshOrder
+    });
+});
diff --git a/aleksis/core/static/js/serviceworker.js b/aleksis/core/static/js/serviceworker.js
index 93d9797c78db9756cba6f1f6caa46fc8c14ae57b..818e27cb7d28e820431a4ac28ba6fdf0c422deb7 100644
--- a/aleksis/core/static/js/serviceworker.js
+++ b/aleksis/core/static/js/serviceworker.js
@@ -1,35 +1,9 @@
-// This is the AlekSIS service worker
-
-const CACHE = "aleksis-cache";
-
-const precacheFiles = [
-    '',
-];
 
-const offlineFallbackPage = '/offline';
-
-const avoidCachingPaths = [
-    '/admin',
-    '/settings',
-    '/accounts/login'
-]; // TODO: More paths are needed
-
-function pathComparer(requestUrl, pathRegEx) {
-    return requestUrl.match(new RegExp(pathRegEx));
-}
+// This is the AlekSIS service worker
 
-function comparePaths(requestUrl, pathsArray) {
-    if (requestUrl) {
-        for (let index = 0; index < pathsArray.length; index++) {
-            const pathRegEx = pathsArray[index];
-            if (pathComparer(requestUrl, pathRegEx)) {
-                return true;
-            }
-        }
-    }
+const CACHE = 'aleksis-cache';
 
-    return false;
-}
+const offlineFallbackPage = 'offline/';
 
 self.addEventListener("install", function (event) {
     console.log("[AlekSIS PWA] Install Event processing.");
@@ -40,10 +14,7 @@ self.addEventListener("install", function (event) {
     event.waitUntil(
         caches.open(CACHE).then(function (cache) {
             console.log("[AlekSIS PWA] Caching pages during install.");
-
-            return cache.addAll(precacheFiles).then(function () {
-                return cache.add(offlineFallbackPage);
-            });
+            return cache.add(offlineFallbackPage);
         })
     );
 });
@@ -95,11 +66,11 @@ function fromCache(event) {
 }
 
 function updateCache(request, response) {
-    if (!comparePaths(request.url, avoidCachingPaths)) {
+    if (response.headers.get('cache-control') && response.headers.get('cache-control').includes('no-cache')) {
+        return Promise.resolve();
+    } else {
         return caches.open(CACHE).then(function (cache) {
             return cache.put(request, response);
         });
     }
-
-    return Promise.resolve();
 }
diff --git a/aleksis/core/static/style.scss b/aleksis/core/static/style.scss
index e2ea3bbd9953477469350e1c477556947bf8f0e2..81f443f75ab79c34121390bf03da9ab1e611dd5b 100644
--- a/aleksis/core/static/style.scss
+++ b/aleksis/core/static/style.scss
@@ -70,6 +70,10 @@ header, main, footer {
   }
 }
 
+.materialize-circle {
+  @extend .circle;
+}
+
 /**********/
 /* HEADER */
 /**********/
@@ -622,3 +626,7 @@ main .alert p:first-child, main .alert div:first-child {
   overflow: visible;
   width: 100%;
 }
+
+.draggable {
+  cursor: grab;
+}
diff --git a/aleksis/core/tables.py b/aleksis/core/tables.py
index f562b61a151487c5047ef2861255a00ef7a5f785..54d3f3e5c75e4c5ac0dca4b8ccfe868f70b7efb2 100644
--- a/aleksis/core/tables.py
+++ b/aleksis/core/tables.py
@@ -70,3 +70,24 @@ class GroupTypesTable(tables.Table):
     delete = tables.LinkColumn(
         "delete_group_type_by_id", args=[A("id")], verbose_name=_("Delete"), text=_("Delete")
     )
+
+
+class DashboardWidgetTable(tables.Table):
+    """Table to list dashboard widgets."""
+
+    class Meta:
+        attrs = {"class": "responsive-table highlight"}
+
+    widget_name = tables.Column(accessor="pk")
+    title = tables.LinkColumn("edit_dashboard_widget", args=[A("id")])
+    active = tables.BooleanColumn(yesno="check,cancel", attrs={"span": {"class": "material-icons"}})
+    delete = tables.LinkColumn(
+        "delete_dashboard_widget",
+        args=[A("id")],
+        text=_("Delete"),
+        attrs={"a": {"class": "btn-flat waves-effect waves-red red-text"}},
+        verbose_name=_("Actions"),
+    )
+
+    def render_widget_name(self, value, record):
+        return record._meta.verbose_name
diff --git a/aleksis/core/templates/core/base.html b/aleksis/core/templates/core/base.html
index 07db50c7fb3a29dc169543ae03c4f26dff1ec25f..2087829da24c157b7dc0a797da776d7bb1cc9a3b 100644
--- a/aleksis/core/templates/core/base.html
+++ b/aleksis/core/templates/core/base.html
@@ -170,6 +170,8 @@
 
 
 {% include_js "materialize" %}
+{% include_js "sortablejs" %}
+{% include_js "jquery-sortablejs" %}
 <script type="text/javascript" src="{% static 'js/search.js' %}"></script>
 <script type="text/javascript" src="{% static 'js/main.js' %}"></script>
 </body>
diff --git a/aleksis/core/templates/core/dashboard_widget/create.html b/aleksis/core/templates/core/dashboard_widget/create.html
new file mode 100644
index 0000000000000000000000000000000000000000..cfaa296eefc7afb0abce56ce1802eeba2ef70a76
--- /dev/null
+++ b/aleksis/core/templates/core/dashboard_widget/create.html
@@ -0,0 +1,23 @@
+{# -*- engine:django -*- #}
+
+{% extends "core/base.html" %}
+{% load material_form i18n data_helpers %}
+
+{% block browser_title %}
+  {% verbose_name_object model as widget_title %}
+  {% blocktrans with widget=widget_title %}Create {{ widget }}{% endblocktrans %}
+{% endblock %}
+{% block page_title %}
+  {% verbose_name_object model as widget_title %}
+  {% blocktrans with widget=widget_title %}Create {{ widget }}{% endblocktrans %}
+{% endblock %}
+
+{% block content %}
+
+  <form method="post">
+    {% csrf_token %}
+    {% form form=form %}{% endform %}
+    {% include "core/partials/save_button.html" %}
+  </form>
+
+{% endblock %}
diff --git a/aleksis/core/templates/core/dashboard_widget/edit.html b/aleksis/core/templates/core/dashboard_widget/edit.html
new file mode 100644
index 0000000000000000000000000000000000000000..64dfe0eed64c36257fed529475f355d0ee2e8fc0
--- /dev/null
+++ b/aleksis/core/templates/core/dashboard_widget/edit.html
@@ -0,0 +1,23 @@
+{# -*- engine:django -*- #}
+
+{% extends "core/base.html" %}
+{% load material_form i18n data_helpers %}
+
+{% block browser_title %}
+  {% verbose_name_object object as widget_title %}
+  {% blocktrans with widget=widget_title %}Edit {{ widget }}{% endblocktrans %}
+{% endblock %}
+{% block page_title %}
+  {% verbose_name_object object as widget_title %}
+  {% blocktrans with widget=widget_title %}Edit {{ widget }}{% endblocktrans %}
+{% endblock %}
+
+{% block content %}
+
+  <form method="post">
+    {% csrf_token %}
+    {% form form=form %}{% endform %}
+    {% include "core/partials/save_button.html" %}
+  </form>
+
+{% endblock %}
diff --git a/aleksis/core/templates/core/dashboard_widget/list.html b/aleksis/core/templates/core/dashboard_widget/list.html
new file mode 100644
index 0000000000000000000000000000000000000000..ab384e8620f7c6499adb3e663fdb98e4eb598b2c
--- /dev/null
+++ b/aleksis/core/templates/core/dashboard_widget/list.html
@@ -0,0 +1,22 @@
+{# -*- engine:django -*- #}
+
+{% extends "core/base.html" %}
+
+{% load i18n data_helpers %}
+{% load render_table from django_tables2 %}
+
+{% block browser_title %}{% blocktrans %}Dashboard widgets{% endblocktrans %}{% endblock %}
+{% block page_title %}{% blocktrans %}Dashboard widgets{% endblocktrans %}{% endblock %}
+
+{% block content %}
+
+  {% for ct, model in widget_types %}
+    <a class="btn green waves-effect waves-light" href="{% url 'create_dashboard_widget' ct.app_label ct.model  %}">
+      <i class="material-icons left">add</i>
+      {% verbose_name_object model as widget_name %}
+      {% blocktrans with name=widget_name %}Create {{ name }}{% endblocktrans %}
+    </a>
+  {% endfor %}
+
+  {% render_table table %}
+{% endblock %}
diff --git a/aleksis/core/templates/core/edit_dashboard.html b/aleksis/core/templates/core/edit_dashboard.html
new file mode 100644
index 0000000000000000000000000000000000000000..a15f24bff8ebcca6a7f9b4ea081d9d8c008f3b0a
--- /dev/null
+++ b/aleksis/core/templates/core/edit_dashboard.html
@@ -0,0 +1,41 @@
+{% extends 'core/base.html' %}
+{% load i18n static dashboard any_js %}
+
+{% block browser_title %}{% blocktrans %}Edit dashboard{% endblocktrans %}{% endblock %}
+{% block page_title %}{% blocktrans %}Edit dashboard{% endblocktrans %}{% endblock %}
+
+{% block content %}
+  <div class="alert primary">
+    <p>
+      <i class="material-icons left">info</i>
+      On this page you can arrange your personal dashboard. You can drag any items from "Available widgets" to "Your
+      Dashboard" or change the order by moving the widgets. After you have finished, please don't forget to click on
+      "Save".
+    </p>
+  </div>
+
+  <form action="" method="post" id="order-form">
+    {% csrf_token %}
+    {{ formset.management_form }}
+    {% for form in formset %}
+      {{ form.as_p }}
+    {% endfor %}
+    {% include "core/partials/save_button.html" %}
+  </form>
+
+  <h5>{% trans "Available widgets" %}</h5>
+  <div class="row card-panel grey lighten-3" id="not-used-widgets">
+    {% for widget in not_used_widgets %}
+      {% include "core/partials/edit_dashboard_widget.html" %}
+    {% endfor %}
+  </div>
+
+  <h5>{% trans "Your dashboard" %}</h5>
+  <div class="row card-panel grey lighten-3" id="widgets">
+    {% for widget in widgets %}
+      {% include "core/partials/edit_dashboard_widget.html" %}
+    {% endfor %}
+  </div>
+
+  <script src="{% static "js/edit_dashboard.js" %}"></script>
+{% endblock %}
diff --git a/aleksis/core/templates/core/index.html b/aleksis/core/templates/core/index.html
index bcf0bf1b4180756d53f1519b8ee2371ee3e4e658..93b80ddefffaa8cf72db56a7ef503a694f0514f5 100644
--- a/aleksis/core/templates/core/index.html
+++ b/aleksis/core/templates/core/index.html
@@ -2,13 +2,21 @@
 {% load i18n static dashboard %}
 
 {% block browser_title %}{% blocktrans %}Home{% endblocktrans %}{% endblock %}
-{% block page_title %}{{ request.site.preferences.general__title }}{% endblock %}
+{% block no_page_title %}{% endblock %}
 
 {% block extra_head %}
   {{ media }}
 {% endblock %}
 
 {% block content %}
+  <a class="btn-flat waves-effect waves-light right" href="{% url "edit_dashboard" %}">
+    <i class="material-icons left">edit</i>
+    {% trans "Edit dashboard" %}
+  </a>
+  <h4>
+    {{ request.site.preferences.general__title }}
+  </h4>
+
   {% if user.is_authenticated %}
     {% for notification in unread_notifications %}
       <div class="alert primary scale-transition">
@@ -31,9 +39,19 @@
 
     <div class="row" id="live_load">
       {% for widget in widgets %}
-        <div class="col s12 m12 l6 xl4">
+        <div class="col s{{ widget.size_s }} m{{ widget.size_m }} l{{ widget.size_l }} xl{{ widget.size_xl }}">
           {% include_widget widget %}
         </div>
+        {% empty %}
+        <div class="col s12 grey-text center">
+          <i class="material-icons medium ">widgets</i>
+          <p class="flow-text">
+            {% blocktrans %}
+              You haven't selected any dashboard widgets. Please click on "Edit dashboard" to add widgets to your
+              personal dashboard.
+            {% endblocktrans %}
+          </p>
+        </div>
       {% endfor %}
     </div>
 
diff --git a/aleksis/core/templates/core/partials/edit_dashboard_widget.html b/aleksis/core/templates/core/partials/edit_dashboard_widget.html
new file mode 100644
index 0000000000000000000000000000000000000000..b842c0937f05c4e90a4d7337871460130fa43012
--- /dev/null
+++ b/aleksis/core/templates/core/partials/edit_dashboard_widget.html
@@ -0,0 +1,9 @@
+<div class="col draggable s{{ widget.size_s }} m{{ widget.size_m }} l{{ widget.size_l }} xl{{ widget.size_xl }}"
+     data-pk="{{ widget.pk }}">
+  <div class="card placeholder">
+    <div class="card-content">
+      <i class="material-icons left small">drag_handle</i>
+      <span class="card-title">{{ widget.title }}</span>
+    </div>
+  </div>
+</div>
diff --git a/aleksis/core/templates/core/pages/offline.html b/aleksis/core/templates/offline.html
similarity index 87%
rename from aleksis/core/templates/core/pages/offline.html
rename to aleksis/core/templates/offline.html
index a6a70dc19f074e8c3f3ede50b5e9b5b80b5682f1..bd741268a8902ac6708f33e353337695c03158a3 100644
--- a/aleksis/core/templates/core/pages/offline.html
+++ b/aleksis/core/templates/offline.html
@@ -2,6 +2,8 @@
 
 {% load i18n %}
 
+{% block browser_title %}{% blocktrans %}Network error{% endblocktrans %}{% endblock %}
+
 {% block content %}
   <h3><i class="material-icons left medium" style="font-size: 2.92rem;">signal_wifi_off</i>{% blocktrans %}No internet
     connection.{% endblocktrans %}</h3>
diff --git a/aleksis/core/templates/two_factor/core/setup.html b/aleksis/core/templates/two_factor/core/setup.html
index 2eb4ecb2828ac60104f284b2d75857e9b6be303b..8048403964a5bd4b7c9761d0189f18bb53524a22 100644
--- a/aleksis/core/templates/two_factor/core/setup.html
+++ b/aleksis/core/templates/two_factor/core/setup.html
@@ -22,8 +22,8 @@
     <p>
       {% blocktrans %}
         To start using a token generator, please use your
-        smartphone to scan the QR code below. For example, use Google
-        Authenticator. Then, enter the token generated by the app.
+        favourite two factor authentication (TOTP) app to scan the QR code below.
+        Then, enter the token generated by the app.
       {% endblocktrans %}
     </p>
     <p>
diff --git a/aleksis/core/urls.py b/aleksis/core/urls.py
index 161730a310f43e015a472e5d1565400b6362ed37..930b3fbc8e8bec19bd32cadb6a6fd6bd7833f0f7 100644
--- a/aleksis/core/urls.py
+++ b/aleksis/core/urls.py
@@ -57,6 +57,7 @@ urlpatterns = [
     path("group/<int:id_>/edit", views.edit_group, name="edit_group_by_id"),
     path("group/<int:id_>/delete", views.delete_group, name="delete_group_by_id"),
     path("", views.index, name="index"),
+    path("dashboard/edit/", views.EditDashboardView.as_view(), name="edit_dashboard"),
     path(
         "notifications/mark-read/<int:id_>",
         views.notification_mark_read,
@@ -160,6 +161,22 @@ urlpatterns = [
         views.SolveDataCheckView.as_view(),
         name="data_check_solve",
     ),
+    path("dashboard_widgets/", views.DashboardWidgetListView.as_view(), name="dashboard_widgets"),
+    path(
+        "dashboard_widgets/<int:pk>/edit/",
+        views.DashboardWidgetEditView.as_view(),
+        name="edit_dashboard_widget",
+    ),
+    path(
+        "dashboard_widgets/<int:pk>/delete/",
+        views.DashboardWidgetDeleteView.as_view(),
+        name="delete_dashboard_widget",
+    ),
+    path(
+        "dashboard_widgets/<str:app>/<str:model>/new/",
+        views.DashboardWidgetCreateView.as_view(),
+        name="create_dashboard_widget",
+    ),
 ]
 
 # Serve static files from STATIC_ROOT to make it work with runserver
diff --git a/aleksis/core/util/manage.py b/aleksis/core/util/manage.py
new file mode 100644
index 0000000000000000000000000000000000000000..64441f62dfdadbb589b973dcd65633e100bf647b
--- /dev/null
+++ b/aleksis/core/util/manage.py
@@ -0,0 +1,12 @@
+"""Management utilities for an AlekSIS installation."""
+
+import os
+import sys
+
+from django.core.management import execute_from_command_line
+
+
+def aleksis_cmd():
+    """Run django-admin command with correct settings path."""
+    os.environ.setdefault("DJANGO_SETTINGS_MODULE", "aleksis.core.settings")
+    execute_from_command_line(sys.argv)
diff --git a/aleksis/core/views.py b/aleksis/core/views.py
index 4f2bdb1c5f9032d57a6f5f546d77a35c67118922..fd4b163bd454d6008054780903376b12b7141629 100644
--- a/aleksis/core/views.py
+++ b/aleksis/core/views.py
@@ -1,14 +1,18 @@
-from typing import Any, Dict, Optional
+from typing import Any, Dict, Optional, Type
 
 from django.apps import apps
 from django.conf import settings
+from django.contrib.contenttypes.models import ContentType
 from django.core.exceptions import PermissionDenied
 from django.core.paginator import Paginator
 from django.db.models import QuerySet
+from django.forms.models import BaseModelForm, modelform_factory
 from django.http import HttpRequest, HttpResponse, HttpResponseNotFound
 from django.shortcuts import get_object_or_404, redirect, render
 from django.urls import reverse_lazy
+from django.utils.decorators import method_decorator
 from django.utils.translation import gettext_lazy as _
+from django.views.decorators.cache import never_cache
 from django.views.generic.base import View
 from django.views.generic.detail import DetailView
 from django.views.generic.list import ListView
@@ -31,6 +35,7 @@ from .filters import GroupFilter, PersonFilter
 from .forms import (
     AnnouncementForm,
     ChildGroupsForm,
+    DashboardWidgetOrderFormSet,
     EditAdditionalFieldForm,
     EditGroupForm,
     EditGroupTypeForm,
@@ -41,11 +46,12 @@ from .forms import (
     SchoolTermForm,
     SitePreferenceForm,
 )
-from .mixins import AdvancedCreateView, AdvancedEditView
+from .mixins import AdvancedCreateView, AdvancedDeleteView, AdvancedEditView
 from .models import (
     AdditionalField,
     Announcement,
     DashboardWidget,
+    DashboardWidgetOrder,
     DataCheckResult,
     Group,
     GroupType,
@@ -60,6 +66,7 @@ from .registries import (
 )
 from .tables import (
     AdditionalFieldsTable,
+    DashboardWidgetTable,
     GroupsTable,
     GroupTypesTable,
     PersonsTable,
@@ -86,7 +93,7 @@ def index(request: HttpRequest) -> HttpResponse:
     announcements = Announcement.objects.at_time().for_person(request.user.person)
     context["announcements"] = announcements
 
-    widgets = DashboardWidget.objects.filter(active=True)
+    widgets = request.user.person.dashboard_widgets
     media = DashboardWidget.get_media(widgets)
 
     context["widgets"] = widgets
@@ -95,11 +102,6 @@ def index(request: HttpRequest) -> HttpResponse:
     return render(request, "core/index.html", context)
 
 
-def offline(request: HttpRequest) -> HttpResponse:
-    """Offline message for PWA."""
-    return render(request, "core/pages/offline.html")
-
-
 def about(request: HttpRequest) -> HttpResponse:
     """About page listing all apps."""
     context = {}
@@ -120,6 +122,7 @@ class SchoolTermListView(SingleTableView, PermissionRequiredMixin):
     template_name = "core/school_term/list.html"
 
 
+@method_decorator(never_cache, name="dispatch")
 class SchoolTermCreateView(AdvancedCreateView, PermissionRequiredMixin):
     """Create view for school terms."""
 
@@ -131,6 +134,7 @@ class SchoolTermCreateView(AdvancedCreateView, PermissionRequiredMixin):
     success_message = _("The school term has been created.")
 
 
+@method_decorator(never_cache, name="dispatch")
 class SchoolTermEditView(AdvancedEditView, PermissionRequiredMixin):
     """Edit view for school terms."""
 
@@ -238,6 +242,7 @@ def groups(request: HttpRequest) -> HttpResponse:
     return render(request, "core/group/list.html", context)
 
 
+@never_cache
 @permission_required("core.link_persons_accounts")
 def persons_accounts(request: HttpRequest) -> HttpResponse:
     """View allowing to batch-process linking of users to persons."""
@@ -258,6 +263,7 @@ def persons_accounts(request: HttpRequest) -> HttpResponse:
     return render(request, "core/person/accounts.html", context)
 
 
+@never_cache
 @permission_required("core.assign_child_groups_to_groups")
 def groups_child_groups(request: HttpRequest) -> HttpResponse:
     """View for batch-processing assignment from child groups to groups."""
@@ -295,6 +301,7 @@ def groups_child_groups(request: HttpRequest) -> HttpResponse:
     return render(request, "core/group/child_groups.html", context)
 
 
+@never_cache
 @permission_required("core.edit_person", fn=objectgetter_optional(Person))
 def edit_person(request: HttpRequest, id_: Optional[int] = None) -> HttpResponse:
     """Edit view for a single person, defaulting to logged-in person."""
@@ -333,6 +340,7 @@ def get_group_by_id(request: HttpRequest, id_: Optional[int] = None):
         return None
 
 
+@never_cache
 @permission_required("core.edit_group", fn=objectgetter_optional(Group, None, False))
 def edit_group(request: HttpRequest, id_: Optional[int] = None) -> HttpResponse:
     """View to edit or create a group."""
@@ -426,6 +434,7 @@ def announcements(request: HttpRequest) -> HttpResponse:
     return render(request, "core/announcement/list.html", context)
 
 
+@never_cache
 @permission_required(
     "core.create_or_edit_announcement", fn=objectgetter_optional(Announcement, None, False)
 )
@@ -493,6 +502,7 @@ class PermissionSearchView(PermissionRequiredMixin, SearchView):
         return render(self.request, self.template, context)
 
 
+@never_cache
 def preferences(
     request: HttpRequest,
     registry_name: str = "person",
@@ -578,6 +588,7 @@ def delete_group(request: HttpRequest, id_: int) -> HttpResponse:
     return redirect("groups")
 
 
+@never_cache
 @permission_required(
     "core.change_additionalfield", fn=objectgetter_optional(AdditionalField, None, False)
 )
@@ -643,6 +654,7 @@ def delete_additional_field(request: HttpRequest, id_: int) -> HttpResponse:
     return redirect("additional_fields")
 
 
+@never_cache
 @permission_required("core.change_grouptype", fn=objectgetter_optional(GroupType, None, False))
 def edit_group_type(request: HttpRequest, id_: Optional[int] = None) -> HttpResponse:
     """View to edit or create a group_type."""
@@ -752,3 +764,133 @@ class SolveDataCheckView(PermissionRequiredMixin, RevisionMixin, DetailView):
             return redirect("check_data")
         else:
             return HttpResponseNotFound()
+
+
+class DashboardWidgetListView(SingleTableView, PermissionRequiredMixin):
+    """Table of all dashboard widgets."""
+
+    model = DashboardWidget
+    table_class = DashboardWidgetTable
+    permission_required = "core.view_dashboardwidget"
+    template_name = "core/dashboard_widget/list.html"
+
+    def get_context_data(self, **kwargs: Any) -> Dict[str, Any]:
+        context = super().get_context_data(**kwargs)
+        context["widget_types"] = [
+            (ContentType.objects.get_for_model(m, False), m)
+            for m in DashboardWidget.__subclasses__()
+        ]
+        return context
+
+
+@method_decorator(never_cache, name="dispatch")
+class DashboardWidgetEditView(AdvancedEditView, PermissionRequiredMixin):
+    """Edit view for dashboard widgets."""
+
+    def get_form_class(self) -> Type[BaseModelForm]:
+        return modelform_factory(self.object.__class__, fields=self.fields)
+
+    model = DashboardWidget
+    fields = "__all__"
+    permission_required = "core.edit_dashboardwidget"
+    template_name = "core/dashboard_widget/edit.html"
+    success_url = reverse_lazy("dashboard_widgets")
+    success_message = _("The dashboard widget has been saved.")
+
+
+@method_decorator(never_cache, name="dispatch")
+class DashboardWidgetCreateView(AdvancedCreateView, PermissionRequiredMixin):
+    """Create view for dashboard widgets."""
+
+    def get_model(self, request, *args, **kwargs):
+        app_label = kwargs.get("app")
+        model = kwargs.get("model")
+        ct = get_object_or_404(ContentType, app_label=app_label, model=model)
+        return ct.model_class()
+
+    def get_context_data(self, **kwargs: Any) -> Dict[str, Any]:
+        context = super().get_context_data(**kwargs)
+        context["model"] = self.model
+        return context
+
+    def get(self, request, *args, **kwargs):
+        self.model = self.get_model(request, *args, **kwargs)
+        return super().get(request, *args, **kwargs)
+
+    def post(self, request, *args, **kwargs):
+        self.model = self.get_model(request, *args, **kwargs)
+        return super().post(request, *args, **kwargs)
+
+    fields = "__all__"
+    permission_required = "core.add_dashboardwidget"
+    template_name = "core/dashboard_widget/create.html"
+    success_url = reverse_lazy("dashboard_widgets")
+    success_message = _("The dashboard widget has been created.")
+
+
+class DashboardWidgetDeleteView(PermissionRequiredMixin, AdvancedDeleteView):
+    """Delete view for dashboard widgets."""
+
+    model = DashboardWidget
+    permission_required = "core.delete_dashboardwidget"
+    template_name = "core/pages/delete.html"
+    success_url = reverse_lazy("dashboard_widgets")
+    success_message = _("The dashboard widget has been deleted.")
+
+
+class EditDashboardView(View):
+    """View for editing dashboard widget order."""
+
+    def get_context_data(self, request):
+        context = {}
+
+        widgets = request.user.person.dashboard_widgets
+        not_used_widgets = DashboardWidget.objects.exclude(pk__in=[w.pk for w in widgets])
+        context["widgets"] = widgets
+        context["not_used_widgets"] = not_used_widgets
+
+        order = 10
+        initial = []
+        for widget in widgets:
+            initial.append({"pk": widget, "order": order})
+            order += 10
+        for widget in not_used_widgets:
+            initial.append({"pk": widget, "order": 0})
+
+        formset = DashboardWidgetOrderFormSet(
+            request.POST or None, initial=initial, prefix="widget_order"
+        )
+        context["formset"] = formset
+
+        return context
+
+    def post(self, request):
+        context = self.get_context_data(request)
+
+        if context["formset"].is_valid():
+            added_objects = []
+            for form in context["formset"]:
+                if not form.cleaned_data["order"]:
+                    continue
+
+                obj, created = DashboardWidgetOrder.objects.update_or_create(
+                    widget=form.cleaned_data["pk"],
+                    person=request.user.person,
+                    defaults={"order": form.cleaned_data["order"]},
+                )
+
+                added_objects.append(obj.pk)
+
+            DashboardWidgetOrder.objects.filter(person=request.user.person).exclude(
+                pk__in=added_objects
+            ).delete()
+
+            messages.success(
+                request, _("Your dashboard configuration has been saved successfully.")
+            )
+            return redirect("index")
+
+    def get(self, request):
+        context = self.get_context_data(request)
+
+        return render(request, "core/edit_dashboard.html", context=context)
diff --git a/poetry.lock b/poetry.lock
index 2896275e72cc9c004709eb5a089be772087e9fea..be0e61cd822675c59e9c9fc318fd2f104c7f900a 100644
--- a/poetry.lock
+++ b/poetry.lock
@@ -109,16 +109,16 @@ pytz = ">=2015.7"
 
 [[package]]
 name = "bandit"
-version = "1.6.2"
+version = "1.7.0"
 description = "Security oriented static analyser for python code."
 category = "dev"
 optional = false
-python-versions = "*"
+python-versions = ">=3.5"
 
 [package.dependencies]
 colorama = {version = ">=0.3.9", markers = "platform_system == \"Windows\""}
 GitPython = ">=1.0.1"
-PyYAML = ">=3.13"
+PyYAML = ">=5.3.1"
 six = ">=1.10.0"
 stevedore = ">=1.20.0"
 
@@ -184,7 +184,7 @@ django = ["Django (>=2.2,<4.0)"]
 
 [[package]]
 name = "celery"
-version = "5.0.2"
+version = "5.0.5"
 description = "Distributed Task Queue."
 category = "main"
 optional = true
@@ -192,8 +192,9 @@ python-versions = ">=3.6,"
 
 [package.dependencies]
 billiard = ">=3.6.3.0,<4.0"
-click = ">=7.0"
+click = ">=7.0,<8.0"
 click-didyoumean = ">=0.0.3"
+click-plugins = ">=1.1.1"
 click-repl = ">=0.1.6"
 Django = {version = ">=1.11", optional = true, markers = "extra == \"django\""}
 kombu = ">=5.0.0,<6.0"
@@ -223,6 +224,7 @@ mongodb = ["pymongo[srv] (>=3.3.0)"]
 msgpack = ["msgpack"]
 pymemcache = ["python-memcached"]
 pyro = ["pyro4"]
+pytest = ["pytest-celery"]
 redis = ["redis (>=3.2.0)"]
 s3 = ["boto3 (>=1.9.125)"]
 slmq = ["softlayer-messaging (>=1.0.3)"]
@@ -260,7 +262,7 @@ websockets = ["channels"]
 
 [[package]]
 name = "certifi"
-version = "2020.11.8"
+version = "2020.12.5"
 description = "Python package for providing Mozilla's CA Bundle."
 category = "main"
 optional = false
@@ -268,11 +270,11 @@ python-versions = "*"
 
 [[package]]
 name = "chardet"
-version = "3.0.4"
+version = "4.0.0"
 description = "Universal encoding detector for Python 2 and 3"
 category = "main"
 optional = false
-python-versions = "*"
+python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
 
 [[package]]
 name = "click"
@@ -293,6 +295,20 @@ python-versions = "*"
 [package.dependencies]
 click = "*"
 
+[[package]]
+name = "click-plugins"
+version = "1.1.1"
+description = "An extension module for click to enable registering CLI commands via setuptools entry-points."
+category = "main"
+optional = true
+python-versions = "*"
+
+[package.dependencies]
+click = ">=4.0"
+
+[package.extras]
+dev = ["pytest (>=3.6)", "pytest-cov", "wheel", "coveralls"]
+
 [[package]]
 name = "click-repl"
 version = "0.1.6"
@@ -357,7 +373,7 @@ python-versions = "*"
 
 [[package]]
 name = "django"
-version = "3.1.3"
+version = "3.1.4"
 description = "A high-level Python Web framework that encourages rapid development and clean, pragmatic design."
 category = "main"
 optional = false
@@ -429,6 +445,17 @@ python-versions = "*"
 [package.dependencies]
 Django = ">=1.8"
 
+[[package]]
+name = "django-cachalot"
+version = "2.3.3"
+description = "Caches your Django ORM queries and automatically invalidates them."
+category = "main"
+optional = false
+python-versions = "*"
+
+[package.dependencies]
+Django = ">=2"
+
 [[package]]
 name = "django-cache-memoize"
 version = "0.1.7"
@@ -704,7 +731,7 @@ django = "*"
 
 [[package]]
 name = "django-model-utils"
-version = "4.0.0"
+version = "4.1.1"
 description = "Django model mixins and utilities"
 category = "main"
 optional = false
@@ -825,7 +852,7 @@ management-command = ["django-compressor (>=2.4)"]
 
 [[package]]
 name = "django-select2"
-version = "7.4.2"
+version = "7.5.0"
 description = "Select2 option fields for Django"
 category = "main"
 optional = false
@@ -887,7 +914,7 @@ six = ">=1"
 
 [[package]]
 name = "django-timezone-field"
-version = "4.0"
+version = "4.1.1"
 description = "A Django app providing database and form fields for pytz timezone objects."
 category = "main"
 optional = true
@@ -897,6 +924,9 @@ python-versions = ">=3.5"
 django = ">=2.2"
 pytz = "*"
 
+[package.extras]
+rest_framework = ["djangorestframework (>=3.0.0)"]
+
 [[package]]
 name = "django-two-factor-auth"
 version = "1.13"
@@ -990,11 +1020,11 @@ yaml = ["ruamel.yaml"]
 
 [[package]]
 name = "faker"
-version = "4.17.1"
+version = "5.0.2"
 description = "Faker is a Python package that generates fake data for you."
 category = "main"
 optional = false
-python-versions = ">=3.5"
+python-versions = ">=3.6"
 
 [package.dependencies]
 python-dateutil = ">=2.4"
@@ -1185,18 +1215,19 @@ python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
 
 [[package]]
 name = "importlib-metadata"
-version = "3.1.0"
+version = "3.3.0"
 description = "Read metadata from Python packages"
 category = "main"
 optional = false
 python-versions = ">=3.6"
 
 [package.dependencies]
+typing-extensions = {version = ">=3.6.4", markers = "python_version < \"3.8\""}
 zipp = ">=0.5"
 
 [package.extras]
-docs = ["sphinx", "rst.linker"]
-testing = ["packaging", "pep517", "unittest2", "importlib-resources (>=1.3)"]
+docs = ["sphinx", "jaraco.packaging (>=3.2)", "rst.linker (>=1.9)"]
+testing = ["pytest (>=3.5,!=3.7.3)", "pytest-checkdocs (>=1.2.3)", "pytest-flake8", "pytest-cov", "jaraco.test (>=3.2.0)", "packaging", "pep517", "pyfakefs", "flufl.flake8", "pytest-black (>=0.3.7)", "pytest-mypy", "importlib-resources (>=1.3)"]
 
 [[package]]
 name = "iniconfig"
@@ -1325,7 +1356,7 @@ python-versions = "*"
 
 [[package]]
 name = "packaging"
-version = "20.4"
+version = "20.8"
 description = "Core utilities for Python packages"
 category = "main"
 optional = false
@@ -1333,7 +1364,6 @@ python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
 
 [package.dependencies]
 pyparsing = ">=2.0.2"
-six = "*"
 
 [[package]]
 name = "pathspec"
@@ -1372,7 +1402,7 @@ scramp = "1.2.0"
 
 [[package]]
 name = "phonenumbers"
-version = "8.12.13"
+version = "8.12.15"
 description = "Python version of Google's common library for parsing, formatting, storing and validating international phone numbers."
 category = "main"
 optional = false
@@ -1424,7 +1454,7 @@ wcwidth = "*"
 
 [[package]]
 name = "psutil"
-version = "5.7.3"
+version = "5.8.0"
 description = "Cross-platform lib for process and system monitoring in Python."
 category = "main"
 optional = false
@@ -1443,7 +1473,7 @@ python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*"
 
 [[package]]
 name = "py"
-version = "1.9.0"
+version = "1.10.0"
 description = "library with cross-python path, ini-parsing, io, code, log facilities"
 category = "dev"
 optional = false
@@ -1505,7 +1535,7 @@ python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
 
 [[package]]
 name = "pygments"
-version = "2.7.2"
+version = "2.7.3"
 description = "Pygments is a syntax highlighting package written in Python."
 category = "dev"
 optional = false
@@ -1534,25 +1564,24 @@ python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*"
 
 [[package]]
 name = "pytest"
-version = "6.1.2"
+version = "6.2.1"
 description = "pytest: simple powerful testing with Python"
 category = "dev"
 optional = false
-python-versions = ">=3.5"
+python-versions = ">=3.6"
 
 [package.dependencies]
 atomicwrites = {version = ">=1.0", markers = "sys_platform == \"win32\""}
-attrs = ">=17.4.0"
+attrs = ">=19.2.0"
 colorama = {version = "*", markers = "sys_platform == \"win32\""}
 importlib-metadata = {version = ">=0.12", markers = "python_version < \"3.8\""}
 iniconfig = "*"
 packaging = "*"
-pluggy = ">=0.12,<1.0"
+pluggy = ">=0.12,<1.0.0a1"
 py = ">=1.8.2"
 toml = "*"
 
 [package.extras]
-checkqa_mypy = ["mypy (==0.780)"]
 testing = ["argcomplete", "hypothesis (>=3.56)", "mock", "nose", "requests", "xmlschema"]
 
 [[package]]
@@ -1714,7 +1743,7 @@ python-versions = "*"
 
 [[package]]
 name = "requests"
-version = "2.25.0"
+version = "2.25.1"
 description = "Python HTTP for Humans."
 category = "main"
 optional = false
@@ -1722,7 +1751,7 @@ python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
 
 [package.dependencies]
 certifi = ">=2017.4.17"
-chardet = ">=3.0.2,<4"
+chardet = ">=3.0.2,<5"
 idna = ">=2.5,<3"
 urllib3 = ">=1.21.1,<1.27"
 
@@ -1831,7 +1860,7 @@ python-versions = "*"
 
 [[package]]
 name = "spdx-license-list"
-version = "0.5.1"
+version = "0.5.2"
 description = "A simple tool/library for working with SPDX license definitions."
 category = "main"
 optional = false
@@ -1972,7 +2001,7 @@ python-versions = ">=3.5"
 
 [[package]]
 name = "stevedore"
-version = "3.2.2"
+version = "3.3.0"
 description = "Manage dynamic plugins for Python applications"
 category = "dev"
 optional = false
@@ -1992,7 +2021,7 @@ python-versions = "*"
 
 [[package]]
 name = "testfixtures"
-version = "6.15.0"
+version = "6.17.0"
 description = "A collection of helpers and mock objects for unit tests and doc tests."
 category = "dev"
 optional = false
@@ -2047,7 +2076,7 @@ python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*"
 
 [[package]]
 name = "tqdm"
-version = "4.54.0"
+version = "4.54.1"
 description = "Fast, Extensible Progress Meter"
 category = "main"
 optional = false
@@ -2058,7 +2087,7 @@ dev = ["py-make (>=0.1.0)", "twine", "argopt", "pydoc-markdown", "wheel"]
 
 [[package]]
 name = "twilio"
-version = "6.48.0"
+version = "6.50.1"
 description = "Twilio API client and TwiML generator"
 category = "main"
 optional = false
@@ -2082,7 +2111,7 @@ python-versions = "*"
 name = "typing-extensions"
 version = "3.7.4.3"
 description = "Backported and Experimental Type Hints for Python 3.5+"
-category = "dev"
+category = "main"
 optional = false
 python-versions = "*"
 
@@ -2153,7 +2182,7 @@ ldap = ["django-auth-ldap"]
 [metadata]
 lock-version = "1.1"
 python-versions = "^3.7"
-content-hash = "796fb4bd6779730135a6d2c08f09646dd3f91cdb59dfe2210bdfabd5050f0d2f"
+content-hash = "65a4b7b891965a330e7fec9ed64213579550aac1ac8295aa8c3022a4978d488c"
 
 [metadata.files]
 alabaster = [
@@ -2188,8 +2217,8 @@ babel = [
     {file = "Babel-2.9.0.tar.gz", hash = "sha256:da031ab54472314f210b0adcff1588ee5d1d1d0ba4dbd07b94dba82bde791e05"},
 ]
 bandit = [
-    {file = "bandit-1.6.2-py2.py3-none-any.whl", hash = "sha256:336620e220cf2d3115877685e264477ff9d9abaeb0afe3dc7264f55fa17a3952"},
-    {file = "bandit-1.6.2.tar.gz", hash = "sha256:41e75315853507aa145d62a78a2a6c5e3240fe14ee7c601459d0df9418196065"},
+    {file = "bandit-1.7.0-py3-none-any.whl", hash = "sha256:216be4d044209fa06cf2a3e51b319769a51be8318140659719aa7a115c35ed07"},
+    {file = "bandit-1.7.0.tar.gz", hash = "sha256:8a4c7415254d75df8ff3c3b15cfe9042ecee628a1e40b44c15a98890fbfc2608"},
 ]
 billiard = [
     {file = "billiard-3.6.3.0-py3-none-any.whl", hash = "sha256:bff575450859a6e0fbc2f9877d9b715b0bbc07c3565bb7ed2280526a0cdf5ede"},
@@ -2212,8 +2241,8 @@ calendarweek = [
     {file = "calendarweek-0.4.7.tar.gz", hash = "sha256:7655d6a4c3b4f6a4e01aa7d23b49cd121db0399050e9c08cd8d1210155be25dd"},
 ]
 celery = [
-    {file = "celery-5.0.2-py3-none-any.whl", hash = "sha256:930c3acd55349d028c4e7104a7d377729cbcca19d9fce470c17172d9e7f9a8b6"},
-    {file = "celery-5.0.2.tar.gz", hash = "sha256:012c814967fe89e3f5d2cf49df2dba3de5f29253a7f4f2270e8fce6b901b4ebf"},
+    {file = "celery-5.0.5-py3-none-any.whl", hash = "sha256:5e8d364e058554e83bbb116e8377d90c79be254785f357cb2cec026e79febe13"},
+    {file = "celery-5.0.5.tar.gz", hash = "sha256:f4efebe6f8629b0da2b8e529424de376494f5b7a743c321c8a2ddc2b1414921c"},
 ]
 celery-haystack = [
     {file = "celery-haystack-0.10.tar.gz", hash = "sha256:b6e2a3c70071bef0838ca1a7d9f14fae6c2ecf385704092e59b82147a1ee552e"},
@@ -2224,12 +2253,12 @@ celery-progress = [
     {file = "celery_progress-0.0.14-py3-none-any.whl", hash = "sha256:6d95c01fe044dd5dbb1e2d507724f9ace70bde796bc6db51ba19c8a95e94da07"},
 ]
 certifi = [
-    {file = "certifi-2020.11.8-py2.py3-none-any.whl", hash = "sha256:1f422849db327d534e3d0c5f02a263458c3955ec0aae4ff09b95f195c59f4edd"},
-    {file = "certifi-2020.11.8.tar.gz", hash = "sha256:f05def092c44fbf25834a51509ef6e631dc19765ab8a57b4e7ab85531f0a9cf4"},
+    {file = "certifi-2020.12.5-py2.py3-none-any.whl", hash = "sha256:719a74fb9e33b9bd44cc7f3a8d94bc35e4049deebe19ba7d8e108280cfd59830"},
+    {file = "certifi-2020.12.5.tar.gz", hash = "sha256:1a4995114262bffbc2413b159f2a1a480c969de6e6eb13ee966d470af86af59c"},
 ]
 chardet = [
-    {file = "chardet-3.0.4-py2.py3-none-any.whl", hash = "sha256:fc323ffcaeaed0e0a02bf4d117757b98aed530d9ed4531e3e15460124c106691"},
-    {file = "chardet-3.0.4.tar.gz", hash = "sha256:84ab92ed1c4d4f16916e05906b6b75a6c0fb5db821cc65e70cbd64a3e2a5eaae"},
+    {file = "chardet-4.0.0-py2.py3-none-any.whl", hash = "sha256:f864054d66fd9118f2e67044ac8981a54775ec5b67aed0441892edb553d21da5"},
+    {file = "chardet-4.0.0.tar.gz", hash = "sha256:0d6f53a15db4120f2b08c94f11e7d93d2c911ee118b6b30a04ec3ee8310179fa"},
 ]
 click = [
     {file = "click-7.1.2-py2.py3-none-any.whl", hash = "sha256:dacca89f4bfadd5de3d7489b7c8a566eee0d3676333fbb50030263894c38c0dc"},
@@ -2238,13 +2267,16 @@ click = [
 click-didyoumean = [
     {file = "click-didyoumean-0.0.3.tar.gz", hash = "sha256:112229485c9704ff51362fe34b2d4f0b12fc71cc20f6d2b3afabed4b8bfa6aeb"},
 ]
+click-plugins = [
+    {file = "click-plugins-1.1.1.tar.gz", hash = "sha256:46ab999744a9d831159c3411bb0c79346d94a444df9a3a3742e9ed63645f264b"},
+    {file = "click_plugins-1.1.1-py2.py3-none-any.whl", hash = "sha256:5d262006d3222f5057fd81e1623d4443e41dcda5dc815c06b442aa3c02889fc8"},
+]
 click-repl = [
     {file = "click-repl-0.1.6.tar.gz", hash = "sha256:b9f29d52abc4d6059f8e276132a111ab8d94980afe6a5432b9d996544afa95d5"},
     {file = "click_repl-0.1.6-py3-none-any.whl", hash = "sha256:9c4c3d022789cae912aad8a3f5e1d7c2cdd016ee1225b5212ad3e8691563cda5"},
 ]
 colorama = [
     {file = "colorama-0.4.4-py2.py3-none-any.whl", hash = "sha256:9f47eda37229f68eee03b24b9748937c7dc3868f906e8ba69fbcbdd3bc5dc3e2"},
-    {file = "colorama-0.4.4.tar.gz", hash = "sha256:5941b2b48a20143d2267e95b1c2a7603ce057ee39fd88e7329b0c292aa16869b"},
 ]
 colour = [
     {file = "colour-0.1.5-py2.py3-none-any.whl", hash = "sha256:33f6db9d564fadc16e59921a56999b79571160ce09916303d35346dddc17978c"},
@@ -2294,8 +2326,8 @@ dj-database-url = [
     {file = "dj_database_url-0.5.0-py2.py3-none-any.whl", hash = "sha256:851785365761ebe4994a921b433062309eb882fedd318e1b0fcecc607ed02da9"},
 ]
 django = [
-    {file = "Django-3.1.3-py3-none-any.whl", hash = "sha256:14a4b7cd77297fba516fc0d92444cc2e2e388aa9de32d7a68d4a83d58f5a4927"},
-    {file = "Django-3.1.3.tar.gz", hash = "sha256:14b87775ffedab2ef6299b73343d1b4b41e5d4e2aa58c6581f114dbec01e3f8f"},
+    {file = "Django-3.1.4-py3-none-any.whl", hash = "sha256:5c866205f15e7a7123f1eec6ab939d22d5bde1416635cab259684af66d8e48a2"},
+    {file = "Django-3.1.4.tar.gz", hash = "sha256:edb10b5c45e7e9c0fb1dc00b76ec7449aca258a39ffd613dbd078c51d19c9f03"},
 ]
 django-any-js = [
     {file = "django-any-js-1.0.3.post0.tar.gz", hash = "sha256:1da88b44b861b0f54f6b8ea0eb4c7c4fa1a5772e9a4320532cd4e0871a4e23f7"},
@@ -2316,6 +2348,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-cachalot = [
+    {file = "django-cachalot-2.3.3.tar.gz", hash = "sha256:ba3a6cabf834139196179c4f6d77409ae9170267ee8ce40e27bbf6c3f6733b2b"},
+    {file = "django_cachalot-2.3.3-py3-none-any.whl", hash = "sha256:55f94e94f7000f5f6bd92188d3d7535cfdef79f2e697e36daf69cba8f435e156"},
+]
 django-cache-memoize = [
     {file = "django-cache-memoize-0.1.7.tar.gz", hash = "sha256:5e96349b0159aec1eb79257199a1902ea3ed538231ce7b4fee12e563127ca657"},
     {file = "django_cache_memoize-0.1.7-py2.py3-none-any.whl", hash = "sha256:bc7f53725558244af62197d0125732d7ec88ecc1281a3a2f37d77ae1a8c269d3"},
@@ -2411,8 +2447,8 @@ django-middleware-global-request = [
     {file = "django-middleware-global-request-0.1.2.tar.gz", hash = "sha256:f6490759bc9f7dbde4001709554e29ca715daf847f2222914b4e47117dca9313"},
 ]
 django-model-utils = [
-    {file = "django-model-utils-4.0.0.tar.gz", hash = "sha256:adf09e5be15122a7f4e372cb5a6dd512bbf8d78a23a90770ad0983ee9d909061"},
-    {file = "django_model_utils-4.0.0-py2.py3-none-any.whl", hash = "sha256:9cf882e5b604421b62dbe57ad2b18464dc9c8f963fc3f9831badccae66c1139c"},
+    {file = "django-model-utils-4.1.1.tar.gz", hash = "sha256:eb5dd05ef7d7ce6bc79cae54ea7c4a221f6f81e2aad7722933aee66489e7264b"},
+    {file = "django_model_utils-4.1.1-py3-none-any.whl", hash = "sha256:ef7c440024e797796a3811432abdd2be8b5225ae64ef346f8bfc6de7d8e5d73c"},
 ]
 django-otp = [
     {file = "django-otp-1.0.2.tar.gz", hash = "sha256:f523fb9dec420f28a29d3e2ad72ac06f64588956ed4f2b5b430d8e957ebb8287"},
@@ -2450,8 +2486,8 @@ django-sass-processor = [
     {file = "django-sass-processor-0.8.2.tar.gz", hash = "sha256:9b46a12ca8bdcb397d46fbcc49e6a926ff9f76a93c5efeb23b495419fd01fc7a"},
 ]
 django-select2 = [
-    {file = "django-select2-7.4.2.tar.gz", hash = "sha256:9d3330fa0083a03fb69fceb5dcd2e78065cfd08e45c89d4fd727fce4673d3e08"},
-    {file = "django_select2-7.4.2-py2.py3-none-any.whl", hash = "sha256:06531d563ce33c3133682ae2bb9e6d762103a863d0054ffef51bae8b4cfcca6c"},
+    {file = "django-select2-7.5.0.tar.gz", hash = "sha256:df71dedba9a362041b65e3cd692cb8b4f9e1e17a19681c7b4e61f331868bae0c"},
+    {file = "django_select2-7.5.0-py2.py3-none-any.whl", hash = "sha256:6662aa1c21d4839b8fff38e4c9d402ed3da81f7c5ef7f7e703c862255ba3b9ed"},
 ]
 django-settings-context-processor = [
     {file = "django-settings-context-processor-0.2.tar.gz", hash = "sha256:d37c853d69a3069f5abbf94c7f4f6fc0fac38bbd0524190cd5a250ba800e496a"},
@@ -2468,8 +2504,8 @@ django-templated-email = [
     {file = "django-templated-email-2.3.0.tar.gz", hash = "sha256:536c4e5ae099eabfb9aab36087d4d7799948c654e73da55a744213d086d5bb33"},
 ]
 django-timezone-field = [
-    {file = "django-timezone-field-4.0.tar.gz", hash = "sha256:7e3620fe2211c2d372fad54db8f86ff884098d018d56fda4dca5e64929e05ffc"},
-    {file = "django_timezone_field-4.0-py3-none-any.whl", hash = "sha256:758b7d41084e9ea2e89e59eb616e9b6326e6fbbf9d14b6ef062d624fe8cc6246"},
+    {file = "django-timezone-field-4.1.1.tar.gz", hash = "sha256:b5b587aabed8db66eb3453691522164915c1aa1b326d8ddeadc8832a8580faeb"},
+    {file = "django_timezone_field-4.1.1-py3-none-any.whl", hash = "sha256:068dc2c9b11c2230e126f511a515609d46f8cc49278b293e7536be07997fe892"},
 ]
 django-two-factor-auth = [
     {file = "django-two-factor-auth-1.13.tar.gz", hash = "sha256:24c2850a687c86800f4aa4131b7cebadf56f35be04ca359c4990578df1cc249a"},
@@ -2495,8 +2531,8 @@ dynaconf = [
     {file = "dynaconf-3.1.2.tar.gz", hash = "sha256:9b34ab2f811a81755f5eb4beac77a69e1e0887528c7e37fc4bc83fed52dcf502"},
 ]
 faker = [
-    {file = "Faker-4.17.1-py3-none-any.whl", hash = "sha256:5398268e1d751ffdb3ed36b8a790ed98659200599b368eec38a02eed15bce997"},
-    {file = "Faker-4.17.1.tar.gz", hash = "sha256:d4183b8f57316de3be27cd6c3b40e9f9343d27c95c96179f027316c58c2c239e"},
+    {file = "Faker-5.0.2-py3-none-any.whl", hash = "sha256:5b17c95cfb013a22b062b8df18286f08ce4ea880f9948ec74295e5a42dbb2e44"},
+    {file = "Faker-5.0.2.tar.gz", hash = "sha256:00ce4342c221b1931b2f35d46f5027d35bc62a4ca3a34628b2c5b514b4ca958a"},
 ]
 flake8 = [
     {file = "flake8-3.8.4-py2.py3-none-any.whl", hash = "sha256:749dbbd6bfd0cf1318af27bf97a14e28e5ff548ef8e5b1566ccfb25a11e7c839"},
@@ -2560,11 +2596,10 @@ imagesize = [
     {file = "imagesize-1.2.0.tar.gz", hash = "sha256:b1f6b5a4eab1f73479a50fb79fcf729514a900c341d8503d62a62dbc4127a2b1"},
 ]
 importlib-metadata = [
-    {file = "importlib_metadata-3.1.0-py2.py3-none-any.whl", hash = "sha256:590690d61efdd716ff82c39ca9a9d4209252adfe288a4b5721181050acbd4175"},
-    {file = "importlib_metadata-3.1.0.tar.gz", hash = "sha256:d9b8a46a0885337627a6430db287176970fff18ad421becec1d64cfc763c2099"},
+    {file = "importlib_metadata-3.3.0-py3-none-any.whl", hash = "sha256:bf792d480abbd5eda85794e4afb09dd538393f7d6e6ffef6e9f03d2014cf9450"},
+    {file = "importlib_metadata-3.3.0.tar.gz", hash = "sha256:5c5a2720817414a6c41f0a49993908068243ae02c1635a228126519b509c8aed"},
 ]
 iniconfig = [
-    {file = "iniconfig-1.1.1-py2.py3-none-any.whl", hash = "sha256:011e24c64b7f47f6ebd835bb12a743f2fbe9a26d4cecaa7f53bc4f35ee9da8b3"},
     {file = "iniconfig-1.1.1.tar.gz", hash = "sha256:bc3af051d7d14b2ee5ef9969666def0cd1a000e121eaea580d4a313df4b37f32"},
 ]
 isort = [
@@ -2658,8 +2693,8 @@ mypy-extensions = [
     {file = "mypy_extensions-0.4.3.tar.gz", hash = "sha256:2d82818f5bb3e369420cb3c4060a7970edba416647068eb4c5343488a6c604a8"},
 ]
 packaging = [
-    {file = "packaging-20.4-py2.py3-none-any.whl", hash = "sha256:998416ba6962ae7fbd6596850b80e17859a5753ba17c32284f67bfff33784181"},
-    {file = "packaging-20.4.tar.gz", hash = "sha256:4357f74f47b9c12db93624a82154e9b120fa8293699949152b22065d556079f8"},
+    {file = "packaging-20.8-py2.py3-none-any.whl", hash = "sha256:24e0da08660a87484d1602c30bb4902d74816b6985b93de36926f5bc95741858"},
+    {file = "packaging-20.8.tar.gz", hash = "sha256:78598185a7008a470d64526a8059de9aaa449238f280fc9eb6b13ba6c4109093"},
 ]
 pathspec = [
     {file = "pathspec-0.8.1-py2.py3-none-any.whl", hash = "sha256:aa0cb481c4041bf52ffa7b0d8fa6cd3e88a2ca4879c533c9153882ee2556790d"},
@@ -2677,8 +2712,8 @@ pg8000 = [
     {file = "pg8000-1.16.6.tar.gz", hash = "sha256:8fc1e6a62ccb7c9830f1e7e9288e2d20eaf373cc8875b5c55b7d5d9b7717be91"},
 ]
 phonenumbers = [
-    {file = "phonenumbers-8.12.13-py2.py3-none-any.whl", hash = "sha256:9de2937034deb040eb9ac56519b0887e0fe89811e57f6f5c88359e3be20ae3b5"},
-    {file = "phonenumbers-8.12.13.tar.gz", hash = "sha256:96d02120a3481e22d8a8eb5e4595ceec1930855749f6e4a06ef931881f59f562"},
+    {file = "phonenumbers-8.12.15-py2.py3-none-any.whl", hash = "sha256:13d499f7114c4b37c54ee844b188d5e7441951a7da41de5fc1a25ff8fdceef80"},
+    {file = "phonenumbers-8.12.15.tar.gz", hash = "sha256:b734bfcf33e87ddae72196a40b3d1af35abd0beb263816ae18e1bff612926406"},
 ]
 pillow = [
     {file = "Pillow-8.0.1-cp36-cp36m-macosx_10_10_x86_64.whl", hash = "sha256:b63d4ff734263ae4ce6593798bcfee6dbfb00523c82753a3a03cbc05555a9cc3"},
@@ -2723,17 +2758,34 @@ prompt-toolkit = [
     {file = "prompt_toolkit-3.0.8.tar.gz", hash = "sha256:25c95d2ac813909f813c93fde734b6e44406d1477a9faef7c915ff37d39c0a8c"},
 ]
 psutil = [
-    {file = "psutil-5.7.3-cp27-none-win32.whl", hash = "sha256:1cd6a0c9fb35ece2ccf2d1dd733c1e165b342604c67454fd56a4c12e0a106787"},
-    {file = "psutil-5.7.3-cp27-none-win_amd64.whl", hash = "sha256:e02c31b2990dcd2431f4524b93491941df39f99619b0d312dfe1d4d530b08b4b"},
-    {file = "psutil-5.7.3-cp35-cp35m-win32.whl", hash = "sha256:56c85120fa173a5d2ad1d15a0c6e0ae62b388bfb956bb036ac231fbdaf9e4c22"},
-    {file = "psutil-5.7.3-cp35-cp35m-win_amd64.whl", hash = "sha256:fa38ac15dbf161ab1e941ff4ce39abd64b53fec5ddf60c23290daed2bc7d1157"},
-    {file = "psutil-5.7.3-cp36-cp36m-win32.whl", hash = "sha256:01bc82813fbc3ea304914581954979e637bcc7084e59ac904d870d6eb8bb2bc7"},
-    {file = "psutil-5.7.3-cp36-cp36m-win_amd64.whl", hash = "sha256:6a3e1fd2800ca45083d976b5478a2402dd62afdfb719b30ca46cd28bb25a2eb4"},
-    {file = "psutil-5.7.3-cp37-cp37m-win32.whl", hash = "sha256:fbcac492cb082fa38d88587d75feb90785d05d7e12d4565cbf1ecc727aff71b7"},
-    {file = "psutil-5.7.3-cp37-cp37m-win_amd64.whl", hash = "sha256:5d9106ff5ec2712e2f659ebbd112967f44e7d33f40ba40530c485cc5904360b8"},
-    {file = "psutil-5.7.3-cp38-cp38-win32.whl", hash = "sha256:ade6af32eb80a536eff162d799e31b7ef92ddcda707c27bbd077238065018df4"},
-    {file = "psutil-5.7.3-cp38-cp38-win_amd64.whl", hash = "sha256:2cb55ef9591b03ef0104bedf67cc4edb38a3edf015cf8cf24007b99cb8497542"},
-    {file = "psutil-5.7.3.tar.gz", hash = "sha256:af73f7bcebdc538eda9cc81d19db1db7bf26f103f91081d780bbacfcb620dee2"},
+    {file = "psutil-5.8.0-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:0066a82f7b1b37d334e68697faba68e5ad5e858279fd6351c8ca6024e8d6ba64"},
+    {file = "psutil-5.8.0-cp27-cp27m-manylinux2010_i686.whl", hash = "sha256:0ae6f386d8d297177fd288be6e8d1afc05966878704dad9847719650e44fc49c"},
+    {file = "psutil-5.8.0-cp27-cp27m-manylinux2010_x86_64.whl", hash = "sha256:12d844996d6c2b1d3881cfa6fa201fd635971869a9da945cf6756105af73d2df"},
+    {file = "psutil-5.8.0-cp27-cp27mu-manylinux2010_i686.whl", hash = "sha256:02b8292609b1f7fcb34173b25e48d0da8667bc85f81d7476584d889c6e0f2131"},
+    {file = "psutil-5.8.0-cp27-cp27mu-manylinux2010_x86_64.whl", hash = "sha256:6ffe81843131ee0ffa02c317186ed1e759a145267d54fdef1bc4ea5f5931ab60"},
+    {file = "psutil-5.8.0-cp27-none-win32.whl", hash = "sha256:ea313bb02e5e25224e518e4352af4bf5e062755160f77e4b1767dd5ccb65f876"},
+    {file = "psutil-5.8.0-cp27-none-win_amd64.whl", hash = "sha256:5da29e394bdedd9144c7331192e20c1f79283fb03b06e6abd3a8ae45ffecee65"},
+    {file = "psutil-5.8.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:74fb2557d1430fff18ff0d72613c5ca30c45cdbfcddd6a5773e9fc1fe9364be8"},
+    {file = "psutil-5.8.0-cp36-cp36m-manylinux2010_i686.whl", hash = "sha256:74f2d0be88db96ada78756cb3a3e1b107ce8ab79f65aa885f76d7664e56928f6"},
+    {file = "psutil-5.8.0-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:99de3e8739258b3c3e8669cb9757c9a861b2a25ad0955f8e53ac662d66de61ac"},
+    {file = "psutil-5.8.0-cp36-cp36m-win32.whl", hash = "sha256:36b3b6c9e2a34b7d7fbae330a85bf72c30b1c827a4366a07443fc4b6270449e2"},
+    {file = "psutil-5.8.0-cp36-cp36m-win_amd64.whl", hash = "sha256:52de075468cd394ac98c66f9ca33b2f54ae1d9bff1ef6b67a212ee8f639ec06d"},
+    {file = "psutil-5.8.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:c6a5fd10ce6b6344e616cf01cc5b849fa8103fbb5ba507b6b2dee4c11e84c935"},
+    {file = "psutil-5.8.0-cp37-cp37m-manylinux2010_i686.whl", hash = "sha256:61f05864b42fedc0771d6d8e49c35f07efd209ade09a5afe6a5059e7bb7bf83d"},
+    {file = "psutil-5.8.0-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:0dd4465a039d343925cdc29023bb6960ccf4e74a65ad53e768403746a9207023"},
+    {file = "psutil-5.8.0-cp37-cp37m-win32.whl", hash = "sha256:1bff0d07e76114ec24ee32e7f7f8d0c4b0514b3fae93e3d2aaafd65d22502394"},
+    {file = "psutil-5.8.0-cp37-cp37m-win_amd64.whl", hash = "sha256:fcc01e900c1d7bee2a37e5d6e4f9194760a93597c97fee89c4ae51701de03563"},
+    {file = "psutil-5.8.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:6223d07a1ae93f86451d0198a0c361032c4c93ebd4bf6d25e2fb3edfad9571ef"},
+    {file = "psutil-5.8.0-cp38-cp38-manylinux2010_i686.whl", hash = "sha256:d225cd8319aa1d3c85bf195c4e07d17d3cd68636b8fc97e6cf198f782f99af28"},
+    {file = "psutil-5.8.0-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:28ff7c95293ae74bf1ca1a79e8805fcde005c18a122ca983abf676ea3466362b"},
+    {file = "psutil-5.8.0-cp38-cp38-win32.whl", hash = "sha256:ce8b867423291cb65cfc6d9c4955ee9bfc1e21fe03bb50e177f2b957f1c2469d"},
+    {file = "psutil-5.8.0-cp38-cp38-win_amd64.whl", hash = "sha256:90f31c34d25b1b3ed6c40cdd34ff122b1887a825297c017e4cbd6796dd8b672d"},
+    {file = "psutil-5.8.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:6323d5d845c2785efb20aded4726636546b26d3b577aded22492908f7c1bdda7"},
+    {file = "psutil-5.8.0-cp39-cp39-manylinux2010_i686.whl", hash = "sha256:245b5509968ac0bd179287d91210cd3f37add77dad385ef238b275bad35fa1c4"},
+    {file = "psutil-5.8.0-cp39-cp39-manylinux2010_x86_64.whl", hash = "sha256:90d4091c2d30ddd0a03e0b97e6a33a48628469b99585e2ad6bf21f17423b112b"},
+    {file = "psutil-5.8.0-cp39-cp39-win32.whl", hash = "sha256:ea372bcc129394485824ae3e3ddabe67dc0b118d262c568b4d2602a7070afdb0"},
+    {file = "psutil-5.8.0-cp39-cp39-win_amd64.whl", hash = "sha256:f4634b033faf0d968bb9220dd1c793b897ab7f1189956e1aa9eae752527127d3"},
+    {file = "psutil-5.8.0.tar.gz", hash = "sha256:0c9ccb99ab76025f2f0bbecf341d4656e9c1351db8cc8a03ccd62e318ab4b5c6"},
 ]
 psycopg2 = [
     {file = "psycopg2-2.8.6-cp27-cp27m-win32.whl", hash = "sha256:068115e13c70dc5982dfc00c5d70437fe37c014c808acce119b5448361c03725"},
@@ -2751,8 +2803,8 @@ psycopg2 = [
     {file = "psycopg2-2.8.6.tar.gz", hash = "sha256:fb23f6c71107c37fd667cb4ea363ddeb936b348bbd6449278eb92c189699f543"},
 ]
 py = [
-    {file = "py-1.9.0-py2.py3-none-any.whl", hash = "sha256:366389d1db726cd2fcfc79732e75410e5fe4d31db13692115529d34069a043c2"},
-    {file = "py-1.9.0.tar.gz", hash = "sha256:9ca6883ce56b4e8da7e79ac18787889fa5206c79dcc67fb065376cd2fe03f342"},
+    {file = "py-1.10.0-py2.py3-none-any.whl", hash = "sha256:3b80836aa6d1feeaa108e046da6423ab8f6ceda6468545ae8d02d9d58d18818a"},
+    {file = "py-1.10.0.tar.gz", hash = "sha256:21b81bda15b66ef5e1a777a21c4dcd9c20ad3efd0b3f817e7a809035269e1bd3"},
 ]
 pyasn1 = [
     {file = "pyasn1-0.4.8-py2.4.egg", hash = "sha256:fec3e9d8e36808a28efb59b489e4528c10ad0f480e57dcc32b4de5c9d8c9fdf3"},
@@ -2792,8 +2844,6 @@ pycryptodome = [
     {file = "pycryptodome-3.9.9-cp27-cp27m-macosx_10_6_intel.whl", hash = "sha256:5598dc6c9dbfe882904e54584322893eff185b98960bbe2cdaaa20e8a437b6e5"},
     {file = "pycryptodome-3.9.9-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:1cfdb92dca388e27e732caa72a1cc624520fe93752a665c3b6cd8f1a91b34916"},
     {file = "pycryptodome-3.9.9-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:5f19e6ef750f677d924d9c7141f54bade3cd56695bbfd8a9ef15d0378557dfe4"},
-    {file = "pycryptodome-3.9.9-cp27-cp27m-win32.whl", hash = "sha256:a3d8a9efa213be8232c59cdc6b65600276508e375e0a119d710826248fd18d37"},
-    {file = "pycryptodome-3.9.9-cp27-cp27m-win_amd64.whl", hash = "sha256:50826b49fbca348a61529693b0031cdb782c39060fb9dca5ac5dff858159dc5a"},
     {file = "pycryptodome-3.9.9-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:19cb674df6c74a14b8b408aa30ba8a89bd1c01e23505100fb45f930fbf0ed0d9"},
     {file = "pycryptodome-3.9.9-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:28f75e58d02019a7edc7d4135203d2501dfc47256d175c72c9798f9a129a49a7"},
     {file = "pycryptodome-3.9.9-cp35-cp35m-macosx_10_6_intel.whl", hash = "sha256:6d3baaf82681cfb1a842f1c8f77beac791ceedd99af911e4f5fabec32bae2259"},
@@ -2804,26 +2854,17 @@ pycryptodome = [
     {file = "pycryptodome-3.9.9-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:7798e73225a699651888489fbb1dbc565e03a509942a8ce6194bbe6fb582a41f"},
     {file = "pycryptodome-3.9.9-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:46e96aeb8a9ca8b1edf9b1fd0af4bf6afcf3f1ca7fa35529f5d60b98f3e4e959"},
     {file = "pycryptodome-3.9.9-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:843e5f10ecdf9d307032b8b91afe9da1d6ed5bb89d0bbec5c8dcb4ba44008e11"},
-    {file = "pycryptodome-3.9.9-cp36-cp36m-win32.whl", hash = "sha256:b68794fba45bdb367eeb71249c26d23e61167510a1d0c3d6cf0f2f14636e62ee"},
-    {file = "pycryptodome-3.9.9-cp36-cp36m-win_amd64.whl", hash = "sha256:60febcf5baf70c566d9d9351c47fbd8321da9a4edf2eff45c4c31c86164ca794"},
     {file = "pycryptodome-3.9.9-cp37-cp37m-macosx_10_6_intel.whl", hash = "sha256:4ed27951b0a17afd287299e2206a339b5b6d12de9321e1a1575261ef9c4a851b"},
     {file = "pycryptodome-3.9.9-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:9000877383e2189dafd1b2fc68c6c726eca9a3cfb6d68148fbb72ccf651959b6"},
     {file = "pycryptodome-3.9.9-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:faa682c404c218e8788c3126c9a4b8fbcc54dc245b5b6e8ea5b46f3b63bd0c84"},
     {file = "pycryptodome-3.9.9-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:62c488a21c253dadc9f731a32f0ac61e4e436d81a1ea6f7d1d9146ed4d20d6bd"},
-    {file = "pycryptodome-3.9.9-cp37-cp37m-win32.whl", hash = "sha256:834b790bbb6bd18956f625af4004d9c15eed12d5186d8e57851454ae76d52215"},
-    {file = "pycryptodome-3.9.9-cp37-cp37m-win_amd64.whl", hash = "sha256:70d807d11d508433daf96244ec1c64e55039e8a35931fc5ea9eee94dbe3cb6b5"},
     {file = "pycryptodome-3.9.9-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:27397aee992af69d07502126561d851ba3845aa808f0e55c71ad0efa264dd7d4"},
     {file = "pycryptodome-3.9.9-cp38-cp38-manylinux1_i686.whl", hash = "sha256:d7ec2bd8f57c559dd24e71891c51c25266a8deb66fc5f02cc97c7fb593d1780a"},
     {file = "pycryptodome-3.9.9-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:e15bde67ccb7d4417f627dd16ffe2f5a4c2941ce5278444e884cb26d73ecbc61"},
     {file = "pycryptodome-3.9.9-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:5c3c4865730dfb0263f822b966d6d58429d8b1e560d1ddae37685fd9e7c63161"},
-    {file = "pycryptodome-3.9.9-cp38-cp38-win32.whl", hash = "sha256:76b1a34d74bb2c91bce460cdc74d1347592045627a955e9a252554481c17c52f"},
-    {file = "pycryptodome-3.9.9-cp38-cp38-win_amd64.whl", hash = "sha256:6e4227849e4231a3f5b35ea5bdedf9a82b3883500e5624f00a19156e9a9ef861"},
     {file = "pycryptodome-3.9.9-cp39-cp39-manylinux1_i686.whl", hash = "sha256:2a68df525b387201a43b27b879ce8c08948a430e883a756d6c9e3acdaa7d7bd8"},
     {file = "pycryptodome-3.9.9-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:a4599c0ca0fc027c780c1c45ed996d5bef03e571470b7b1c7171ec1e1a90914c"},
     {file = "pycryptodome-3.9.9-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:b4e6b269a8ddaede774e5c3adbef6bf452ee144e6db8a716d23694953348cd86"},
-    {file = "pycryptodome-3.9.9-cp39-cp39-win32.whl", hash = "sha256:a199e9ca46fc6e999e5f47fce342af4b56c7de85fae893c69ab6aa17531fb1e1"},
-    {file = "pycryptodome-3.9.9-cp39-cp39-win_amd64.whl", hash = "sha256:6e89bb3826e6f84501e8e3b205c22595d0c5492c2f271cbb9ee1c48eb1866645"},
-    {file = "pycryptodome-3.9.9.tar.gz", hash = "sha256:910e202a557e1131b1c1b3f17a63914d57aac55cf9fb9b51644962841c3995c4"},
 ]
 pydocstyle = [
     {file = "pydocstyle-5.1.1-py3-none-any.whl", hash = "sha256:aca749e190a01726a4fb472dd4ef23b5c9da7b9205c0a7857c06533de13fd678"},
@@ -2834,8 +2875,8 @@ pyflakes = [
     {file = "pyflakes-2.2.0.tar.gz", hash = "sha256:35b2d75ee967ea93b55750aa9edbbf72813e06a66ba54438df2cfac9e3c27fc8"},
 ]
 pygments = [
-    {file = "Pygments-2.7.2-py3-none-any.whl", hash = "sha256:88a0bbcd659fcb9573703957c6b9cff9fab7295e6e76db54c9d00ae42df32773"},
-    {file = "Pygments-2.7.2.tar.gz", hash = "sha256:381985fcc551eb9d37c52088a32914e00517e57f4a21609f48141ba08e193fa0"},
+    {file = "Pygments-2.7.3-py3-none-any.whl", hash = "sha256:f275b6c0909e5dafd2d6269a656aa90fa58ebf4a74f8fcf9053195d226b24a08"},
+    {file = "Pygments-2.7.3.tar.gz", hash = "sha256:ccf3acacf3782cbed4a989426012f1c535c9a90d3a7fc3f16d231b9372d2b716"},
 ]
 pyjwt = [
     {file = "PyJWT-1.7.1-py2.py3-none-any.whl", hash = "sha256:5c6eca3c2940464d106b99ba83b00c6add741c9becaec087fb7ccdefea71350e"},
@@ -2846,8 +2887,8 @@ pyparsing = [
     {file = "pyparsing-2.4.7.tar.gz", hash = "sha256:c203ec8783bf771a155b207279b9bccb8dea02d8f0c9e5f8ead507bc3246ecc1"},
 ]
 pytest = [
-    {file = "pytest-6.1.2-py3-none-any.whl", hash = "sha256:4288fed0d9153d9646bfcdf0c0428197dba1ecb27a33bb6e031d002fa88653fe"},
-    {file = "pytest-6.1.2.tar.gz", hash = "sha256:c0a7e94a8cdbc5422a51ccdad8e6f1024795939cc89159a0ae7f0b316ad3823e"},
+    {file = "pytest-6.2.1-py3-none-any.whl", hash = "sha256:1969f797a1a0dbd8ccf0fecc80262312729afea9c17f1d70ebf85c5e76c6f7c8"},
+    {file = "pytest-6.2.1.tar.gz", hash = "sha256:66e419b1899bc27346cb2c993e12c5e5e8daba9073c1fbce33b9807abc95c306"},
 ]
 pytest-cov = [
     {file = "pytest-cov-2.10.1.tar.gz", hash = "sha256:47bd0ce14056fdd79f93e1713f88fad7bdcc583dcd7783da86ef2f085a0bb88e"},
@@ -2947,8 +2988,8 @@ regex = [
     {file = "regex-2020.11.13.tar.gz", hash = "sha256:83d6b356e116ca119db8e7c6fc2983289d87b27b3fac238cfe5dca529d884562"},
 ]
 requests = [
-    {file = "requests-2.25.0-py2.py3-none-any.whl", hash = "sha256:e786fa28d8c9154e6a4de5d46a1d921b8749f8b74e28bde23768e5e16eece998"},
-    {file = "requests-2.25.0.tar.gz", hash = "sha256:7f1a0b932f4a60a1a65caa4263921bb7d9ee911957e0ae4a23a6dd08185ad5f8"},
+    {file = "requests-2.25.1-py2.py3-none-any.whl", hash = "sha256:c210084e36a42ae6b9219e00e48287def368a26d03a048ddad7bfee44f75871e"},
+    {file = "requests-2.25.1.tar.gz", hash = "sha256:27973dd4a904a4f13b263a19c866c13b92a39ed1c964655f025f3f8d3d75b804"},
 ]
 restructuredtext-lint = [
     {file = "restructuredtext_lint-1.3.2.tar.gz", hash = "sha256:d3b10a1fe2ecac537e51ae6d151b223b78de9fafdd50e5eb6b08c243df173c80"},
@@ -3009,8 +3050,8 @@ snowballstemmer = [
     {file = "snowballstemmer-2.0.0.tar.gz", hash = "sha256:df3bac3df4c2c01363f3dd2cfa78cce2840a79b9f1c2d2de9ce8d31683992f52"},
 ]
 spdx-license-list = [
-    {file = "spdx_license_list-0.5.1-py3-none-any.whl", hash = "sha256:32f1401e0077b46ba8b3d9c648b6503ef1d49c41aab51aa13816be2dde3b4a13"},
-    {file = "spdx_license_list-0.5.1.tar.gz", hash = "sha256:64cb5de37724c64cdeccafa2ae68667ff8ccdb7b688f51c1c2be82d7ebe3a112"},
+    {file = "spdx_license_list-0.5.2-py3-none-any.whl", hash = "sha256:1b338470c7b403dbecceca563a316382c7977516128ca6c1e8f7078e3ed6e7b0"},
+    {file = "spdx_license_list-0.5.2.tar.gz", hash = "sha256:952996f72ab807972dc2278bb9b91e5294767211e51f09aad9c0e2ff5b82a31b"},
 ]
 sphinx = [
     {file = "Sphinx-3.3.1-py3-none-any.whl", hash = "sha256:d4e59ad4ea55efbb3c05cde3bfc83bfc14f0c95aa95c3d75346fcce186a47960"},
@@ -3053,15 +3094,15 @@ sqlparse = [
     {file = "sqlparse-0.4.1.tar.gz", hash = "sha256:0f91fd2e829c44362cbcfab3e9ae12e22badaa8a29ad5ff599f9ec109f0454e8"},
 ]
 stevedore = [
-    {file = "stevedore-3.2.2-py3-none-any.whl", hash = "sha256:5e1ab03eaae06ef6ce23859402de785f08d97780ed774948ef16c4652c41bc62"},
-    {file = "stevedore-3.2.2.tar.gz", hash = "sha256:f845868b3a3a77a2489d226568abe7328b5c2d4f6a011cc759dfa99144a521f0"},
+    {file = "stevedore-3.3.0-py3-none-any.whl", hash = "sha256:50d7b78fbaf0d04cd62411188fa7eedcb03eb7f4c4b37005615ceebe582aa82a"},
+    {file = "stevedore-3.3.0.tar.gz", hash = "sha256:3a5bbd0652bf552748871eaa73a4a8dc2899786bc497a2aa1fcb4dcdb0debeee"},
 ]
 termcolor = [
     {file = "termcolor-1.1.0.tar.gz", hash = "sha256:1d6d69ce66211143803fbc56652b41d73b4a400a2891d7bf7a1cdf4c02de613b"},
 ]
 testfixtures = [
-    {file = "testfixtures-6.15.0-py2.py3-none-any.whl", hash = "sha256:e17f4f526fc90b0ac9bc7f8ca62b7dec17d9faf3d721f56bda4f0fd94d02f85a"},
-    {file = "testfixtures-6.15.0.tar.gz", hash = "sha256:409f77cfbdad822d12a8ce5c4aa8fb4d0bb38073f4a5444fede3702716a2cec2"},
+    {file = "testfixtures-6.17.0-py2.py3-none-any.whl", hash = "sha256:ebcc3e024d47bb58a60cdc678604151baa0c920ae2814004c89ac9066de31b2c"},
+    {file = "testfixtures-6.17.0.tar.gz", hash = "sha256:fa7c170df68ca6367eda061e9ec339ae3e6d3679c31e04033f83ef97a7d7d0ce"},
 ]
 "testing.common.database" = [
     {file = "testing.common.database-2.0.3-py2.py3-none-any.whl", hash = "sha256:e3ed492bf480a87f271f74c53b262caf5d85c8bc09989a8f534fa2283ec52492"},
@@ -3080,11 +3121,11 @@ toml = [
     {file = "toml-0.10.2.tar.gz", hash = "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f"},
 ]
 tqdm = [
-    {file = "tqdm-4.54.0-py2.py3-none-any.whl", hash = "sha256:9e7b8ab0ecbdbf0595adadd5f0ebbb9e69010e0bd48bbb0c15e550bf2a5292df"},
-    {file = "tqdm-4.54.0.tar.gz", hash = "sha256:5c0d04e06ccc0da1bd3fa5ae4550effcce42fcad947b4a6cafa77bdc9b09ff22"},
+    {file = "tqdm-4.54.1-py2.py3-none-any.whl", hash = "sha256:d4f413aecb61c9779888c64ddf0c62910ad56dcbe857d8922bb505d4dbff0df1"},
+    {file = "tqdm-4.54.1.tar.gz", hash = "sha256:38b658a3e4ecf9b4f6f8ff75ca16221ae3378b2e175d846b6b33ea3a20852cf5"},
 ]
 twilio = [
-    {file = "twilio-6.48.0.tar.gz", hash = "sha256:745f3dfe6685e001ddd2baa290b37377426388906ea6d3549e06fb2f669da7b3"},
+    {file = "twilio-6.50.1.tar.gz", hash = "sha256:dd8371c9b4ea422d6de7526b63b587da82e8488f2b3f6b1258d2cad6e4006a65"},
 ]
 typed-ast = [
     {file = "typed_ast-1.4.1-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:73d785a950fc82dd2a25897d525d003f6378d1cb23ab305578394694202a58c3"},
diff --git a/pyproject.toml b/pyproject.toml
index 9d0c7e7a40c795c77697aefea7b04cbef1b8e284..cfb02aa3649f591f00fac32004c0a397e43a09ab 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -89,6 +89,7 @@ django-favicon-plus-reloaded = "^1.0.4"
 django-health-check = "^3.12.1"
 psutil = "^5.7.0"
 celery-progress = "^0.0.14"
+django-cachalot = "^2.3.2"
 django-prometheus = "^2.1.0"
 importlib-metadata = {version = "^3.0.0", python = "<3.9"}
 django-model-utils = "^4.0.0"
@@ -100,6 +101,9 @@ celery = ["Celery", "django-celery-results", "django-celery-beat", "django-celer
 [tool.poetry.dev-dependencies]
 aleksis-builddeps = "*"
 
+[tool.poetry.scripts]
+aleksis-admin = 'aleksis.core.util.manage:aleksis_cmd'
+
 [tool.black]
 line-length = 100
 exclude = "/migrations/"