diff --git a/Dockerfile b/Dockerfile
index 3eb58d43d6b60bcfa899de3df1fb11d156f60217..c2b7db5ebe8f4b8f2198ceb62fdfd984e541e897 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -1,4 +1,4 @@
-FROM python:3.9-buster AS core
+FROM debian:bullseye-slim AS core
 
 # Build arguments
 ARG EXTRAS="ldap,s3"
@@ -12,6 +12,7 @@ ENV PIP_DISABLE_PIP_VERSION_CHECK 1
 ENV PIP_NO_CACHE_DIR 1
 ENV PIP_EXTRA_INDEX_URL https://edugit.org/api/v4/projects/461/packages/pypi/simple
 ENV PIP_USE_DEPRECATED legacy-resolver
+ENV DEBIAN_FRONTEND noninteractive
 
 # Configure app settings for build and runtime
 ENV ALEKSIS_static__root /usr/share/aleksis/static
@@ -25,14 +26,18 @@ RUN apt-get -y update && \
     eatmydata apt-get -y upgrade && \
     eatmydata apt-get install -y --no-install-recommends \
         build-essential \
+    chromium \
 	dumb-init \
 	gettext \
 	libpq5 \
 	libpq-dev \
 	libssl-dev \
 	postgresql-client \
-	yarnpkg && \
-    eatmydata pip install uwsgi
+	python3-dev \
+	python3-pip \
+	uwsgi \
+	uwsgi-plugin-python3 \
+	yarnpkg
 
 # Install extra dependencies
 RUN   case ",$EXTRAS," in \
@@ -73,7 +78,8 @@ RUN set -e; \
         libpq-dev \
         libssl-dev \
         libldap2-dev \
-        libsasl2-dev; \
+        libsasl2-dev \
+        python3-dev; \
     eatmydata apt-get autoremove --purge -y; \
     apt-get clean -y; \
     rm -rf /root/.cache
diff --git a/README.rst b/README.rst
index 067f83e32bfedf6251c91b5d5e42162c1db97b9e..5946b0d4eb15331b918e95446fddbc440250bfcb 100644
--- a/README.rst
+++ b/README.rst
@@ -36,16 +36,16 @@ Licence
 
 ::
 
-  Copyright © 2021 magicfelix <felix@felix-zauberer.de>
-  Copyright © 2017, 2018, 2019, 2020 Jonathan Weth <wethjo@katharineum.de>
-  Copyright © 2017, 2018, 2019 Frank Poetzsch-Heffter <p-h@katharineum.de>
-  Copyright © 2018, 2019, 2020 Julian Leucker <leuckeju@katharineum.de>
-  Copyright © 2018, 2019, 2020 Hangzhi Yu <yuha@katharineum.de>
-  Copyright © 2019, 2020 Dominik George <dominik.george@teckids.org>
-  Copyright © 2019, 2020 Tom Teichler <tom.teichler@teckids.org>
+  Copyright © 2017, 2018, 2019, 2020, 2021 Jonathan Weth <wethjo@katharineum.de>
+  Copyright © 2017, 2018, 2019, 2020 Frank Poetzsch-Heffter <p-h@katharineum.de>
+  Copyright © 2018, 2019, 2020, 2021 Julian Leucker <leuckeju@katharineum.de>
+  Copyright © 2018, 2019, 2020, 2021 Hangzhi Yu <yuha@katharineum.de>
+  Copyright © 2019, 2020, 2021 Dominik George <dominik.george@teckids.org>
+  Copyright © 2019, 2020, 2021 Tom Teichler <tom.teichler@teckids.org>
   Copyright © 2019 mirabilos <thorsten.glaser@teckids.org>
+  Copyright © 2021 magicfelix <felix@felix-zauberer.de>
 
-  Licenced under the EUPL, version 1.2 or later
+  Licenced under the EUPL, version 1.2 or later, by Teckids e.V. (Bonn, Germany).
 
 Please see the LICENCE.rst file accompanying this distribution for the
 full licence text or on the `European Union Public Licence`_ website
diff --git a/aleksis/core/apps.py b/aleksis/core/apps.py
index a3b9b7a9a3957b9af7ad0b9ddeb57d00a630ca73..fa48d04407a4e047ea98e6ce7b1102158ba25fbb 100644
--- a/aleksis/core/apps.py
+++ b/aleksis/core/apps.py
@@ -1,4 +1,4 @@
-from typing import Any, List, Optional, Tuple
+from typing import Any, Optional
 
 import django.apps
 from django.apps import apps
@@ -28,14 +28,14 @@ class CoreConfig(AppConfig):
     }
     licence = "EUPL-1.2+"
     copyright_info = (
-        ([2021], "magicfelix", "felix@felix-zauberer.de"),
-        ([2017, 2018, 2019, 2020], "Jonathan Weth", "wethjo@katharineum.de"),
-        ([2017, 2018, 2019], "Frank Poetzsch-Heffter", "p-h@katharineum.de"),
-        ([2018, 2019, 2020], "Julian Leucker", "leuckeju@katharineum.de"),
-        ([2018, 2019, 2020], "Hangzhi Yu", "yuha@katharineum.de"),
-        ([2019, 2020], "Dominik George", "dominik.george@teckids.org"),
-        ([2019, 2020], "Tom Teichler", "tom.teichler@teckids.org"),
+        ([2017, 2018, 2019, 2020, 2021], "Jonathan Weth", "wethjo@katharineum.de"),
+        ([2017, 2018, 2019, 2020], "Frank Poetzsch-Heffter", "p-h@katharineum.de"),
+        ([2018, 2019, 2020, 2021], "Julian Leucker", "leuckeju@katharineum.de"),
+        ([2018, 2019, 2020, 2021], "Hangzhi Yu", "yuha@katharineum.de"),
+        ([2019, 2020, 2021], "Dominik George", "dominik.george@teckids.org"),
+        ([2019, 2020, 2021], "Tom Teichler", "tom.teichler@teckids.org"),
         ([2019], "mirabilos", "thorsten.glaser@teckids.org"),
+        ([2021], "magicfelix", "felix@felix-zauberer.de"),
     )
 
     def ready(self):
@@ -107,11 +107,9 @@ class CoreConfig(AppConfig):
         verbosity: int,
         interactive: bool,
         using: str,
-        plan: List[Tuple],
-        apps: django.apps.registry.Apps,
         **kwargs,
     ) -> None:
-        super().post_migrate(app_config, verbosity, interactive, using, plan, apps)
+        super().post_migrate(app_config, verbosity, interactive, using, **kwargs)
 
         # Ensure presence of an OTP YubiKey default config
         apps.get_model("otp_yubikey", "ValidationService").objects.using(using).update_or_create(
diff --git a/aleksis/core/forms.py b/aleksis/core/forms.py
index feb7d996b7cca83c040ef7add8a5355e82335e82..dd738da735eded0be57bf60cb7f0f651e38d333e 100644
--- a/aleksis/core/forms.py
+++ b/aleksis/core/forms.py
@@ -10,6 +10,7 @@ from django.utils.translation import gettext_lazy as _
 
 from django_select2.forms import ModelSelect2MultipleWidget, ModelSelect2Widget, Select2Widget
 from dynamic_preferences.forms import PreferenceForm
+from guardian.core import ObjectPermissionChecker
 from material import Fieldset, Layout, Row
 
 from .mixins import ExtensibleForm, SchoolTermRelatedExtensibleForm
@@ -137,6 +138,22 @@ class EditPersonForm(ExtensibleForm):
         required=False, label=_("New user"), help_text=_("Create a new account")
     )
 
+    def __init__(self, request: HttpRequest, *args, **kwargs):
+        super().__init__(*args, **kwargs)
+
+        # Disable non-editable fields
+        person_fields = set([field.name for field in Person.syncable_fields()]).intersection(
+            set(self.fields)
+        )
+
+        if self.instance:
+            checker = ObjectPermissionChecker(request.user)
+            checker.prefetch_perms([self.instance])
+
+            for field in person_fields:
+                if not checker.has_perm(f"core.change_person_field_{field}", self.instance):
+                    self.fields[field].disabled = True
+
     def clean(self) -> None:
         # Use code implemented in dedicated form to verify user selection
         return PersonAccountForm.clean(self)
diff --git a/aleksis/core/locale/de_DE/LC_MESSAGES/django.po b/aleksis/core/locale/de_DE/LC_MESSAGES/django.po
index c26e99b1bc38619b7b131cb67718516e4b6da5e1..d8a3b4e5a53a776cd9fb0433267539bde11efe99 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: 2021-01-11 21:30+0100\n"
-"PO-Revision-Date: 2021-02-17 12:13+0000\n"
+"PO-Revision-Date: 2021-03-14 11:51+0000\n"
 "Last-Translator: Jonathan Weth <teckids@jonathanweth.de>\n"
 "Language-Team: German <https://translate.edugit.org/projects/aleksis/aleksis/"
 "de/>\n"
@@ -619,7 +619,7 @@ msgstr "Menü"
 
 #: models.py:787
 msgid "Icon"
-msgstr "Icon"
+msgstr "Symbol"
 
 #: models.py:793
 msgid "Custom menu item"
diff --git a/aleksis/core/migrations/0001_initial.py b/aleksis/core/migrations/0001_initial.py
index 07e4bf433df4d8aec43814493d7dde6527d1f8f1..14041d3eca0f1830dc4da2154f318ad7f7c1d516 100644
--- a/aleksis/core/migrations/0001_initial.py
+++ b/aleksis/core/migrations/0001_initial.py
@@ -37,7 +37,7 @@ class Migration(migrations.Migration):
             name='AdditionalField',
             fields=[
                 ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
-                ('extended_data', django.contrib.postgres.fields.jsonb.JSONField(default=dict, editable=False)),
+                ('extended_data', models.JSONField(default=dict, editable=False)),
                 ('title', models.CharField(max_length=255, verbose_name='Title of field')),
                 ('field_type', models.CharField(choices=[('BooleanField', 'Boolean (Yes/No)'), ('CharField', 'Text (one line)'), ('DateField', 'Date'), ('DateTimeField', 'Date and time'), ('DecimalField', 'Decimal number'), ('EmailField', 'E-mail address'), ('IntegerField', 'Integer'), ('GenericIPAddressField', 'IP address'), ('NullBooleanField', 'Boolean or empty (Yes/No/Neither)'), ('TextField', 'Text (multi-line)'), ('TimeField', 'Time'), ('URLField', 'URL / Link')], max_length=50, verbose_name='Type of field')),
                 ('site', models.ForeignKey(default=1, editable=False, on_delete=django.db.models.deletion.CASCADE, to='sites.Site')),
@@ -54,7 +54,7 @@ class Migration(migrations.Migration):
             name='Announcement',
             fields=[
                 ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
-                ('extended_data', django.contrib.postgres.fields.jsonb.JSONField(default=dict, editable=False)),
+                ('extended_data', models.JSONField(default=dict, editable=False)),
                 ('title', models.CharField(max_length=150, verbose_name='Title')),
                 ('description', models.TextField(blank=True, max_length=500, verbose_name='Description')),
                 ('link', models.URLField(blank=True, verbose_name='Link to detailed view')),
@@ -71,7 +71,7 @@ class Migration(migrations.Migration):
             name='CustomMenu',
             fields=[
                 ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
-                ('extended_data', django.contrib.postgres.fields.jsonb.JSONField(default=dict, editable=False)),
+                ('extended_data', models.JSONField(default=dict, editable=False)),
                 ('name', models.CharField(max_length=100, unique=True, verbose_name='Menu ID')),
                 ('site', models.ForeignKey(default=1, editable=False, on_delete=django.db.models.deletion.CASCADE, to='sites.Site')),
             ],
@@ -87,7 +87,7 @@ class Migration(migrations.Migration):
             name='Group',
             fields=[
                 ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
-                ('extended_data', django.contrib.postgres.fields.jsonb.JSONField(default=dict, editable=False)),
+                ('extended_data', models.JSONField(default=dict, editable=False)),
                 ('name', models.CharField(max_length=255, unique=True, verbose_name='Long name')),
                 ('short_name', models.CharField(blank=True, max_length=255, null=True, unique=True, verbose_name='Short name')),
                 ('additional_fields', models.ManyToManyField(to='core.AdditionalField', verbose_name='Additional fields')),
@@ -106,7 +106,7 @@ class Migration(migrations.Migration):
             name='Person',
             fields=[
                 ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
-                ('extended_data', django.contrib.postgres.fields.jsonb.JSONField(default=dict, editable=False)),
+                ('extended_data', models.JSONField(default=dict, editable=False)),
                 ('is_active', models.BooleanField(default=True, verbose_name='Is person active?')),
                 ('first_name', models.CharField(max_length=255, verbose_name='First name')),
                 ('last_name', models.CharField(max_length=255, verbose_name='Last name')),
@@ -178,7 +178,7 @@ class Migration(migrations.Migration):
             name='PersonGroupThrough',
             fields=[
                 ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
-                ('extended_data', django.contrib.postgres.fields.jsonb.JSONField(default=dict, editable=False)),
+                ('extended_data', models.JSONField(default=dict, editable=False)),
                 ('group', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='core.Group')),
                 ('person', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='core.Person')),
                 ('site', models.ForeignKey(default=1, editable=False, on_delete=django.db.models.deletion.CASCADE, to='sites.Site')),
@@ -194,7 +194,7 @@ class Migration(migrations.Migration):
             name='Notification',
             fields=[
                 ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
-                ('extended_data', django.contrib.postgres.fields.jsonb.JSONField(default=dict, editable=False)),
+                ('extended_data', models.JSONField(default=dict, editable=False)),
                 ('sender', models.CharField(max_length=100, verbose_name='Sender')),
                 ('title', models.CharField(max_length=150, verbose_name='Title')),
                 ('description', models.TextField(max_length=500, verbose_name='Description')),
@@ -216,7 +216,7 @@ class Migration(migrations.Migration):
             name='GroupType',
             fields=[
                 ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
-                ('extended_data', django.contrib.postgres.fields.jsonb.JSONField(default=dict, editable=False)),
+                ('extended_data', models.JSONField(default=dict, editable=False)),
                 ('name', models.CharField(max_length=50, verbose_name='Title of type')),
                 ('description', models.CharField(max_length=500, verbose_name='Description')),
                 ('site', models.ForeignKey(default=1, editable=False, on_delete=django.db.models.deletion.CASCADE, to='sites.Site')),
@@ -283,7 +283,7 @@ class Migration(migrations.Migration):
             name='CustomMenuItem',
             fields=[
                 ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
-                ('extended_data', django.contrib.postgres.fields.jsonb.JSONField(default=dict, editable=False)),
+                ('extended_data', models.JSONField(default=dict, editable=False)),
                 ('name', models.CharField(max_length=150, verbose_name='Name')),
                 ('url', models.URLField(verbose_name='Link')),
                 ('icon', models.CharField(blank=True, choices=[('3d_rotation', '3d_rotation'), ('ac_unit', 'ac_unit'), ('access_alarm', 'access_alarm'), ('access_alarms', 'access_alarms'), ('access_time', 'access_time'), ('accessibility', 'accessibility'), ('accessible', 'accessible'), ('account_balance', 'account_balance'), ('account_balance_wallet', 'account_balance_wallet'), ('account_box', 'account_box'), ('account_circle', 'account_circle'), ('adb', 'adb'), ('add', 'add'), ('add_a_photo', 'add_a_photo'), ('add_alarm', 'add_alarm'), ('add_alert', 'add_alert'), ('add_box', 'add_box'), ('add_circle', 'add_circle'), ('add_circle_outline', 'add_circle_outline'), ('add_location', 'add_location'), ('add_shopping_cart', 'add_shopping_cart'), ('add_to_photos', 'add_to_photos'), ('add_to_queue', 'add_to_queue'), ('adjust', 'adjust'), ('airline_seat_flat', 'airline_seat_flat'), ('airline_seat_flat_angled', 'airline_seat_flat_angled'), ('airline_seat_individual_suite', 'airline_seat_individual_suite'), ('airline_seat_legroom_extra', 'airline_seat_legroom_extra'), ('airline_seat_legroom_normal', 'airline_seat_legroom_normal'), ('airline_seat_legroom_reduced', 'airline_seat_legroom_reduced'), ('airline_seat_recline_extra', 'airline_seat_recline_extra'), ('airline_seat_recline_normal', 'airline_seat_recline_normal'), ('airplanemode_active', 'airplanemode_active'), ('airplanemode_inactive', 'airplanemode_inactive'), ('airplay', 'airplay'), ('airport_shuttle', 'airport_shuttle'), ('alarm', 'alarm'), ('alarm_add', 'alarm_add'), ('alarm_off', 'alarm_off'), ('alarm_on', 'alarm_on'), ('album', 'album'), ('all_inclusive', 'all_inclusive'), ('all_out', 'all_out'), ('android', 'android'), ('announcement', 'announcement'), ('apps', 'apps'), ('archive', 'archive'), ('arrow_back', 'arrow_back'), ('arrow_downward', 'arrow_downward'), ('arrow_drop_down', 'arrow_drop_down'), ('arrow_drop_down_circle', 'arrow_drop_down_circle'), ('arrow_drop_up', 'arrow_drop_up'), ('arrow_forward', 'arrow_forward'), ('arrow_upward', 'arrow_upward'), ('art_track', 'art_track'), ('aspect_ratio', 'aspect_ratio'), ('assessment', 'assessment'), ('assignment', 'assignment'), ('assignment_ind', 'assignment_ind'), ('assignment_late', 'assignment_late'), ('assignment_return', 'assignment_return'), ('assignment_returned', 'assignment_returned'), ('assignment_turned_in', 'assignment_turned_in'), ('assistant', 'assistant'), ('assistant_photo', 'assistant_photo'), ('attach_file', 'attach_file'), ('attach_money', 'attach_money'), ('attachment', 'attachment'), ('audiotrack', 'audiotrack'), ('autorenew', 'autorenew'), ('av_timer', 'av_timer'), ('backspace', 'backspace'), ('backup', 'backup'), ('battery_alert', 'battery_alert'), ('battery_charging_full', 'battery_charging_full'), ('battery_full', 'battery_full'), ('battery_std', 'battery_std'), ('battery_unknown', 'battery_unknown'), ('beach_access', 'beach_access'), ('beenhere', 'beenhere'), ('block', 'block'), ('bluetooth', 'bluetooth'), ('bluetooth_audio', 'bluetooth_audio'), ('bluetooth_connected', 'bluetooth_connected'), ('bluetooth_disabled', 'bluetooth_disabled'), ('bluetooth_searching', 'bluetooth_searching'), ('blur_circular', 'blur_circular'), ('blur_linear', 'blur_linear'), ('blur_off', 'blur_off'), ('blur_on', 'blur_on'), ('book', 'book'), ('bookmark', 'bookmark'), ('bookmark_border', 'bookmark_border'), ('border_all', 'border_all'), ('border_bottom', 'border_bottom'), ('border_clear', 'border_clear'), ('border_color', 'border_color'), ('border_horizontal', 'border_horizontal'), ('border_inner', 'border_inner'), ('border_left', 'border_left'), ('border_outer', 'border_outer'), ('border_right', 'border_right'), ('border_style', 'border_style'), ('border_top', 'border_top'), ('border_vertical', 'border_vertical'), ('branding_watermark', 'branding_watermark'), ('brightness_1', 'brightness_1'), ('brightness_2', 'brightness_2'), ('brightness_3', 'brightness_3'), ('brightness_4', 'brightness_4'), ('brightness_5', 'brightness_5'), ('brightness_6', 'brightness_6'), ('brightness_7', 'brightness_7'), ('brightness_auto', 'brightness_auto'), ('brightness_high', 'brightness_high'), ('brightness_low', 'brightness_low'), ('brightness_medium', 'brightness_medium'), ('broken_image', 'broken_image'), ('brush', 'brush'), ('bubble_chart', 'bubble_chart'), ('bug_report', 'bug_report'), ('build', 'build'), ('burst_mode', 'burst_mode'), ('business', 'business'), ('business_center', 'business_center'), ('cached', 'cached'), ('cake', 'cake'), ('call', 'call'), ('call_end', 'call_end'), ('call_made', 'call_made'), ('call_merge', 'call_merge'), ('call_missed', 'call_missed'), ('call_missed_outgoing', 'call_missed_outgoing'), ('call_received', 'call_received'), ('call_split', 'call_split'), ('call_to_action', 'call_to_action'), ('camera', 'camera'), ('camera_alt', 'camera_alt'), ('camera_enhance', 'camera_enhance'), ('camera_front', 'camera_front'), ('camera_rear', 'camera_rear'), ('camera_roll', 'camera_roll'), ('cancel', 'cancel'), ('card_giftcard', 'card_giftcard'), ('card_membership', 'card_membership'), ('card_travel', 'card_travel'), ('casino', 'casino'), ('cast', 'cast'), ('cast_connected', 'cast_connected'), ('center_focus_strong', 'center_focus_strong'), ('center_focus_weak', 'center_focus_weak'), ('change_history', 'change_history'), ('chat', 'chat'), ('chat_bubble', 'chat_bubble'), ('chat_bubble_outline', 'chat_bubble_outline'), ('check', 'check'), ('check_box', 'check_box'), ('check_box_outline_blank', 'check_box_outline_blank'), ('check_circle', 'check_circle'), ('chevron_left', 'chevron_left'), ('chevron_right', 'chevron_right'), ('child_care', 'child_care'), ('child_friendly', 'child_friendly'), ('chrome_reader_mode', 'chrome_reader_mode'), ('class', 'class'), ('clear', 'clear'), ('clear_all', 'clear_all'), ('close', 'close'), ('closed_caption', 'closed_caption'), ('cloud', 'cloud'), ('cloud_circle', 'cloud_circle'), ('cloud_done', 'cloud_done'), ('cloud_download', 'cloud_download'), ('cloud_off', 'cloud_off'), ('cloud_queue', 'cloud_queue'), ('cloud_upload', 'cloud_upload'), ('code', 'code'), ('collections', 'collections'), ('collections_bookmark', 'collections_bookmark'), ('color_lens', 'color_lens'), ('colorize', 'colorize'), ('comment', 'comment'), ('compare', 'compare'), ('compare_arrows', 'compare_arrows'), ('computer', 'computer'), ('confirmation_number', 'confirmation_number'), ('contact_mail', 'contact_mail'), ('contact_phone', 'contact_phone'), ('contacts', 'contacts'), ('content_copy', 'content_copy'), ('content_cut', 'content_cut'), ('content_paste', 'content_paste'), ('control_point', 'control_point'), ('control_point_duplicate', 'control_point_duplicate'), ('copyright', 'copyright'), ('create', 'create'), ('create_new_folder', 'create_new_folder'), ('credit_card', 'credit_card'), ('crop', 'crop'), ('crop_16_9', 'crop_16_9'), ('crop_3_2', 'crop_3_2'), ('crop_5_4', 'crop_5_4'), ('crop_7_5', 'crop_7_5'), ('crop_din', 'crop_din'), ('crop_free', 'crop_free'), ('crop_landscape', 'crop_landscape'), ('crop_original', 'crop_original'), ('crop_portrait', 'crop_portrait'), ('crop_rotate', 'crop_rotate'), ('crop_square', 'crop_square'), ('dashboard', 'dashboard'), ('data_usage', 'data_usage'), ('date_range', 'date_range'), ('dehaze', 'dehaze'), ('delete', 'delete'), ('delete_forever', 'delete_forever'), ('delete_sweep', 'delete_sweep'), ('description', 'description'), ('desktop_mac', 'desktop_mac'), ('desktop_windows', 'desktop_windows'), ('details', 'details'), ('developer_board', 'developer_board'), ('developer_mode', 'developer_mode'), ('device_hub', 'device_hub'), ('devices', 'devices'), ('devices_other', 'devices_other'), ('dialer_sip', 'dialer_sip'), ('dialpad', 'dialpad'), ('directions', 'directions'), ('directions_bike', 'directions_bike'), ('directions_boat', 'directions_boat'), ('directions_bus', 'directions_bus'), ('directions_car', 'directions_car'), ('directions_railway', 'directions_railway'), ('directions_run', 'directions_run'), ('directions_subway', 'directions_subway'), ('directions_transit', 'directions_transit'), ('directions_walk', 'directions_walk'), ('disc_full', 'disc_full'), ('dns', 'dns'), ('do_not_disturb', 'do_not_disturb'), ('do_not_disturb_alt', 'do_not_disturb_alt'), ('do_not_disturb_off', 'do_not_disturb_off'), ('do_not_disturb_on', 'do_not_disturb_on'), ('dock', 'dock'), ('domain', 'domain'), ('done', 'done'), ('done_all', 'done_all'), ('donut_large', 'donut_large'), ('donut_small', 'donut_small'), ('drafts', 'drafts'), ('drag_handle', 'drag_handle'), ('drive_eta', 'drive_eta'), ('dvr', 'dvr'), ('edit', 'edit'), ('edit_location', 'edit_location'), ('eject', 'eject'), ('email', 'email'), ('enhanced_encryption', 'enhanced_encryption'), ('equalizer', 'equalizer'), ('error', 'error'), ('error_outline', 'error_outline'), ('euro_symbol', 'euro_symbol'), ('ev_station', 'ev_station'), ('event', 'event'), ('event_available', 'event_available'), ('event_busy', 'event_busy'), ('event_note', 'event_note'), ('event_seat', 'event_seat'), ('exit_to_app', 'exit_to_app'), ('expand_less', 'expand_less'), ('expand_more', 'expand_more'), ('explicit', 'explicit'), ('explore', 'explore'), ('exposure', 'exposure'), ('exposure_neg_1', 'exposure_neg_1'), ('exposure_neg_2', 'exposure_neg_2'), ('exposure_plus_1', 'exposure_plus_1'), ('exposure_plus_2', 'exposure_plus_2'), ('exposure_zero', 'exposure_zero'), ('extension', 'extension'), ('face', 'face'), ('fast_forward', 'fast_forward'), ('fast_rewind', 'fast_rewind'), ('favorite', 'favorite'), ('favorite_border', 'favorite_border'), ('featured_play_list', 'featured_play_list'), ('featured_video', 'featured_video'), ('feedback', 'feedback'), ('fiber_dvr', 'fiber_dvr'), ('fiber_manual_record', 'fiber_manual_record'), ('fiber_new', 'fiber_new'), ('fiber_pin', 'fiber_pin'), ('fiber_smart_record', 'fiber_smart_record'), ('file_download', 'file_download'), ('file_upload', 'file_upload'), ('filter', 'filter'), ('filter_1', 'filter_1'), ('filter_2', 'filter_2'), ('filter_3', 'filter_3'), ('filter_4', 'filter_4'), ('filter_5', 'filter_5'), ('filter_6', 'filter_6'), ('filter_7', 'filter_7'), ('filter_8', 'filter_8'), ('filter_9', 'filter_9'), ('filter_9_plus', 'filter_9_plus'), ('filter_b_and_w', 'filter_b_and_w'), ('filter_center_focus', 'filter_center_focus'), ('filter_drama', 'filter_drama'), ('filter_frames', 'filter_frames'), ('filter_hdr', 'filter_hdr'), ('filter_list', 'filter_list'), ('filter_none', 'filter_none'), ('filter_tilt_shift', 'filter_tilt_shift'), ('filter_vintage', 'filter_vintage'), ('find_in_page', 'find_in_page'), ('find_replace', 'find_replace'), ('fingerprint', 'fingerprint'), ('first_page', 'first_page'), ('fitness_center', 'fitness_center'), ('flag', 'flag'), ('flare', 'flare'), ('flash_auto', 'flash_auto'), ('flash_off', 'flash_off'), ('flash_on', 'flash_on'), ('flight', 'flight'), ('flight_land', 'flight_land'), ('flight_takeoff', 'flight_takeoff'), ('flip', 'flip'), ('flip_to_back', 'flip_to_back'), ('flip_to_front', 'flip_to_front'), ('folder', 'folder'), ('folder_open', 'folder_open'), ('folder_shared', 'folder_shared'), ('folder_special', 'folder_special'), ('font_download', 'font_download'), ('format_align_center', 'format_align_center'), ('format_align_justify', 'format_align_justify'), ('format_align_left', 'format_align_left'), ('format_align_right', 'format_align_right'), ('format_bold', 'format_bold'), ('format_clear', 'format_clear'), ('format_color_fill', 'format_color_fill'), ('format_color_reset', 'format_color_reset'), ('format_color_text', 'format_color_text'), ('format_indent_decrease', 'format_indent_decrease'), ('format_indent_increase', 'format_indent_increase'), ('format_italic', 'format_italic'), ('format_line_spacing', 'format_line_spacing'), ('format_list_bulleted', 'format_list_bulleted'), ('format_list_numbered', 'format_list_numbered'), ('format_paint', 'format_paint'), ('format_quote', 'format_quote'), ('format_shapes', 'format_shapes'), ('format_size', 'format_size'), ('format_strikethrough', 'format_strikethrough'), ('format_textdirection_l_to_r', 'format_textdirection_l_to_r'), ('format_textdirection_r_to_l', 'format_textdirection_r_to_l'), ('format_underlined', 'format_underlined'), ('forum', 'forum'), ('forward', 'forward'), ('forward_10', 'forward_10'), ('forward_30', 'forward_30'), ('forward_5', 'forward_5'), ('free_breakfast', 'free_breakfast'), ('fullscreen', 'fullscreen'), ('fullscreen_exit', 'fullscreen_exit'), ('functions', 'functions'), ('g_translate', 'g_translate'), ('gamepad', 'gamepad'), ('games', 'games'), ('gavel', 'gavel'), ('gesture', 'gesture'), ('get_app', 'get_app'), ('gif', 'gif'), ('golf_course', 'golf_course'), ('gps_fixed', 'gps_fixed'), ('gps_not_fixed', 'gps_not_fixed'), ('gps_off', 'gps_off'), ('grade', 'grade'), ('gradient', 'gradient'), ('grain', 'grain'), ('graphic_eq', 'graphic_eq'), ('grid_off', 'grid_off'), ('grid_on', 'grid_on'), ('group', 'group'), ('group_add', 'group_add'), ('group_work', 'group_work'), ('hd', 'hd'), ('hdr_off', 'hdr_off'), ('hdr_on', 'hdr_on'), ('hdr_strong', 'hdr_strong'), ('hdr_weak', 'hdr_weak'), ('headset', 'headset'), ('headset_mic', 'headset_mic'), ('healing', 'healing'), ('hearing', 'hearing'), ('help', 'help'), ('help_outline', 'help_outline'), ('high_quality', 'high_quality'), ('highlight', 'highlight'), ('highlight_off', 'highlight_off'), ('history', 'history'), ('home', 'home'), ('hot_tub', 'hot_tub'), ('hotel', 'hotel'), ('hourglass_empty', 'hourglass_empty'), ('hourglass_full', 'hourglass_full'), ('http', 'http'), ('https', 'https'), ('image', 'image'), ('image_aspect_ratio', 'image_aspect_ratio'), ('import_contacts', 'import_contacts'), ('import_export', 'import_export'), ('important_devices', 'important_devices'), ('inbox', 'inbox'), ('indeterminate_check_box', 'indeterminate_check_box'), ('info', 'info'), ('info_outline', 'info_outline'), ('input', 'input'), ('insert_chart', 'insert_chart'), ('insert_comment', 'insert_comment'), ('insert_drive_file', 'insert_drive_file'), ('insert_emoticon', 'insert_emoticon'), ('insert_invitation', 'insert_invitation'), ('insert_link', 'insert_link'), ('insert_photo', 'insert_photo'), ('invert_colors', 'invert_colors'), ('invert_colors_off', 'invert_colors_off'), ('iso', 'iso'), ('keyboard', 'keyboard'), ('keyboard_arrow_down', 'keyboard_arrow_down'), ('keyboard_arrow_left', 'keyboard_arrow_left'), ('keyboard_arrow_right', 'keyboard_arrow_right'), ('keyboard_arrow_up', 'keyboard_arrow_up'), ('keyboard_backspace', 'keyboard_backspace'), ('keyboard_capslock', 'keyboard_capslock'), ('keyboard_hide', 'keyboard_hide'), ('keyboard_return', 'keyboard_return'), ('keyboard_tab', 'keyboard_tab'), ('keyboard_voice', 'keyboard_voice'), ('kitchen', 'kitchen'), ('label', 'label'), ('label_outline', 'label_outline'), ('landscape', 'landscape'), ('language', 'language'), ('laptop', 'laptop'), ('laptop_chromebook', 'laptop_chromebook'), ('laptop_mac', 'laptop_mac'), ('laptop_windows', 'laptop_windows'), ('last_page', 'last_page'), ('launch', 'launch'), ('layers', 'layers'), ('layers_clear', 'layers_clear'), ('leak_add', 'leak_add'), ('leak_remove', 'leak_remove'), ('lens', 'lens'), ('library_add', 'library_add'), ('library_books', 'library_books'), ('library_music', 'library_music'), ('lightbulb_outline', 'lightbulb_outline'), ('line_style', 'line_style'), ('line_weight', 'line_weight'), ('linear_scale', 'linear_scale'), ('link', 'link'), ('linked_camera', 'linked_camera'), ('list', 'list'), ('live_help', 'live_help'), ('live_tv', 'live_tv'), ('local_activity', 'local_activity'), ('local_airport', 'local_airport'), ('local_atm', 'local_atm'), ('local_bar', 'local_bar'), ('local_cafe', 'local_cafe'), ('local_car_wash', 'local_car_wash'), ('local_convenience_store', 'local_convenience_store'), ('local_dining', 'local_dining'), ('local_drink', 'local_drink'), ('local_florist', 'local_florist'), ('local_gas_station', 'local_gas_station'), ('local_grocery_store', 'local_grocery_store'), ('local_hospital', 'local_hospital'), ('local_hotel', 'local_hotel'), ('local_laundry_service', 'local_laundry_service'), ('local_library', 'local_library'), ('local_mall', 'local_mall'), ('local_movies', 'local_movies'), ('local_offer', 'local_offer'), ('local_parking', 'local_parking'), ('local_pharmacy', 'local_pharmacy'), ('local_phone', 'local_phone'), ('local_pizza', 'local_pizza'), ('local_play', 'local_play'), ('local_post_office', 'local_post_office'), ('local_printshop', 'local_printshop'), ('local_see', 'local_see'), ('local_shipping', 'local_shipping'), ('local_taxi', 'local_taxi'), ('location_city', 'location_city'), ('location_disabled', 'location_disabled'), ('location_off', 'location_off'), ('location_on', 'location_on'), ('location_searching', 'location_searching'), ('lock', 'lock'), ('lock_open', 'lock_open'), ('lock_outline', 'lock_outline'), ('looks', 'looks'), ('looks_3', 'looks_3'), ('looks_4', 'looks_4'), ('looks_5', 'looks_5'), ('looks_6', 'looks_6'), ('looks_one', 'looks_one'), ('looks_two', 'looks_two'), ('loop', 'loop'), ('loupe', 'loupe'), ('low_priority', 'low_priority'), ('loyalty', 'loyalty'), ('mail', 'mail'), ('mail_outline', 'mail_outline'), ('map', 'map'), ('markunread', 'markunread'), ('markunread_mailbox', 'markunread_mailbox'), ('memory', 'memory'), ('menu', 'menu'), ('merge_type', 'merge_type'), ('message', 'message'), ('mic', 'mic'), ('mic_none', 'mic_none'), ('mic_off', 'mic_off'), ('mms', 'mms'), ('mode_comment', 'mode_comment'), ('mode_edit', 'mode_edit'), ('monetization_on', 'monetization_on'), ('money_off', 'money_off'), ('monochrome_photos', 'monochrome_photos'), ('mood', 'mood'), ('mood_bad', 'mood_bad'), ('more', 'more'), ('more_horiz', 'more_horiz'), ('more_vert', 'more_vert'), ('motorcycle', 'motorcycle'), ('mouse', 'mouse'), ('move_to_inbox', 'move_to_inbox'), ('movie', 'movie'), ('movie_creation', 'movie_creation'), ('movie_filter', 'movie_filter'), ('multiline_chart', 'multiline_chart'), ('music_note', 'music_note'), ('music_video', 'music_video'), ('my_location', 'my_location'), ('nature', 'nature'), ('nature_people', 'nature_people'), ('navigate_before', 'navigate_before'), ('navigate_next', 'navigate_next'), ('navigation', 'navigation'), ('near_me', 'near_me'), ('network_cell', 'network_cell'), ('network_check', 'network_check'), ('network_locked', 'network_locked'), ('network_wifi', 'network_wifi'), ('new_releases', 'new_releases'), ('next_week', 'next_week'), ('nfc', 'nfc'), ('no_encryption', 'no_encryption'), ('no_sim', 'no_sim'), ('not_interested', 'not_interested'), ('note', 'note'), ('note_add', 'note_add'), ('notifications', 'notifications'), ('notifications_active', 'notifications_active'), ('notifications_none', 'notifications_none'), ('notifications_off', 'notifications_off'), ('notifications_paused', 'notifications_paused'), ('offline_pin', 'offline_pin'), ('ondemand_video', 'ondemand_video'), ('opacity', 'opacity'), ('open_in_browser', 'open_in_browser'), ('open_in_new', 'open_in_new'), ('open_with', 'open_with'), ('pages', 'pages'), ('pageview', 'pageview'), ('palette', 'palette'), ('pan_tool', 'pan_tool'), ('panorama', 'panorama'), ('panorama_fish_eye', 'panorama_fish_eye'), ('panorama_horizontal', 'panorama_horizontal'), ('panorama_vertical', 'panorama_vertical'), ('panorama_wide_angle', 'panorama_wide_angle'), ('party_mode', 'party_mode'), ('pause', 'pause'), ('pause_circle_filled', 'pause_circle_filled'), ('pause_circle_outline', 'pause_circle_outline'), ('payment', 'payment'), ('people', 'people'), ('people_outline', 'people_outline'), ('perm_camera_mic', 'perm_camera_mic'), ('perm_contact_calendar', 'perm_contact_calendar'), ('perm_data_setting', 'perm_data_setting'), ('perm_device_information', 'perm_device_information'), ('perm_identity', 'perm_identity'), ('perm_media', 'perm_media'), ('perm_phone_msg', 'perm_phone_msg'), ('perm_scan_wifi', 'perm_scan_wifi'), ('person', 'person'), ('person_add', 'person_add'), ('person_outline', 'person_outline'), ('person_pin', 'person_pin'), ('person_pin_circle', 'person_pin_circle'), ('personal_video', 'personal_video'), ('pets', 'pets'), ('phone', 'phone'), ('phone_android', 'phone_android'), ('phone_bluetooth_speaker', 'phone_bluetooth_speaker'), ('phone_forwarded', 'phone_forwarded'), ('phone_in_talk', 'phone_in_talk'), ('phone_iphone', 'phone_iphone'), ('phone_locked', 'phone_locked'), ('phone_missed', 'phone_missed'), ('phone_paused', 'phone_paused'), ('phonelink', 'phonelink'), ('phonelink_erase', 'phonelink_erase'), ('phonelink_lock', 'phonelink_lock'), ('phonelink_off', 'phonelink_off'), ('phonelink_ring', 'phonelink_ring'), ('phonelink_setup', 'phonelink_setup'), ('photo', 'photo'), ('photo_album', 'photo_album'), ('photo_camera', 'photo_camera'), ('photo_filter', 'photo_filter'), ('photo_library', 'photo_library'), ('photo_size_select_actual', 'photo_size_select_actual'), ('photo_size_select_large', 'photo_size_select_large'), ('photo_size_select_small', 'photo_size_select_small'), ('picture_as_pdf', 'picture_as_pdf'), ('picture_in_picture', 'picture_in_picture'), ('picture_in_picture_alt', 'picture_in_picture_alt'), ('pie_chart', 'pie_chart'), ('pie_chart_outlined', 'pie_chart_outlined'), ('pin_drop', 'pin_drop'), ('place', 'place'), ('play_arrow', 'play_arrow'), ('play_circle_filled', 'play_circle_filled'), ('play_circle_outline', 'play_circle_outline'), ('play_for_work', 'play_for_work'), ('playlist_add', 'playlist_add'), ('playlist_add_check', 'playlist_add_check'), ('playlist_play', 'playlist_play'), ('plus_one', 'plus_one'), ('poll', 'poll'), ('polymer', 'polymer'), ('pool', 'pool'), ('portable_wifi_off', 'portable_wifi_off'), ('portrait', 'portrait'), ('power', 'power'), ('power_input', 'power_input'), ('power_settings_new', 'power_settings_new'), ('pregnant_woman', 'pregnant_woman'), ('present_to_all', 'present_to_all'), ('print', 'print'), ('priority_high', 'priority_high'), ('public', 'public'), ('publish', 'publish'), ('query_builder', 'query_builder'), ('question_answer', 'question_answer'), ('queue', 'queue'), ('queue_music', 'queue_music'), ('queue_play_next', 'queue_play_next'), ('radio', 'radio'), ('radio_button_checked', 'radio_button_checked'), ('radio_button_unchecked', 'radio_button_unchecked'), ('rate_review', 'rate_review'), ('receipt', 'receipt'), ('recent_actors', 'recent_actors'), ('record_voice_over', 'record_voice_over'), ('redeem', 'redeem'), ('redo', 'redo'), ('refresh', 'refresh'), ('remove', 'remove'), ('remove_circle', 'remove_circle'), ('remove_circle_outline', 'remove_circle_outline'), ('remove_from_queue', 'remove_from_queue'), ('remove_red_eye', 'remove_red_eye'), ('remove_shopping_cart', 'remove_shopping_cart'), ('reorder', 'reorder'), ('repeat', 'repeat'), ('repeat_one', 'repeat_one'), ('replay', 'replay'), ('replay_10', 'replay_10'), ('replay_30', 'replay_30'), ('replay_5', 'replay_5'), ('reply', 'reply'), ('reply_all', 'reply_all'), ('report', 'report'), ('report_problem', 'report_problem'), ('restaurant', 'restaurant'), ('restaurant_menu', 'restaurant_menu'), ('restore', 'restore'), ('restore_page', 'restore_page'), ('ring_volume', 'ring_volume'), ('room', 'room'), ('room_service', 'room_service'), ('rotate_90_degrees_ccw', 'rotate_90_degrees_ccw'), ('rotate_left', 'rotate_left'), ('rotate_right', 'rotate_right'), ('rounded_corner', 'rounded_corner'), ('router', 'router'), ('rowing', 'rowing'), ('rss_feed', 'rss_feed'), ('rv_hookup', 'rv_hookup'), ('satellite', 'satellite'), ('save', 'save'), ('scanner', 'scanner'), ('schedule', 'schedule'), ('school', 'school'), ('screen_lock_landscape', 'screen_lock_landscape'), ('screen_lock_portrait', 'screen_lock_portrait'), ('screen_lock_rotation', 'screen_lock_rotation'), ('screen_rotation', 'screen_rotation'), ('screen_share', 'screen_share'), ('sd_card', 'sd_card'), ('sd_storage', 'sd_storage'), ('search', 'search'), ('security', 'security'), ('select_all', 'select_all'), ('send', 'send'), ('sentiment_dissatisfied', 'sentiment_dissatisfied'), ('sentiment_neutral', 'sentiment_neutral'), ('sentiment_satisfied', 'sentiment_satisfied'), ('sentiment_very_dissatisfied', 'sentiment_very_dissatisfied'), ('sentiment_very_satisfied', 'sentiment_very_satisfied'), ('settings', 'settings'), ('settings_applications', 'settings_applications'), ('settings_backup_restore', 'settings_backup_restore'), ('settings_bluetooth', 'settings_bluetooth'), ('settings_brightness', 'settings_brightness'), ('settings_cell', 'settings_cell'), ('settings_ethernet', 'settings_ethernet'), ('settings_input_antenna', 'settings_input_antenna'), ('settings_input_component', 'settings_input_component'), ('settings_input_composite', 'settings_input_composite'), ('settings_input_hdmi', 'settings_input_hdmi'), ('settings_input_svideo', 'settings_input_svideo'), ('settings_overscan', 'settings_overscan'), ('settings_phone', 'settings_phone'), ('settings_power', 'settings_power'), ('settings_remote', 'settings_remote'), ('settings_system_daydream', 'settings_system_daydream'), ('settings_voice', 'settings_voice'), ('share', 'share'), ('shop', 'shop'), ('shop_two', 'shop_two'), ('shopping_basket', 'shopping_basket'), ('shopping_cart', 'shopping_cart'), ('short_text', 'short_text'), ('show_chart', 'show_chart'), ('shuffle', 'shuffle'), ('signal_cellular_4_bar', 'signal_cellular_4_bar'), ('signal_cellular_connected_no_internet_4_bar', 'signal_cellular_connected_no_internet_4_bar'), ('signal_cellular_no_sim', 'signal_cellular_no_sim'), ('signal_cellular_null', 'signal_cellular_null'), ('signal_cellular_off', 'signal_cellular_off'), ('signal_wifi_4_bar', 'signal_wifi_4_bar'), ('signal_wifi_4_bar_lock', 'signal_wifi_4_bar_lock'), ('signal_wifi_off', 'signal_wifi_off'), ('sim_card', 'sim_card'), ('sim_card_alert', 'sim_card_alert'), ('skip_next', 'skip_next'), ('skip_previous', 'skip_previous'), ('slideshow', 'slideshow'), ('slow_motion_video', 'slow_motion_video'), ('smartphone', 'smartphone'), ('smoke_free', 'smoke_free'), ('smoking_rooms', 'smoking_rooms'), ('sms', 'sms'), ('sms_failed', 'sms_failed'), ('snooze', 'snooze'), ('sort', 'sort'), ('sort_by_alpha', 'sort_by_alpha'), ('spa', 'spa'), ('space_bar', 'space_bar'), ('speaker', 'speaker'), ('speaker_group', 'speaker_group'), ('speaker_notes', 'speaker_notes'), ('speaker_notes_off', 'speaker_notes_off'), ('speaker_phone', 'speaker_phone'), ('spellcheck', 'spellcheck'), ('star', 'star'), ('star_border', 'star_border'), ('star_half', 'star_half'), ('stars', 'stars'), ('stay_current_landscape', 'stay_current_landscape'), ('stay_current_portrait', 'stay_current_portrait'), ('stay_primary_landscape', 'stay_primary_landscape'), ('stay_primary_portrait', 'stay_primary_portrait'), ('stop', 'stop'), ('stop_screen_share', 'stop_screen_share'), ('storage', 'storage'), ('store', 'store'), ('store_mall_directory', 'store_mall_directory'), ('straighten', 'straighten'), ('streetview', 'streetview'), ('strikethrough_s', 'strikethrough_s'), ('style', 'style'), ('subdirectory_arrow_left', 'subdirectory_arrow_left'), ('subdirectory_arrow_right', 'subdirectory_arrow_right'), ('subject', 'subject'), ('subscriptions', 'subscriptions'), ('subtitles', 'subtitles'), ('subway', 'subway'), ('supervisor_account', 'supervisor_account'), ('surround_sound', 'surround_sound'), ('swap_calls', 'swap_calls'), ('swap_horiz', 'swap_horiz'), ('swap_vert', 'swap_vert'), ('swap_vertical_circle', 'swap_vertical_circle'), ('switch_camera', 'switch_camera'), ('switch_video', 'switch_video'), ('sync', 'sync'), ('sync_disabled', 'sync_disabled'), ('sync_problem', 'sync_problem'), ('system_update', 'system_update'), ('system_update_alt', 'system_update_alt'), ('tab', 'tab'), ('tab_unselected', 'tab_unselected'), ('tablet', 'tablet'), ('tablet_android', 'tablet_android'), ('tablet_mac', 'tablet_mac'), ('tag_faces', 'tag_faces'), ('tap_and_play', 'tap_and_play'), ('terrain', 'terrain'), ('text_fields', 'text_fields'), ('text_format', 'text_format'), ('textsms', 'textsms'), ('texture', 'texture'), ('theaters', 'theaters'), ('thumb_down', 'thumb_down'), ('thumb_up', 'thumb_up'), ('thumbs_up_down', 'thumbs_up_down'), ('time_to_leave', 'time_to_leave'), ('timelapse', 'timelapse'), ('timeline', 'timeline'), ('timer', 'timer'), ('timer_10', 'timer_10'), ('timer_3', 'timer_3'), ('timer_off', 'timer_off'), ('title', 'title'), ('toc', 'toc'), ('today', 'today'), ('toll', 'toll'), ('tonality', 'tonality'), ('touch_app', 'touch_app'), ('toys', 'toys'), ('track_changes', 'track_changes'), ('traffic', 'traffic'), ('train', 'train'), ('tram', 'tram'), ('transfer_within_a_station', 'transfer_within_a_station'), ('transform', 'transform'), ('translate', 'translate'), ('trending_down', 'trending_down'), ('trending_flat', 'trending_flat'), ('trending_up', 'trending_up'), ('tune', 'tune'), ('turned_in', 'turned_in'), ('turned_in_not', 'turned_in_not'), ('tv', 'tv'), ('unarchive', 'unarchive'), ('undo', 'undo'), ('unfold_less', 'unfold_less'), ('unfold_more', 'unfold_more'), ('update', 'update'), ('usb', 'usb'), ('verified_user', 'verified_user'), ('vertical_align_bottom', 'vertical_align_bottom'), ('vertical_align_center', 'vertical_align_center'), ('vertical_align_top', 'vertical_align_top'), ('vibration', 'vibration'), ('video_call', 'video_call'), ('video_label', 'video_label'), ('video_library', 'video_library'), ('videocam', 'videocam'), ('videocam_off', 'videocam_off'), ('videogame_asset', 'videogame_asset'), ('view_agenda', 'view_agenda'), ('view_array', 'view_array'), ('view_carousel', 'view_carousel'), ('view_column', 'view_column'), ('view_comfy', 'view_comfy'), ('view_compact', 'view_compact'), ('view_day', 'view_day'), ('view_headline', 'view_headline'), ('view_list', 'view_list'), ('view_module', 'view_module'), ('view_quilt', 'view_quilt'), ('view_stream', 'view_stream'), ('view_week', 'view_week'), ('vignette', 'vignette'), ('visibility', 'visibility'), ('visibility_off', 'visibility_off'), ('voice_chat', 'voice_chat'), ('voicemail', 'voicemail'), ('volume_down', 'volume_down'), ('volume_mute', 'volume_mute'), ('volume_off', 'volume_off'), ('volume_up', 'volume_up'), ('vpn_key', 'vpn_key'), ('vpn_lock', 'vpn_lock'), ('wallpaper', 'wallpaper'), ('warning', 'warning'), ('watch', 'watch'), ('watch_later', 'watch_later'), ('wb_auto', 'wb_auto'), ('wb_cloudy', 'wb_cloudy'), ('wb_incandescent', 'wb_incandescent'), ('wb_iridescent', 'wb_iridescent'), ('wb_sunny', 'wb_sunny'), ('wc', 'wc'), ('web', 'web'), ('web_asset', 'web_asset'), ('weekend', 'weekend'), ('whatshot', 'whatshot'), ('widgets', 'widgets'), ('wifi', 'wifi'), ('wifi_lock', 'wifi_lock'), ('wifi_tethering', 'wifi_tethering'), ('work', 'work'), ('wrap_text', 'wrap_text'), ('youtube_searched_for', 'youtube_searched_for'), ('zoom_in', 'zoom_in'), ('zoom_out', 'zoom_out'), ('zoom_out_map', 'zoom_out_map')], max_length=50, verbose_name='Icon')),
@@ -302,7 +302,7 @@ class Migration(migrations.Migration):
             name='AnnouncementRecipient',
             fields=[
                 ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
-                ('extended_data', django.contrib.postgres.fields.jsonb.JSONField(default=dict, editable=False)),
+                ('extended_data', models.JSONField(default=dict, editable=False)),
                 ('recipient_id', models.PositiveIntegerField()),
                 ('announcement', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='recipients', to='core.Announcement')),
                 ('content_type', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='contenttypes.ContentType')),
@@ -320,7 +320,7 @@ class Migration(migrations.Migration):
             name='Activity',
             fields=[
                 ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
-                ('extended_data', django.contrib.postgres.fields.jsonb.JSONField(default=dict, editable=False)),
+                ('extended_data', models.JSONField(default=dict, editable=False)),
                 ('title', models.CharField(max_length=150, verbose_name='Title')),
                 ('description', models.TextField(max_length=500, verbose_name='Description')),
                 ('app', models.CharField(max_length=100, verbose_name='Application')),
diff --git a/aleksis/core/migrations/0002_school_term.py b/aleksis/core/migrations/0002_school_term.py
index 456f78ebf7e16f32b92b954d36ad39f5d323e866..fba542ed9c4a7be9b60e94d28414ade4189a6d6b 100644
--- a/aleksis/core/migrations/0002_school_term.py
+++ b/aleksis/core/migrations/0002_school_term.py
@@ -17,7 +17,7 @@ class Migration(migrations.Migration):
             name='SchoolTerm',
             fields=[
                 ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
-                ('extended_data', django.contrib.postgres.fields.jsonb.JSONField(default=dict, editable=False)),
+                ('extended_data', models.JSONField(default=dict, editable=False)),
                 ('name', models.CharField(max_length=255, unique=True, verbose_name='Name')),
                 ('date_start', models.DateField(verbose_name='Start date')),
                 ('date_end', models.DateField(verbose_name='End date')),
diff --git a/aleksis/core/migrations/0011_globalpermissions_options.py b/aleksis/core/migrations/0011_globalpermissions_options.py
new file mode 100644
index 0000000000000000000000000000000000000000..a338d910a07843804f8c7e8833e045191d743356
--- /dev/null
+++ b/aleksis/core/migrations/0011_globalpermissions_options.py
@@ -0,0 +1,18 @@
+# Generated by Django 3.1.7 on 2021-04-08 19:15
+
+from django.db import migrations, models
+import django.utils.timezone
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('core', '0010_external_link_widget'),
+    ]
+
+    operations = [
+        migrations.AlterModelOptions(
+            name='globalpermissions',
+            options={'default_permissions': (), 'managed': False, 'permissions': (('view_system_status', 'Can view system status'), ('link_persons_accounts', 'Can link persons to accounts'), ('manage_data', 'Can manage data'), ('impersonate', 'Can impersonate'), ('search', 'Can use search'), ('change_site_preferences', 'Can change site preferences'), ('change_person_preferences', 'Can change person preferences'), ('change_group_preferences', 'Can change group preferences'))},
+        ),
+    ]
diff --git a/aleksis/core/migrations/0012_valid_from_announcement.py b/aleksis/core/migrations/0012_valid_from_announcement.py
new file mode 100644
index 0000000000000000000000000000000000000000..abfc8cdeb0b9dece8f23389c3d3158d63bca13c2
--- /dev/null
+++ b/aleksis/core/migrations/0012_valid_from_announcement.py
@@ -0,0 +1,19 @@
+# Generated by Django 3.1.7 on 2021-04-08 19:15
+
+from django.db import migrations, models
+import django.utils.timezone
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('core', '0011_globalpermissions_options'),
+    ]
+
+    operations = [
+        migrations.AlterField(
+            model_name='announcement',
+            name='valid_from',
+            field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='Date and time from when to show'),
+        ),
+    ]
diff --git a/aleksis/core/migrations/0011_oauth_permissions.py b/aleksis/core/migrations/0013_oauth_permissions.py
similarity index 95%
rename from aleksis/core/migrations/0011_oauth_permissions.py
rename to aleksis/core/migrations/0013_oauth_permissions.py
index 207776bc679436c6e5d64c6646343acd88234541..67193ebcd6aba64b675226c07de2c980be733fb2 100644
--- a/aleksis/core/migrations/0011_oauth_permissions.py
+++ b/aleksis/core/migrations/0013_oauth_permissions.py
@@ -7,7 +7,7 @@ import django.utils.timezone
 class Migration(migrations.Migration):
 
     dependencies = [
-        ('core', '0010_external_link_widget'),
+        ('core', '0012_valid_from_announcement'),
     ]
 
     operations = [
diff --git a/aleksis/core/migrations/0013_pdf_file.py b/aleksis/core/migrations/0013_pdf_file.py
new file mode 100644
index 0000000000000000000000000000000000000000..63b8a95607ce306f8d14d8b4266e184e12a26838
--- /dev/null
+++ b/aleksis/core/migrations/0013_pdf_file.py
@@ -0,0 +1,46 @@
+# Generated by Django 3.2 on 2021-04-10 18:58
+
+import aleksis.core.models
+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', '0012_valid_from_announcement'),
+    ]
+
+    operations = [
+        migrations.AlterModelOptions(
+            name='globalpermissions',
+            options={'default_permissions': (), 'managed': False, 'permissions': (
+            ('view_system_status', 'Can view system status'), ('link_persons_accounts', 'Can link persons to accounts'),
+            ('manage_data', 'Can manage data'), ('impersonate', 'Can impersonate'), ('search', 'Can use search'),
+            ('change_site_preferences', 'Can change site preferences'),
+            ('change_person_preferences', 'Can change person preferences'),
+            ('change_group_preferences', 'Can change group preferences'), ('test_pdf', 'Can test PDF generation'))},
+        ),
+        migrations.CreateModel(
+            name='PDFFile',
+            fields=[
+                ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+                ('extended_data', models.JSONField(default=dict, editable=False)),
+                ('expires_at', models.DateTimeField(default=aleksis.core.models.PDFFile._get_default_expiration, verbose_name='File expires at')),
+                ('html', models.TextField(verbose_name='Rendered HTML')),
+                ('file', models.FileField(blank=True, null=True, upload_to='pdfs/', verbose_name='Generated PDF file')),
+                ('person', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='pdf_files', to='core.person', verbose_name='Owner')),
+                ('site', models.ForeignKey(default=1, editable=False, on_delete=django.db.models.deletion.CASCADE, to='sites.site')),
+            ],
+            options={
+                'verbose_name': 'PDF file',
+                'verbose_name_plural': 'PDF files',
+            },
+            managers=[
+                ('objects', django.contrib.sites.managers.CurrentSiteManager()),
+            ],
+        ),
+    ]
diff --git a/aleksis/core/migrations/0014_alter_pdffile_file.py b/aleksis/core/migrations/0014_alter_pdffile_file.py
new file mode 100644
index 0000000000000000000000000000000000000000..2668488942f93db37703cccc79b9a43b6403904b
--- /dev/null
+++ b/aleksis/core/migrations/0014_alter_pdffile_file.py
@@ -0,0 +1,19 @@
+# Generated by Django 3.2 on 2021-04-17 18:47
+
+import aleksis.core.models
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('core', '0013_pdf_file'),
+    ]
+
+    operations = [
+        migrations.AlterField(
+            model_name='pdffile',
+            name='file',
+            field=models.FileField(blank=True, null=True, upload_to=aleksis.core.models.PDFFile._get_upload_path, verbose_name='Generated PDF file'),
+        ),
+    ]
diff --git a/aleksis/core/models.py b/aleksis/core/models.py
index 78674942228bf23066e606e2a1a94fb59df6d74d..473b4e9447eea70de1ccb3d269d62a895f0d596b 100644
--- a/aleksis/core/models.py
+++ b/aleksis/core/models.py
@@ -1,7 +1,8 @@
 # flake8: noqa: DJ01
-
-from datetime import date, datetime
+import hmac
+from datetime import date, datetime, timedelta
 from typing import Iterable, List, Optional, Sequence, Union
+from urllib.parse import urlparse
 
 from django.conf import settings
 from django.contrib.auth import get_user_model
@@ -909,6 +910,7 @@ class GlobalPermissions(GlobalPermissionModel):
             ("view_oauth_applications", _("Can view oauth applications")),
             ("update_oauth_applications", _("Can update oauth applications")),
             ("delete_oauth_applications", _("Can delete oauth applications")),
+            ("test_pdf", _("Can test PDF generation")),
         )
 
 
@@ -972,3 +974,49 @@ class DataCheckResult(ExtensibleModel):
             ("run_data_checks", _("Can run data checks")),
             ("solve_data_problem", _("Can solve data check problems")),
         )
+
+
+class PDFFile(ExtensibleModel):
+    """Link to a rendered PDF file."""
+
+    def _get_default_expiration():  # noqa
+        return timezone.now() + timedelta(minutes=get_site_preferences()["general__pdf_expiration"])
+
+    def _get_upload_path(instance, filename):  # noqa
+        return f"pdfs/{instance.secret}.pdf"
+
+    person = models.ForeignKey(
+        to=Person, on_delete=models.CASCADE, verbose_name=_("Owner"), related_name="pdf_files"
+    )
+    expires_at = models.DateTimeField(
+        verbose_name=_("File expires at"), default=_get_default_expiration
+    )
+    html = models.TextField(verbose_name=_("Rendered HTML"))
+    file = models.FileField(
+        upload_to=_get_upload_path, blank=True, null=True, verbose_name=_("Generated PDF file")
+    )
+
+    def __str__(self):
+        return f"{self.person} ({self.pk})"
+
+    @property
+    def secret(self) -> str:
+        """Get secret needed for accessing the HTML page."""
+        return hmac.new(
+            bytes(settings.SECRET_KEY, "utf-8"),
+            msg=bytes(self.html + str(self.expires_at), "utf-8"),
+            digestmod="sha256",
+        ).hexdigest()
+
+    @property
+    def html_url(self) -> str:
+        """Get URL for the HTML page."""
+        return (
+            urlparse(reverse("html_for_pdf_file", args=[self.pk]))
+            ._replace(query=f"secret={self.secret}")
+            .geturl()
+        )
+
+    class Meta:
+        verbose_name = _("PDF file")
+        verbose_name_plural = _("PDF files")
diff --git a/aleksis/core/preferences.py b/aleksis/core/preferences.py
index 60574f3a8863a4107f04dae66f0c1361b5a5f3c1..c4cac9ee8a26e80b1bd3834ed5957a22ffd37041 100644
--- a/aleksis/core/preferences.py
+++ b/aleksis/core/preferences.py
@@ -9,6 +9,7 @@ from dynamic_preferences.types import (
     BooleanPreference,
     ChoicePreference,
     FilePreference,
+    IntegerPreference,
     ModelMultipleChoicePreference,
     MultipleChoicePreference,
     StringPreference,
@@ -278,3 +279,52 @@ class DashboardEditing(BooleanPreference):
     default = True
     required = False
     verbose_name = _("Allow users to edit their dashboard")
+
+
+@site_preferences_registry.register
+class EditableFieldsPerson(MultipleChoicePreference):
+    """Fields on person model that should be editable by the person."""
+
+    section = account
+    name = "editable_fields_person"
+    default = []
+    widget = SelectMultiple
+    verbose_name = _("Fields on person model which are editable by themselves.")
+    field_attribute = {"initial": []}
+    choices = [(field.name, field.name) for field in Person.syncable_fields()]
+
+
+@site_preferences_registry.register
+class SendNotificationOnPersonChange(MultipleChoicePreference):
+    """Fields on the person model that should trigger a notification on change."""
+
+    section = account
+    name = "notification_on_person_change"
+    default = []
+    widget = SelectMultiple
+    verbose_name = _(
+        "Editable fields on person model which should trigger a notification on change"
+    )
+    field_attribute = {"initial": []}
+    choices = [(field.name, field.name) for field in Person.syncable_fields()]
+
+
+@site_preferences_registry.register
+class PersonChangeNotificationContact(StringPreference):
+    """Mail recipient address for change notifications."""
+
+    section = account
+    name = "person_change_notification_contact"
+    default = ""
+    verbose_name = _("Contact for notification if a person changes their data")
+
+
+@site_preferences_registry.register
+class PDFFileExpirationDuration(IntegerPreference):
+    """PDF file expiration duration."""
+
+    section = general
+    name = "pdf_expiration"
+    default = 3
+    verbose_name = _("PDF file expiration duration")
+    help_text = _("in minutes")
diff --git a/aleksis/core/rules.py b/aleksis/core/rules.py
index c198c10a784dfa4299029f2e4b85ebba215446f7..5fd398a4b0824969db525998365a4bca03187705 100644
--- a/aleksis/core/rules.py
+++ b/aleksis/core/rules.py
@@ -2,6 +2,7 @@ import rules
 
 from .models import AdditionalField, Announcement, Group, GroupType, Person
 from .util.predicates import (
+    contains_site_preference_value,
     has_any_object,
     has_global_perm,
     has_object_perm,
@@ -67,7 +68,9 @@ rules.add_perm("core.view_person_groups", view_groups_predicate)
 
 # Edit person
 edit_person_predicate = has_person & (
-    has_global_perm("core.change_person") | has_object_perm("core.change_person")
+    has_global_perm("core.change_person")
+    | has_object_perm("core.change_person")
+    | is_current_person & is_site_preference_set("account", "editable_fields_person")
 )
 rules.add_perm("core.edit_person", edit_person_predicate)
 
@@ -357,3 +360,18 @@ rules.add_perm("core.delete_oauth_applications", delete_oauth_applications_predi
 # Upload and browse files via CKEditor
 upload_files_ckeditor_predicate = has_person & has_global_perm("core.upload_files_ckeditor")
 rules.add_perm("core.upload_files_ckeditor", upload_files_ckeditor_predicate)
+
+test_pdf_generation_predicate = has_person & has_global_perm("core.test_pdf")
+rules.add_perm("core.test_pdf", test_pdf_generation_predicate)
+
+# Generate rules for syncable fields
+for field in Person._meta.fields:
+    perm = (
+        has_global_perm("core.edit_person")
+        | has_object_perm("core.edit_person")
+        | (
+            is_current_person
+            & contains_site_preference_value("account", "editable_fields_person", field.name)
+        )
+    )
+    rules.add_perm(f"core.change_person_field_{field.name}", perm)
diff --git a/aleksis/core/settings.py b/aleksis/core/settings.py
index f5b2cd8d0b35f0006eba3ec3e685271748e08300..302ae8c9c57e19b02d6bc28c7d7cbb8956f66926 100644
--- a/aleksis/core/settings.py
+++ b/aleksis/core/settings.py
@@ -87,7 +87,6 @@ INSTALLED_APPS = [
     "rules.apps.AutodiscoverRulesConfig",
     "haystack",
     "polymorphic",
-    "django_global_request",
     "dbbackup",
     "django_celery_beat",
     "django_celery_results",
@@ -156,7 +155,6 @@ MIDDLEWARE = [
     "debug_toolbar.middleware.DebugToolbarMiddleware",
     "django.middleware.locale.LocaleMiddleware",
     "django.middleware.http.ConditionalGetMiddleware",
-    "django_global_request.middleware.GlobalRequestMiddleware",
     "django.contrib.sites.middleware.CurrentSiteMiddleware",
     "django.middleware.common.CommonMiddleware",
     "django.middleware.csrf.CsrfViewMiddleware",
@@ -246,7 +244,7 @@ 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")
+SILENCED_SYSTEM_CHECKS += ["cachalot.W001", "cachalot.E003"]
 
 SESSION_ENGINE = "django.contrib.sessions.backends.cache"
 SESSION_CACHE_ALIAS = "default"
@@ -314,7 +312,7 @@ if _settings.get("ldap.uri", None):
         AUTH_LDAP_BIND_PASSWORD = _settings.get("ldap.bind.password")
 
     # Keep local password for users to be required to proveide their old password on change
-    AUTH_LDAP_SET_USABLE_PASSWORD = True
+    AUTH_LDAP_SET_USABLE_PASSWORD = _settings.get("ldap.handle_passwords", True)
 
     # Keep bound as the authenticating user
     # Ensures proper read permissions, and ability to change password without admin
@@ -527,6 +525,13 @@ DBBACKUP_CONNECTOR_MAPPING = {
     "django_prometheus.db.backends.postgresql": "dbbackup.db.postgresql.PgDumpConnector",
 }
 
+if _settings.get("backup.storage.type", "").lower() == "s3":
+    DBBACKUP_STORAGE = "storages.backends.s3boto3.S3Boto3Storage"
+
+    DBBACKUP_STORAGE_OPTIONS = {
+        key: value for (key, value) in _settings.get("backup.storage.s3").items()
+    }
+
 IMPERSONATE = {"USE_HTTP_REFERER": True, "REQUIRE_SUPERUSER": True, "ALLOW_SUPERUSER": True}
 
 DJANGO_TABLES2_TEMPLATE = "django_tables2/materialize.html"
@@ -810,7 +815,7 @@ PROMETHEUS_EXPORT_MIGRATIONS = False
 
 SECURE_PROXY_SSL_HEADER = ("REQUEST_SCHEME", "https")
 
-if _settings.get("storage.s3.enabled", False):
+if _settings.get("storage.type", "").lower() == "s3":
     INSTALLED_APPS.append("storages")
 
     DEFAULT_FILE_STORAGE = "storages.backends.s3boto3.S3Boto3Storage"
@@ -822,11 +827,12 @@ if _settings.get("storage.s3.enabled", False):
             "storage.s3.static.max_age_seconds", 24 * 60 * 60
         )
 
-    AWS_REGION = _settings.get("storage.s3.region", "")
-    AWS_ACCESS_KEY_ID = _settings.get("storage.s3.access_key_id", "")
+    AWS_REGION = _settings.get("storage.s3.region_name", "")
+    AWS_ACCESS_KEY_ID = _settings.get("storage.s3.access_key", "")
     AWS_SECRET_ACCESS_KEY = _settings.get("storage.s3.secret_key", "")
     AWS_SESSION_TOKEN = _settings.get("storage.s3.session_token", "")
     AWS_STORAGE_BUCKET_NAME = _settings.get("storage.s3.bucket_name", "")
+    AWS_LOCATION = _settings.get("storage.s3.location", "")
     AWS_S3_ADDRESSING_STYLE = _settings.get("storage.s3.addressing_style", "auto")
     AWS_S3_ENDPOINT_URL = _settings.get("storage.s3.endpoint_url", "")
     AWS_S3_KEY_PREFIX = _settings.get("storage.s3.key_prefix", "")
@@ -846,3 +852,6 @@ else:
     DEFAULT_FILE_STORAGE = "django.core.files.storage.FileSystemStorage"
 
 SASS_PROCESSOR_STORAGE = DEFAULT_FILE_STORAGE
+
+# Add django-cleanup after all apps to ensure that it gets all signals as last app
+INSTALLED_APPS.append("django_cleanup.apps.CleanupConfig")
diff --git a/aleksis/core/static/js/progress.js b/aleksis/core/static/js/progress.js
index 4d4c0692f39e78af60bdc621544ba7a2c2b720c4..8b897096f18c221bde8fd34c88bd93f9800ff06d 100644
--- a/aleksis/core/static/js/progress.js
+++ b/aleksis/core/static/js/progress.js
@@ -47,6 +47,10 @@ function customSuccess(progressBarElement, progressBarMessageElement) {
     $("#result-icon").text("check_circle");
     $("#result-text").text(OPTIONS.success);
     $("#result-box").show();
+    const redirect = "redirect_on_success" in OPTIONS;
+    if (redirect) {
+        window.location.replace(OPTIONS.redirect_on_success);
+    }
 }
 
 function customError(progressBarElement, progressBarMessageElement) {
diff --git a/aleksis/core/static/print.css b/aleksis/core/static/print.css
index cd3eeeea0accb97cb9e2b48054b8ad033476b41c..cda82eacab1d51a39f7f455170eb6ff12121dc30 100644
--- a/aleksis/core/static/print.css
+++ b/aleksis/core/static/print.css
@@ -5,6 +5,7 @@
 @page {
     size: A4;
     padding: 30mm;
+    margin: 0;
 }
 
 header {
diff --git a/aleksis/core/templates/core/base.html b/aleksis/core/templates/core/base.html
index d34db4f65d05021f1d68e61190bf323c249e5ada..1553f5511439dc551f4f29056c23568874bf8f07 100644
--- a/aleksis/core/templates/core/base.html
+++ b/aleksis/core/templates/core/base.html
@@ -47,9 +47,9 @@
 
   <!-- Nav bar (logged in as, logout) -->
   <nav>
-    <a class="brand-logo" href="/">{{ request.site.preferences.general__title }}</a>
-
     <div class="nav-wrapper">
+      <a class="brand-logo" href="/">{{ request.site.preferences.general__title }}</a>
+
       <ul id="nav-mobile" class="right hide-on-med-and-down">
         {% if user.is_authenticated %}
           <li>{% trans "Logged in as" %} {{ user.get_username }}</li>
diff --git a/aleksis/core/templates/core/index.html b/aleksis/core/templates/core/index.html
index 70103f517963f3a9e306e43c9212ac856594b229..ca838aa237a97b9d1773e17c984f734cbcdb92f1 100644
--- a/aleksis/core/templates/core/index.html
+++ b/aleksis/core/templates/core/index.html
@@ -16,9 +16,6 @@
       {% trans "Edit dashboard" %}
     </a>
   {% endif %}
-  <h4>
-    {{ request.site.preferences.general__title }}
-  </h4>
 
   {% for notification in unread_notifications %}
     <div class="alert primary scale-transition">
diff --git a/aleksis/core/templates/core/pages/progress.html b/aleksis/core/templates/core/pages/progress.html
index 6292fb0d243b1b2ecb72ff1cec7c3b6339409c18..799f1f93648ab1de05be3f9432653b8c4e8b1466 100644
--- a/aleksis/core/templates/core/pages/progress.html
+++ b/aleksis/core/templates/core/pages/progress.html
@@ -46,6 +46,12 @@
           <i class="material-icons left">arrow_back</i>
           {% trans "Go back" %}
         </a>
+        {% if additional_button %}
+          <a class="btn waves-effect waves-light" href="{{ additional_button.href }}">
+            <i class="material-icons left">{{ additional_button.icon|default:"" }}</i>
+            {{ additional_button.caption }}
+          </a>
+        {% endif %}
       </div>
     </div>
   </div>
diff --git a/aleksis/core/templates/core/pages/test_pdf.html b/aleksis/core/templates/core/pages/test_pdf.html
new file mode 100644
index 0000000000000000000000000000000000000000..f0c0a6169c85a6f37252136b3777dc0967350e43
--- /dev/null
+++ b/aleksis/core/templates/core/pages/test_pdf.html
@@ -0,0 +1,19 @@
+{# -*- engine:django -*- #}
+
+{% extends "core/base_print.html" %}
+
+{% load i18n %}
+
+{% block browser_title %}{% blocktrans %}Test PDF generation{% endblocktrans %}{% endblock %}
+{% block page_title %}{% blocktrans %}Test PDF generation{% endblocktrans %}{% endblock %}
+
+{% block content %}
+  <div class="alert primary">
+    <p>
+      <i class="material-icons left">info</i>
+      {% blocktrans %}
+        This simple view can be used to ensure the correct function of the built-in PDF generation system.
+      {% endblocktrans %}
+    </p>
+  </div>
+{% endblock %}
diff --git a/aleksis/core/templates/oauth2_provider/application_form.html b/aleksis/core/templates/oauth2_provider/application_form.html
index 51f4b5edcff15113787859c3d0c14b434aa8456c..520fa107449dfef6999a3708f0e1f1bbe0090404 100644
--- a/aleksis/core/templates/oauth2_provider/application_form.html
+++ b/aleksis/core/templates/oauth2_provider/application_form.html
@@ -13,9 +13,9 @@
   <form method="post">
     {% csrf_token %}
     {% form form=form %}{% endform %}
+    {% include "core/partials/save_button.html" %}
     <a class="btn waves-effect red waves-light" href="{% block app-form-back-url %}{% url "oauth_detail" application.id %}{% endblock app-form-back-url %}">
       <i class="material-icons left">clear</i> {% trans "Cancel"%}
     </a>
-    {% include "core/partials/save_button.html" %}
   </form>
 {% endblock %}
diff --git a/aleksis/core/templates/oauth2_provider/authorize.html b/aleksis/core/templates/oauth2_provider/authorize.html
index 36423b7ffb074013c4f85ea94e3fe3c3a1ed42cf..f80659cbb39de3375f50e0bc7d342f086999c290 100644
--- a/aleksis/core/templates/oauth2_provider/authorize.html
+++ b/aleksis/core/templates/oauth2_provider/authorize.html
@@ -28,12 +28,12 @@
             {% csrf_token %}
             {% form form=form %}
             {% endform %}
-            <a class="btn red waves-effect waves-light btn-margin" href="{% block app-form-back-url %}{% url "oauth_detail" application.id %}{% endblock app-form-back-url %}">
-              <i class="material-icons left">cancel</i> {% trans "Disallow" %}
-            </a>
             <button type="submit" class="btn green waves-effect waves-light btn-margin">
               <i class="material-icons left">done_all</i> {% trans "Allow" %}
             </button>
+            <a class="btn red waves-effect waves-light btn-margin" href="{% block app-form-back-url %}{% url "oauth_detail" application.id %}{% endblock app-form-back-url %}">
+              <i class="material-icons left">cancel</i> {% trans "Disallow" %}
+            </a>
           </form>
         </div>
       </div>
diff --git a/aleksis/core/templates/templated_email/person_changed.email b/aleksis/core/templates/templated_email/person_changed.email
new file mode 100644
index 0000000000000000000000000000000000000000..2e1db653256873d3f72afcd34a1e19914d116480
--- /dev/null
+++ b/aleksis/core/templates/templated_email/person_changed.email
@@ -0,0 +1,32 @@
+{% load i18n %}
+
+{% block subject %}
+ {% blocktrans with person=person %}{{ person }} changed their data!{% endblocktrans %}
+{% endblock %}
+
+{% block plain %}
+ {% trans "Hello," %}
+
+ {% blocktrans with person=person %}
+   the person {{ person }} recently changed the following fields:
+ {% endblocktrans %}
+
+ {% for field in send_notification_fields %}
+  * {{ field }}
+ {% endfor %}
+{% endblock %}
+
+{% block html %}
+ <p>{% trans "Hello," %}</p>
+ <p>
+  {% blocktrans with person=person %}
+    the person {{ person }} recently changed the following fields:
+  {% endblocktrans %}
+ </p>
+
+ <ul>
+  {% for field in send_notification_fields %}
+   <li>{{ field }}</li>
+  {% endfor %}
+ </ul>
+{% endblock %}
diff --git a/aleksis/core/tests/browser/test_selenium.py b/aleksis/core/tests/browser/test_selenium.py
index a4861d6c18797f7d3e6ed37fa221a2ea1753b96b..8afd4b1584e3b5e6f35d69b2df73625ad8da5fad 100644
--- a/aleksis/core/tests/browser/test_selenium.py
+++ b/aleksis/core/tests/browser/test_selenium.py
@@ -1,10 +1,15 @@
 import os
 
 from django.conf import settings
+from django.contrib.auth.models import User
+from django.test import override_settings
 from django.test.selenium import SeleniumTestCase, SeleniumTestCaseBase
 from django.urls import reverse
 
 import pytest
+from selenium.webdriver.support.wait import WebDriverWait
+
+from aleksis.core.models import Person
 
 pytestmark = pytest.mark.django_db
 
@@ -15,6 +20,8 @@ SeleniumTestCaseBase.browsers = list(
 SeleniumTestCaseBase.selenium_hub = os.environ.get("TEST_SELENIUM_HUB", "") or None
 
 
+@pytest.mark.usefixtures("celery_worker")
+@override_settings(CELERY_BROKER_URL="memory://localhost//")
 class SeleniumTests(SeleniumTestCase):
     serialized_rollback = True
 
@@ -29,18 +36,11 @@ class SeleniumTests(SeleniumTestCase):
         else:
             return False
 
-    def test_index(self):
-        self.selenium.get(self.live_server_url + "/")
-        assert "AlekSIS" in self.selenium.title
-        self._screenshot("index.png")
-
-    def test_login_default_superuser(self):
-        username = "admin"
-        password = "admin"
-
+    def _login(self, username="admin", password="admin", with_screenshots=False):
         # Navigate to configured login page
         self.selenium.get(self.live_server_url + reverse(settings.LOGIN_URL))
-        self._screenshot("login_default_superuser_blank.png")
+        if with_screenshots:
+            self._screenshot("login_default_superuser_blank.png")
 
         # Find login form input fields and enter defined credentials
         self.selenium.find_element_by_xpath(
@@ -49,11 +49,33 @@ class SeleniumTests(SeleniumTestCase):
         self.selenium.find_element_by_xpath(
             '//label[contains(text(), "Password")]/../input'
         ).send_keys(password)
-        self._screenshot("login_default_superuser_filled.png")
+        if with_screenshots:
+            self._screenshot("login_default_superuser_filled.png")
 
         # Submit form by clicking django-two-factor-auth's Next button
         self.selenium.find_element_by_xpath('//button[contains(text(), "Login")]').click()
-        self._screenshot("login_default_superuser_submitted.png")
+        if with_screenshots:
+            self._screenshot("login_default_superuser_submitted.png")
+
+    def _create_person(self, username="admin"):
+        user = User.objects.get(username=username)
+        person = Person.objects.create(user=user, first_name="Jane", last_name="Doe")
+        return person
+
+    def test_index(self):
+        self.selenium.get(self.live_server_url + "/")
+        assert "AlekSIS" in self.selenium.title
+        self._screenshot("index.png")
+
+    def test_login_default_superuser(self):
+        self._login("admin", "admin", with_screenshots=True)
 
         # Should redirect away from login page and not put up an alert about wrong credentials
         assert "Please enter a correct username and password." not in self.selenium.page_source
+
+    def test_pdf_generation(self):
+        self._login()
+        self._create_person()
+        self.selenium.get(self.live_server_url + reverse("test_pdf"))
+        el = WebDriverWait(self.selenium, 10).until(lambda d: ".pdf" in self.selenium.current_url)
+        self._screenshot("pdf.png")
diff --git a/aleksis/core/tests/models/test.pdf b/aleksis/core/tests/models/test.pdf
new file mode 100644
index 0000000000000000000000000000000000000000..4e6c1457f663de82eb95a8613f4626b84146dd45
Binary files /dev/null and b/aleksis/core/tests/models/test.pdf differ
diff --git a/aleksis/core/tests/models/test_openid.py b/aleksis/core/tests/models/test_openid.py
index a253a91de862e6aedd3c1bf1dd2d9ea065df29b4..eee068a36af45131bab94e7e58424aa9957ca88a 100644
--- a/aleksis/core/tests/models/test_openid.py
+++ b/aleksis/core/tests/models/test_openid.py
@@ -7,10 +7,10 @@ pytestmark = pytest.mark.django_db
 
 def test_application_create():
     _application = Application.objects.create(
-        name = "Test Application",
-        redirect_uris = "https://example.com/redirect https://example.de/redirect",
-        client_type = "public",
-        authorization_grant_type = "authorization-code"
+        name="Test Application",
+        redirect_uris="https://example.com/redirect https://example.de/redirect",
+        client_type="public",
+        authorization_grant_type="authorization-code",
     )
 
     assert _application.name == "Test Application"
diff --git a/aleksis/core/tests/models/test_pdffile.py b/aleksis/core/tests/models/test_pdffile.py
new file mode 100644
index 0000000000000000000000000000000000000000..778ff8b5676a93279f9f9261e9c27ccbe6d85939
--- /dev/null
+++ b/aleksis/core/tests/models/test_pdffile.py
@@ -0,0 +1,102 @@
+import os
+import re
+from datetime import datetime, timedelta
+
+from django.core.files import File
+from django.core.files.storage import default_storage
+from django.template.loader import render_to_string
+from django.test import TransactionTestCase, override_settings
+from django.utils import timezone
+
+import freezegun
+import pytest
+
+from aleksis.core.models import PDFFile, Person
+from aleksis.core.util.pdf import clean_up_expired_pdf_files
+
+pytestmark = pytest.mark.django_db
+
+
+@pytest.mark.usefixtures("celery_worker")
+@override_settings(CELERY_BROKER_URL="memory://localhost//")
+class PDFFIleTest(TransactionTestCase):
+    serialized_rollback = True
+
+    _test_pdf = os.path.join(os.path.dirname(os.path.abspath(__file__)), "test.pdf")
+
+    def _get_test_html(self):
+        return render_to_string("core/pages/test_pdf.html")
+
+    def test_pdf_file(self):
+        dummy_person = Person.objects.create(first_name="Jane", last_name="Doe")
+
+        html = self._get_test_html()
+        assert "html" in html
+        file_object = PDFFile.objects.create(person=dummy_person, html=html)
+        assert isinstance(file_object.expires_at, datetime)
+        assert file_object.expires_at > timezone.now()
+        assert not bool(file_object.file)
+
+        with open(self._test_pdf, "rb") as f:
+            file_object.file.save("print.pdf", File(f))
+        file_object.save()
+        re_base = r"pdfs/[a-zA-Z0-9]+\.pdf"
+        assert re.match(re_base, file_object.file.name)
+
+    def test_delete_signal(self):
+        dummy_person = Person.objects.create(first_name="Jane", last_name="Doe")
+        file_object = PDFFile.objects.create(person=dummy_person, html=self._get_test_html())
+        with open(self._test_pdf, "rb") as f:
+            file_object.file.save("print.pdf", File(f))
+        file_object.save()
+
+        file_path = file_object.file.path
+
+        assert default_storage.exists(file_path)
+        file_object.delete()
+        assert not default_storage.exists(file_path)
+
+    def test_delete_expired_files(self):
+        # Create test instances
+        dummy_person = Person.objects.create(first_name="Jane", last_name="Doe")
+        file_object = PDFFile.objects.create(person=dummy_person, html=self._get_test_html())
+        file_object2 = PDFFile.objects.create(
+            person=dummy_person,
+            html=self._get_test_html(),
+            expires_at=timezone.now() + timedelta(minutes=10),
+        )
+        with open(self._test_pdf, "rb") as f:
+            file_object.file.save("print.pdf", File(f))
+            file_object2.file.save("print.pdf", File(f))
+        file_object.save()
+        file_object2.save()
+
+        clean_up_expired_pdf_files()
+        assert PDFFile.objects.get(pk=file_object.pk)
+        assert PDFFile.objects.get(pk=file_object2.pk)
+
+        # Prepare times
+        test_time_before = timezone.now() + timedelta(minutes=2.5)
+        test_time_between = timezone.now() + timedelta(minutes=4)
+        test_time_after = timezone.now() + timedelta(minutes=15)
+
+        # None of the files are expired
+        with freezegun.freeze_time(test_time_before):
+            clean_up_expired_pdf_files()
+            assert PDFFile.objects.get(pk=file_object.pk)
+            assert PDFFile.objects.get(pk=file_object2.pk)
+
+        # One file is expired
+        with freezegun.freeze_time(test_time_between):
+            clean_up_expired_pdf_files()
+            with pytest.raises(PDFFile.DoesNotExist):
+                PDFFile.objects.get(pk=file_object.pk)
+            assert PDFFile.objects.get(pk=file_object2.pk)
+
+        # Both files are expired
+        with freezegun.freeze_time(test_time_after):
+            clean_up_expired_pdf_files()
+            with pytest.raises(PDFFile.DoesNotExist):
+                PDFFile.objects.get(pk=file_object.pk)
+            with pytest.raises(PDFFile.DoesNotExist):
+                PDFFile.objects.get(pk=file_object2.pk)
diff --git a/aleksis/core/tests/util/test_core_helpers.py b/aleksis/core/tests/util/test_core_helpers.py
deleted file mode 100644
index d018ddba3b610251aafcf47698f7669c49ef8fef..0000000000000000000000000000000000000000
--- a/aleksis/core/tests/util/test_core_helpers.py
+++ /dev/null
@@ -1,19 +0,0 @@
-import re
-
-from aleksis.core.util.core_helpers import path_and_rename
-
-
-def test_path_and_rename():
-    re_base = r"[a-z0-9]+"
-
-    example_1 = "sdjaasjkl.jpg"
-    re_example_1 = "files/" + re_base + r"\.jpg"
-    re2_example_1 = "images/" + re_base + r"\.jpg"
-    assert re.match(re_example_1, path_and_rename(None, example_1))
-    assert re.match(re2_example_1, path_and_rename(None, example_1, upload_to="images"))
-
-    example_2 = "sdjaasjkl"
-    re_example_2 = "files/" + re_base
-    re2_example_2 = "images/" + re_base
-    assert re.match(re_example_2, path_and_rename(None, example_2))
-    assert re.match(re2_example_2, path_and_rename(None, example_2, upload_to="images"))
diff --git a/aleksis/core/urls.py b/aleksis/core/urls.py
index f0b355237ea1ff51d522a4452210227f40e688b5..a1a8e2d62790e64895f2f6e7183862ee2c4c9599 100644
--- a/aleksis/core/urls.py
+++ b/aleksis/core/urls.py
@@ -178,6 +178,7 @@ urlpatterns = [
         name="preferences_group",
     ),
     path("health/", include(health_urls)),
+    path("health/pdf/", views.TestPDFGenerationView.as_view(), name="test_pdf"),
     path("data_check/", views.DataCheckView.as_view(), name="check_data",),
     path("data_check/run/", views.RunDataChecks.as_view(), name="data_check_run",),
     path(
@@ -207,6 +208,8 @@ urlpatterns = [
         {"default": True},
         name="edit_default_dashboard",
     ),
+    path("pdfs/<int:pk>/", views.RedirectToPDFFile.as_view(), name="redirect_to_pdf_file"),
+    path("pdfs/<int:pk>/html/", views.HTMLForPDFFile.as_view(), name="html_for_pdf_file"),
 ]
 
 # Add URLs for optional features
diff --git a/aleksis/core/util/apps.py b/aleksis/core/util/apps.py
index d7539c22616abd2d76013546acb15b592862ceb4..a2a6ced0e87b610dde1d74efe57b0fdf46f969c9 100644
--- a/aleksis/core/util/apps.py
+++ b/aleksis/core/util/apps.py
@@ -186,8 +186,6 @@ class AppConfig(django.apps.AppConfig):
         verbosity: int,
         interactive: bool,
         using: str,
-        plan: List[Tuple],
-        apps: django.apps.registry.Apps,
         **kwargs,
     ) -> None:
         """Call on every app instance after its models have been migrated.
diff --git a/aleksis/core/util/core_helpers.py b/aleksis/core/util/core_helpers.py
index 676894d28d62f32ee9c08de874edf6857f94a629..69350b13967b0cf1d57a6fc6307adcbc90725fe6 100644
--- a/aleksis/core/util/core_helpers.py
+++ b/aleksis/core/util/core_helpers.py
@@ -1,11 +1,9 @@
-import os
 import sys
 from datetime import datetime, timedelta
 from importlib import import_module
 from itertools import groupby
 from operator import itemgetter
 from typing import Any, Callable, Optional, Sequence, Union
-from uuid import uuid4
 
 if sys.version_info >= (3, 9):
     from importlib import metadata
@@ -183,20 +181,6 @@ def has_person(obj: Union[HttpRequest, Model]) -> bool:
         return True
 
 
-def path_and_rename(instance, filename: str, upload_to: str = "files") -> str:
-    """Update path of an uploaded file and renames it to a random UUID in Django FileField."""
-    _, ext = os.path.splitext(filename)
-
-    # set filename as random string
-    new_filename = f"{uuid4().hex}{ext}"
-
-    # Create upload directory if necessary
-    os.makedirs(os.path.join(settings.MEDIA_ROOT, upload_to), exist_ok=True)
-
-    # return the whole path to the file
-    return os.path.join(upload_to, new_filename)
-
-
 def custom_information_processor(request: HttpRequest) -> dict:
     """Provide custom information in all templates."""
     from ..models import CustomMenu
diff --git a/aleksis/core/util/ldap.py b/aleksis/core/util/ldap.py
index 5a17cbbb5aea91030fab467c21897826f74eb6aa..96b058ac0060975cdd08281be94ba4df371aa7db 100644
--- a/aleksis/core/util/ldap.py
+++ b/aleksis/core/util/ldap.py
@@ -20,13 +20,13 @@ class LDAPBackend(_LDAPBackend):
         Django database in order to not require it to have global admin permissions
         on the LDAP directory.
         """
-        user = ldap_user.authenticate(password)
-
-        if not user:
-            # Fail early and do not try other backends
-            raise PermissionDenied("LDAP failed to authenticate user")
+        user = super().authenticate_ldap_user(ldap_user, password)
 
         if self.settings.SET_USABLE_PASSWORD:
+            if not user:
+                # Fail early and do not try other backends
+                raise PermissionDenied("LDAP failed to authenticate user")
+
             # Set a usable password so users can change their LDAP password
             user.set_password(password)
             user.save()
diff --git a/aleksis/core/util/pdf.py b/aleksis/core/util/pdf.py
new file mode 100644
index 0000000000000000000000000000000000000000..18640a0e8667d91b7c31b8130b597e7992985e8d
--- /dev/null
+++ b/aleksis/core/util/pdf.py
@@ -0,0 +1,112 @@
+import os
+import subprocess  # noqa
+from tempfile import TemporaryDirectory
+from typing import Optional
+
+from django.core.files import File
+from django.http.request import HttpRequest
+from django.http.response import HttpResponse
+from django.shortcuts import get_object_or_404, render
+from django.template.loader import render_to_string
+from django.urls import reverse
+from django.utils import timezone
+from django.utils.translation import get_language
+from django.utils.translation import gettext as _
+
+from celery_progress.backend import ProgressRecorder
+
+from aleksis.core.celery import app
+from aleksis.core.models import PDFFile
+from aleksis.core.util.celery_progress import recorded_task
+
+
+@recorded_task
+def generate_pdf(
+    file_pk: int, html_url: str, recorder: ProgressRecorder, lang: Optional[str] = None
+):
+    """Generate a PDF file by rendering the HTML code using a headless Chromium."""
+    file_object = get_object_or_404(PDFFile, pk=file_pk)
+
+    recorder.set_progress(0, 1)
+
+    # Open a temporary directory
+    with TemporaryDirectory() as temp_dir:
+        pdf_path = os.path.join(temp_dir, "print.pdf")
+        lang = lang or get_language()
+
+        # Run PDF generation using a headless Chromium
+        cmd = [
+            "chromium",
+            "--headless",
+            "--no-sandbox",
+            "--run-all-compositor-stages-before-draw",
+            "--temp-profile",
+            "--disable-dev-shm-usage",
+            "--disable-gpu",
+            "--disable-setuid-sandbox",
+            "--dbus-stub",
+            f"--home-dir={temp_dir}",
+            f"--lang={lang}",
+            f"--print-to-pdf={pdf_path}",
+            html_url,
+        ]
+        res = subprocess.run(cmd)  # noqa
+
+        # Let the task fail on a non-success return code
+        res.check_returncode()
+
+        # Upload PDF file to media storage
+        with open(pdf_path, "rb") as f:
+            file_object.file.save("print.pdf", File(f))
+            file_object.save()
+
+    recorder.set_progress(1, 1)
+
+
+def render_pdf(request: HttpRequest, template_name: str, context: dict = None) -> HttpResponse:
+    """Start PDF generation and show progress page.
+
+    The progress page will redirect to the PDF after completion.
+    """
+    if not context:
+        context = {}
+
+    html_template = render_to_string(template_name, context)
+
+    file_object = PDFFile.objects.create(person=request.user.person, html=html_template)
+    html_url = request.build_absolute_uri(file_object.html_url)
+
+    result = generate_pdf.delay(file_object.pk, html_url, lang=get_language())
+
+    redirect_url = reverse("redirect_to_pdf_file", args=[file_object.pk])
+
+    progress_context = {
+        "title": _("Progress: Generate PDF file"),
+        "back_url": context.get("back_url", "index"),
+        "progress": {
+            "task_id": result.task_id,
+            "title": _("Generating PDF file …"),
+            "success": _("The PDF file has been generated successfully."),
+            "error": _("There was a problem while generating the PDF file."),
+            "redirect_on_success": redirect_url,
+        },
+        "additional_button": {
+            "href": redirect_url,
+            "caption": _("Download PDF"),
+            "icon": "picture_as_pdf",
+        },
+    }
+
+    # Render progress view
+    return render(request, "core/pages/progress.html", progress_context)
+
+
+def clean_up_expired_pdf_files() -> None:
+    """Clean up expired PDF files."""
+    PDFFile.objects.filter(expires_at__lt=timezone.now()).delete()
+
+
+@app.task
+def clean_up_expired_pdf_files_task() -> None:
+    """Clean up expired PDF files."""
+    return clean_up_expired_pdf_files()
diff --git a/aleksis/core/util/predicates.py b/aleksis/core/util/predicates.py
index c456ee329e3560c65abc9953a689ad23053736f3..c6e6035301be735ac439bd21a229446ad490208c 100644
--- a/aleksis/core/util/predicates.py
+++ b/aleksis/core/util/predicates.py
@@ -131,3 +131,14 @@ def is_notification_recipient(user: User, obj: Model) -> bool:
     notification a user wants to mark read is this user.
     """
     return user == obj.recipient.user
+
+
+def contains_site_preference_value(section: str, pref: str, value: str):
+    """Check if given site preference contains a value."""
+    name = f"check_site_preference_value:{section}__{pref}"
+
+    @predicate(name)
+    def fn() -> bool:
+        return bool(value in get_site_preferences()[f"{section}__{pref}"])
+
+    return fn
diff --git a/aleksis/core/views.py b/aleksis/core/views.py
index 0ef10817f58eae5bb61b52bfda041a7d4e293ab2..25c3ae8ebf2d6e39604d25629564ddcabce742c9 100644
--- a/aleksis/core/views.py
+++ b/aleksis/core/views.py
@@ -6,14 +6,14 @@ 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.http import Http404, HttpRequest, HttpResponse, HttpResponseNotFound
 from django.shortcuts import get_object_or_404, redirect, render
 from django.urls import reverse, 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.base import TemplateView, View
+from django.views.generic.detail import DetailView, SingleObjectMixin
 from django.views.generic.edit import DeleteView, UpdateView
 from django.views.generic.list import ListView
 
@@ -30,6 +30,7 @@ from oauth2_provider.models import Application
 from reversion import set_user
 from reversion.views import RevisionMixin
 from rules.contrib.views import PermissionRequiredMixin, permission_required
+from templated_email import send_templated_mail
 
 from aleksis.core.data_checks import DataCheckRegistry, check_data
 
@@ -60,6 +61,7 @@ from .models import (
     Group,
     GroupType,
     Notification,
+    PDFFile,
     Person,
     SchoolTerm,
 )
@@ -78,8 +80,20 @@ from .tables import (
 )
 from .util import messages
 from .util.apps import AppConfig
-from .util.core_helpers import has_person, objectgetter_optional
+from .util.core_helpers import get_site_preferences, has_person, objectgetter_optional
 from .util.forms import PreferenceLayout
+from .util.pdf import render_pdf
+
+
+class RenderPDFView(TemplateView):
+    """View to render a PDF file from a template.
+
+    Makes use of ``render_pdf``.
+    """
+
+    def get(self, request: HttpRequest, *args, **kwargs) -> HttpResponse:
+        context = self.get_context_data(**kwargs)
+        return render_pdf(request, self.template_name, context)
 
 
 @permission_required("core.view_dashboard")
@@ -341,16 +355,39 @@ def edit_person(request: HttpRequest, id_: Optional[int] = None) -> HttpResponse
     if id_:
         # Edit form for existing group
         edit_person_form = EditPersonForm(
-            request.POST or None, request.FILES or None, instance=person
+            request, request.POST or None, request.FILES or None, instance=person
         )
     else:
         # Empty form to create a new group
         if request.user.has_perm("core.create_person"):
-            edit_person_form = EditPersonForm(request.POST or None, request.FILES or None)
+            edit_person_form = EditPersonForm(request, request.POST or None, request.FILES or None)
         else:
             raise PermissionDenied()
     if request.method == "POST":
         if edit_person_form.is_valid():
+            if person and person == request.user.person:
+                # Check if user edited non-editable field
+                notification_fields = get_site_preferences()[
+                    "account__notification_on_person_change"
+                ]
+                send_notification_fields = set(edit_person_form.changed_data).intersection(
+                    set(notification_fields)
+                )
+                context["send_notification_fields"] = send_notification_fields
+                if send_notification_fields:
+                    context["send_notification_fields"] = send_notification_fields
+                    send_templated_mail(
+                        template_name="person_changed",
+                        from_email=request.user.person.mail_sender_via,
+                        headers={
+                            "Reply-To": request.user.person.mail_sender,
+                            "Sender": request.user.person.mail_sender,
+                        },
+                        recipient_list=[
+                            get_site_preferences()["account__person_change_notification_contact"]
+                        ],
+                        context=context,
+                    )
             with reversion.create_revision():
                 set_user(request.user)
                 edit_person_form.save(commit=True)
@@ -431,6 +468,11 @@ class SystemStatus(PermissionRequiredMixin, MainView):
         return self.render_to_response(context, status=status_code)
 
 
+class TestPDFGenerationView(PermissionRequiredMixin, RenderPDFView):
+    template_name = "core/pages/test_pdf.html"
+    permission_required = "core.test_pdf"
+
+
 @permission_required(
     "core.mark_notification_as_read", fn=objectgetter_optional(Notification, None, False)
 )
@@ -1002,3 +1044,27 @@ class OAuth2Update(PermissionRequiredMixin, UpdateView):
                 "redirect_uris",
             ),
         )
+
+
+class RedirectToPDFFile(SingleObjectMixin, View):
+    """Redirect to a generated PDF file."""
+
+    model = PDFFile
+
+    def get(self, *args, **kwargs):
+        file_object = self.get_object()
+        if not file_object.file:
+            raise Http404()
+        return redirect(file_object.file.url)
+
+
+class HTMLForPDFFile(SingleObjectMixin, View):
+    """Return rendered HTML for generating a PDF file."""
+
+    model = PDFFile
+
+    def get(self, request, *args, **kwargs):
+        file_object = self.get_object()
+        if request.GET.get("secret") != file_object.secret:
+            raise PermissionDenied()
+        return HttpResponse(file_object.html)
diff --git a/conftest.py b/conftest.py
new file mode 100644
index 0000000000000000000000000000000000000000..2aa286cdb37550088652b10cced7bfc3d3b56aa9
--- /dev/null
+++ b/conftest.py
@@ -0,0 +1 @@
+pytest_plugins = ("celery.contrib.pytest", )
diff --git a/docs/dev/01_setup.rst b/docs/dev/01_setup.rst
index 31c72ee6c8bcaea746291ab038318fa614f0103d..122a9baa14ce942886bd8d1a73fc5584f77cad19 100644
--- a/docs/dev/01_setup.rst
+++ b/docs/dev/01_setup.rst
@@ -28,8 +28,7 @@ Install native dependencies
 
 Some system libraries are required to install AlekSIS::
 
-  sudo apt install build-essential libpq-dev libpq5 libssl-dev python3-dev python3-pip python3-venv yarnpkg gettext
-
+  sudo apt install build-essential libpq-dev libpq5 libssl-dev python3-dev python3-pip python3-venv yarnpkg gettext chromium
 
 Get Poetry
 ----------
diff --git a/poetry.lock b/poetry.lock
index 739da325fa6d81610f79f897974b6751e12293e4..39968896fc96a41ef5702cb84fa58ff3283bc705 100644
--- a/poetry.lock
+++ b/poetry.lock
@@ -8,11 +8,11 @@ python-versions = "*"
 
 [[package]]
 name = "aleksis-builddeps"
-version = "2"
+version = "3"
 description = "AlekSIS (School Information System) — Build/Dev dependencies for apps"
 category = "dev"
 optional = false
-python-versions = "*"
+python-versions = ">=3.6,<4.0"
 
 [package.dependencies]
 black = ">=19.10b0,<20.0"
@@ -27,7 +27,8 @@ flake8-docstrings = ">=1.5.0,<2.0.0"
 flake8-fixme = ">=1.1.1,<2.0.0"
 flake8-isort = ">=4.0.0,<5.0.0"
 flake8-mypy = ">=17.8.0,<18.0.0"
-flake8-rst-docstrings = ">=0.0.14,<0.0.15"
+flake8-rst-docstrings = ">=0.1.0,<0.2.0"
+freezegun = ">=1.1.0,<2.0.0"
 isort = ">=5.0.0,<6.0.0"
 pytest = ">=6.0,<7.0"
 pytest-cov = ">=2.8.1,<3.0.0"
@@ -74,14 +75,17 @@ python-versions = "*"
 
 [[package]]
 name = "asgiref"
-version = "3.3.1"
+version = "3.3.4"
 description = "ASGI specs, helper code, and adapters"
 category = "main"
 optional = false
-python-versions = ">=3.5"
+python-versions = ">=3.6"
+
+[package.dependencies]
+typing-extensions = {version = "*", markers = "python_version < \"3.8\""}
 
 [package.extras]
-tests = ["pytest", "pytest-asyncio"]
+tests = ["pytest", "pytest-asyncio", "mypy (>=0.800)"]
 
 [[package]]
 name = "asn1crypto"
@@ -115,7 +119,7 @@ tests_no_zope = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>
 
 [[package]]
 name = "babel"
-version = "2.9.0"
+version = "2.9.1"
 description = "Internationalization utilities"
 category = "dev"
 optional = false
@@ -213,20 +217,20 @@ python-versions = "*"
 
 [[package]]
 name = "boto3"
-version = "1.17.44"
+version = "1.17.62"
 description = "The AWS SDK for Python"
 category = "main"
 optional = true
 python-versions = ">= 2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*"
 
 [package.dependencies]
-botocore = ">=1.20.44,<1.21.0"
+botocore = ">=1.20.62,<1.21.0"
 jmespath = ">=0.7.1,<1.0.0"
-s3transfer = ">=0.3.0,<0.4.0"
+s3transfer = ">=0.4.0,<0.5.0"
 
 [[package]]
 name = "botocore"
-version = "1.20.44"
+version = "1.20.62"
 description = "Low-level, data-driven core of boto 3."
 category = "main"
 optional = true
@@ -238,7 +242,7 @@ python-dateutil = ">=2.1,<3.0.0"
 urllib3 = ">=1.25.4,<1.27"
 
 [package.extras]
-crt = ["awscrt (==0.10.8)"]
+crt = ["awscrt (==0.11.11)"]
 
 [[package]]
 name = "bs4"
@@ -495,7 +499,7 @@ dev = ["black (==19.10b0)", "flake8 (==3.8.4)", "mypy (==0.812)", "pytest (==6.2
 
 [[package]]
 name = "decorator"
-version = "5.0.5"
+version = "5.0.7"
 description = "Decorators for Humans"
 category = "main"
 optional = false
@@ -511,31 +515,31 @@ python-versions = "*"
 
 [[package]]
 name = "django"
-version = "3.1.7"
+version = "3.2"
 description = "A high-level Python Web framework that encourages rapid development and clean, pragmatic design."
 category = "main"
 optional = false
 python-versions = ">=3.6"
 
 [package.dependencies]
-asgiref = ">=3.2.10,<4"
+asgiref = ">=3.3.2,<4"
 pytz = "*"
 sqlparse = ">=0.2.2"
 
 [package.extras]
-argon2 = ["argon2-cffi (>=16.1.0)"]
+argon2 = ["argon2-cffi (>=19.1.0)"]
 bcrypt = ["bcrypt"]
 
 [[package]]
 name = "django-any-js"
-version = "1.0.3.post1"
-description = "Include JavaScript libraries with readable template tags"
+version = "1.1"
+description = "Include JavaScript/CSS libraries with readable template tags"
 category = "main"
 optional = false
-python-versions = "*"
+python-versions = ">=3.7,<4.0"
 
 [package.dependencies]
-Django = ">=1.11"
+Django = ">=2.2,<4.0"
 
 [[package]]
 name = "django-appconf"
@@ -550,7 +554,7 @@ django = "*"
 
 [[package]]
 name = "django-auth-ldap"
-version = "2.3.0"
+version = "2.4.0"
 description = "Django LDAP authentication backend."
 category = "main"
 optional = true
@@ -654,6 +658,14 @@ python-versions = "*"
 [package.dependencies]
 django-js-asset = ">=1.2.2"
 
+[[package]]
+name = "django-cleanup"
+version = "5.2.0"
+description = "Deletes old files."
+category = "main"
+optional = false
+python-versions = "*"
+
 [[package]]
 name = "django-colorfield"
 version = "0.4.1"
@@ -677,7 +689,7 @@ six = "*"
 
 [[package]]
 name = "django-debug-toolbar"
-version = "3.2"
+version = "3.2.1"
 description = "A configurable set of panels that display various debug information about the current request/response."
 category = "main"
 optional = false
@@ -702,12 +714,15 @@ six = "*"
 
 [[package]]
 name = "django-extensions"
-version = "3.1.1"
+version = "3.1.3"
 description = "Extensions for Django"
 category = "main"
 optional = false
 python-versions = ">=3.6"
 
+[package.dependencies]
+Django = ">=2.2"
+
 [[package]]
 name = "django-favicon-plus-reloaded"
 version = "1.0.4"
@@ -733,14 +748,14 @@ Django = ">=2.2"
 
 [[package]]
 name = "django-formtools"
-version = "2.2"
+version = "2.3"
 description = "A set of high-level abstractions for Django forms"
 category = "main"
 optional = false
-python-versions = "*"
+python-versions = ">=3.6"
 
 [package.dependencies]
-Django = ">=1.11"
+Django = ">=2.2"
 
 [[package]]
 name = "django-guardian"
@@ -781,14 +796,14 @@ Django = ">=2.2"
 
 [[package]]
 name = "django-health-check"
-version = "3.16.3"
+version = "3.16.4"
 description = "Run checks on services like databases, queue servers, celery processes, etc."
 category = "main"
 optional = false
 python-versions = "*"
 
 [package.dependencies]
-django = ">=1.11"
+django = ">=2.2"
 
 [[package]]
 name = "django-impersonate"
@@ -847,7 +862,7 @@ python-versions = "*"
 
 [[package]]
 name = "django-material"
-version = "1.7.6"
+version = "1.9.0"
 description = "Material design for django forms and admin"
 category = "main"
 optional = false
@@ -864,17 +879,6 @@ category = "main"
 optional = false
 python-versions = "*"
 
-[[package]]
-name = "django-middleware-global-request"
-version = "0.1.2"
-description = "Django middleware that keep request instance for every thread."
-category = "main"
-optional = false
-python-versions = "*"
-
-[package.dependencies]
-django = "*"
-
 [[package]]
 name = "django-model-utils"
 version = "4.1.1"
@@ -903,7 +907,7 @@ six = "*"
 
 [[package]]
 name = "django-otp"
-version = "1.0.3"
+version = "1.0.4"
 description = "A pluggable framework for adding two-factor authentication to Django using one-time passwords."
 category = "main"
 optional = false
@@ -917,7 +921,7 @@ qrcode = ["qrcode"]
 
 [[package]]
 name = "django-otp-yubikey"
-version = "1.0.0"
+version = "1.0.0.post1"
 description = "A django-otp plugin that verifies YubiKey OTP tokens."
 category = "main"
 optional = false
@@ -929,11 +933,11 @@ YubiOTP = ">=0.2.2"
 
 [[package]]
 name = "django-phonenumber-field"
-version = "5.0.0"
+version = "5.1.0"
 description = "An international phone number field for django models."
 category = "main"
 optional = false
-python-versions = ">=3.5"
+python-versions = ">=3.6"
 
 [package.dependencies]
 Django = ">=2.2"
@@ -1012,7 +1016,7 @@ django = ">=1.11"
 
 [[package]]
 name = "django-sass-processor"
-version = "1.0.0"
+version = "1.0.1"
 description = "SASS processor to compile SCSS files into *.css, while rendering, or offline."
 category = "main"
 optional = false
@@ -1023,7 +1027,7 @@ management_command = ["django-compressor (>=2.4)"]
 
 [[package]]
 name = "django-select2"
-version = "7.7.0"
+version = "7.7.1"
 description = "Select2 option fields for Django"
 category = "main"
 optional = false
@@ -1065,7 +1069,7 @@ sftp = ["paramiko"]
 
 [[package]]
 name = "django-stubs"
-version = "1.7.0"
+version = "1.8.0"
 description = "Mypy stubs for Django"
 category = "dev"
 optional = false
@@ -1073,9 +1077,21 @@ python-versions = ">=3.6"
 
 [package.dependencies]
 django = "*"
+django-stubs-ext = "*"
 mypy = ">=0.790"
 typing-extensions = "*"
 
+[[package]]
+name = "django-stubs-ext"
+version = "0.2.0"
+description = "Monkey-patching and extensions for django-stubs"
+category = "dev"
+optional = false
+python-versions = ">=3.6"
+
+[package.dependencies]
+django = "*"
+
 [[package]]
 name = "django-tables2"
 version = "2.3.4"
@@ -1175,7 +1191,7 @@ six = "*"
 
 [[package]]
 name = "docutils"
-version = "0.17"
+version = "0.16"
 description = "Docutils -- Python Documentation Utilities"
 category = "dev"
 optional = false
@@ -1221,7 +1237,7 @@ yaml = ["ruamel.yaml"]
 
 [[package]]
 name = "faker"
-version = "7.0.1"
+version = "8.1.2"
 description = "Faker is a Python package that generates fake data for you."
 category = "main"
 optional = false
@@ -1233,7 +1249,7 @@ text-unidecode = "1.3"
 
 [[package]]
 name = "flake8"
-version = "3.9.0"
+version = "3.9.1"
 description = "the modular source code checker: pep8 pyflakes and co"
 category = "dev"
 optional = false
@@ -1358,15 +1374,28 @@ flake8 = "*"
 
 [[package]]
 name = "flake8-rst-docstrings"
-version = "0.0.14"
+version = "0.1.2"
 description = "Python docstring reStructuredText (RST) validator"
 category = "dev"
 optional = false
-python-versions = "*"
+python-versions = ">=3.3"
 
 [package.dependencies]
 flake8 = ">=3.0.0"
-restructuredtext_lint = "*"
+pydocstyle = ">=3.0.0"
+pygments = "*"
+restructuredtext-lint = "*"
+
+[[package]]
+name = "freezegun"
+version = "1.1.0"
+description = "Let your Python tests travel through time"
+category = "dev"
+optional = false
+python-versions = ">=3.5"
+
+[package.dependencies]
+python-dateutil = ">=2.7"
 
 [[package]]
 name = "gitdb"
@@ -1381,14 +1410,15 @@ smmap = ">=3.0.1,<5"
 
 [[package]]
 name = "gitpython"
-version = "3.1.14"
+version = "3.1.15"
 description = "Python Git Library"
 category = "dev"
 optional = false
-python-versions = ">=3.4"
+python-versions = ">=3.5"
 
 [package.dependencies]
 gitdb = ">=4.0.1,<5"
+typing-extensions = ">=3.7.4.0"
 
 [[package]]
 name = "html2text"
@@ -1416,7 +1446,7 @@ python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
 
 [[package]]
 name = "importlib-metadata"
-version = "3.10.0"
+version = "4.0.1"
 description = "Read metadata from Python packages"
 category = "main"
 optional = false
@@ -1682,7 +1712,7 @@ python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
 
 [[package]]
 name = "pbr"
-version = "5.5.1"
+version = "5.6.0"
 description = "Python Build Reasonableness"
 category = "dev"
 optional = false
@@ -1709,18 +1739,18 @@ ptyprocess = ">=0.5"
 
 [[package]]
 name = "pg8000"
-version = "1.19.1"
+version = "1.19.3"
 description = "PostgreSQL interface library"
 category = "dev"
 optional = false
 python-versions = ">=3.6"
 
 [package.dependencies]
-scramp = "1.3.0"
+scramp = "1.4.0"
 
 [[package]]
 name = "phonenumbers"
-version = "8.12.20"
+version = "8.12.22"
 description = "Python version of Google's common library for parsing, formatting, storing and validating international phone numbers."
 category = "main"
 optional = false
@@ -1758,7 +1788,7 @@ dev = ["pre-commit", "tox"]
 
 [[package]]
 name = "prometheus-client"
-version = "0.10.0"
+version = "0.10.1"
 description = "Python client for the Prometheus monitoring system."
 category = "main"
 optional = false
@@ -1943,7 +1973,7 @@ testing = ["fields", "hunter", "process-tests (==2.0.2)", "six", "pytest-xdist",
 
 [[package]]
 name = "pytest-django"
-version = "4.1.0"
+version = "4.2.0"
 description = "A Django plugin for pytest."
 category = "dev"
 optional = false
@@ -2103,7 +2133,7 @@ docutils = ">=0.11,<1.0"
 
 [[package]]
 name = "ruamel.yaml"
-version = "0.17.2"
+version = "0.17.4"
 description = "ruamel.yaml is a YAML parser/emitter that supports roundtrip preservation of comments, seq/map flow style, and map key order"
 category = "main"
 optional = false
@@ -2134,7 +2164,7 @@ python-versions = "*"
 
 [[package]]
 name = "s3transfer"
-version = "0.3.6"
+version = "0.4.2"
 description = "An Amazon S3 Transfer Manager"
 category = "main"
 optional = true
@@ -2143,6 +2173,9 @@ python-versions = "*"
 [package.dependencies]
 botocore = ">=1.12.36,<2.0a.0"
 
+[package.extras]
+crt = ["botocore[crt] (>=1.20.29,<2.0a.0)"]
+
 [[package]]
 name = "safety"
 version = "1.10.3"
@@ -2159,7 +2192,7 @@ requests = "*"
 
 [[package]]
 name = "scramp"
-version = "1.3.0"
+version = "1.4.0"
 description = "An implementation of the SCRAM protocol."
 category = "dev"
 optional = false
@@ -2221,7 +2254,7 @@ python-versions = "*"
 
 [[package]]
 name = "sphinx"
-version = "3.5.3"
+version = "3.5.4"
 description = "Python documentation generator"
 category = "dev"
 optional = false
@@ -2231,7 +2264,7 @@ python-versions = ">=3.5"
 alabaster = ">=0.7,<0.8"
 babel = ">=1.3"
 colorama = {version = ">=0.3.5", markers = "sys_platform == \"win32\""}
-docutils = ">=0.12"
+docutils = ">=0.12,<0.17"
 imagesize = "*"
 Jinja2 = ">=2.3"
 packaging = "*"
@@ -2252,11 +2285,11 @@ test = ["pytest", "pytest-cov", "html5lib", "cython", "typed-ast"]
 
 [[package]]
 name = "sphinx-autodoc-typehints"
-version = "1.11.1"
+version = "1.12.0"
 description = "Type hints (PEP 484) support for the Sphinx autodoc extension"
 category = "dev"
 optional = false
-python-versions = ">=3.5.2"
+python-versions = ">=3.6"
 
 [package.dependencies]
 Sphinx = ">=3.0"
@@ -2429,7 +2462,7 @@ python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*"
 
 [[package]]
 name = "tqdm"
-version = "4.59.0"
+version = "4.60.0"
 description = "Fast, Extensible Progress Meter"
 category = "main"
 optional = false
@@ -2456,7 +2489,7 @@ test = ["pytest"]
 
 [[package]]
 name = "twilio"
-version = "6.55.0"
+version = "6.57.0"
 description = "Twilio API client and TwiML generator"
 category = "main"
 optional = false
@@ -2470,7 +2503,7 @@ six = "*"
 
 [[package]]
 name = "typed-ast"
-version = "1.4.2"
+version = "1.4.3"
 description = "a fork of Python 2 and 3 ast modules with type comment support"
 category = "dev"
 optional = false
@@ -2559,7 +2592,7 @@ s3 = ["boto3", "django-storages"]
 [metadata]
 lock-version = "1.1"
 python-versions = "^3.7"
-content-hash = "40f73ca3816a2b7654803fd9169fc13c8a203cb40512231ff0a7d280a7a332c1"
+content-hash = "d5a493b473e4d884266797986af002cd2c44271946df31b3523a5bd25311ceda"
 
 [metadata.files]
 alabaster = [
@@ -2567,7 +2600,7 @@ alabaster = [
     {file = "alabaster-0.7.12.tar.gz", hash = "sha256:a661d72d58e6ea8a57f7a86e37d86716863ee5e92788398526d58b26a4e4dc02"},
 ]
 aleksis-builddeps = [
-    {file = "AlekSIS-Builddeps-2.tar.gz", hash = "sha256:fdf8b230ba4a690c279d99004316e84d7d9d72962768ca6b3205df54db9abaab"},
+    {file = "AlekSIS-Builddeps-3.tar.gz", hash = "sha256:04597e29a861e576d78adc068c9f1bf85450c81dc43cb1f7db0ed43e975b64a2"},
 ]
 amqp = [
     {file = "amqp-5.0.6-py3-none-any.whl", hash = "sha256:493a2ac6788ce270a2f6a765b017299f60c1998f5a8617908ee9be082f7300fb"},
@@ -2582,8 +2615,8 @@ appnope = [
     {file = "appnope-0.1.2.tar.gz", hash = "sha256:dd83cd4b5b460958838f6eb3000c660b1f9caf2a5b1de4264e941512f603258a"},
 ]
 asgiref = [
-    {file = "asgiref-3.3.1-py3-none-any.whl", hash = "sha256:5ee950735509d04eb673bd7f7120f8fa1c9e2df495394992c73234d526907e17"},
-    {file = "asgiref-3.3.1.tar.gz", hash = "sha256:7162a3cb30ab0609f1a4c95938fd73e8604f63bdba516a7f7d64b83ff09478f0"},
+    {file = "asgiref-3.3.4-py3-none-any.whl", hash = "sha256:92906c611ce6c967347bbfea733f13d6313901d54dcca88195eaeb52b2a8e8ee"},
+    {file = "asgiref-3.3.4.tar.gz", hash = "sha256:d1216dfbdfb63826470995d31caed36225dcaf34f182e0fa257a4dd9e86f1b78"},
 ]
 asn1crypto = [
     {file = "asn1crypto-1.4.0-py2.py3-none-any.whl", hash = "sha256:4bcdf33c861c7d40bdcd74d8e4dd7661aac320fcdf40b9a3f95b4ee12fde2fa8"},
@@ -2598,8 +2631,8 @@ attrs = [
     {file = "attrs-20.3.0.tar.gz", hash = "sha256:832aa3cde19744e49938b91fea06d69ecb9e649c93ba974535d08ad92164f700"},
 ]
 babel = [
-    {file = "Babel-2.9.0-py2.py3-none-any.whl", hash = "sha256:9d35c22fcc79893c3ecc85ac4a56cde1ecf3f19c540bba0922308a6c06ca6fa5"},
-    {file = "Babel-2.9.0.tar.gz", hash = "sha256:da031ab54472314f210b0adcff1588ee5d1d1d0ba4dbd07b94dba82bde791e05"},
+    {file = "Babel-2.9.1-py2.py3-none-any.whl", hash = "sha256:ab49e12b91d937cd11f0b67cb259a57ab4ad2b59ac7a3b41d6c06c0ac5b0def9"},
+    {file = "Babel-2.9.1.tar.gz", hash = "sha256:bc0c176f9f6a994582230df350aa6e05ba2ebe4b3ac317eab29d9be5d2768da0"},
 ]
 backcall = [
     {file = "backcall-0.2.0-py2.py3-none-any.whl", hash = "sha256:fbbce6a29f263178a1f7915c1940bde0ec2b2a967566fe1c65c1dfb7422bd255"},
@@ -2631,12 +2664,12 @@ bleach = [
     {file = "boolean.py-3.8.tar.gz", hash = "sha256:cc24e20f985d60cd4a3a5a1c0956dd12611159d32a75081dabd0c9ab981acaa4"},
 ]
 boto3 = [
-    {file = "boto3-1.17.44-py2.py3-none-any.whl", hash = "sha256:e74da1da74fbefbe2db7a9c53082018d862433f35e2ecd4c173632efc5742f40"},
-    {file = "boto3-1.17.44.tar.gz", hash = "sha256:ffb9b192b2b52ab88cde09e2af7d9fd6e541287e5719098be97ffd7144f47eb1"},
+    {file = "boto3-1.17.62-py2.py3-none-any.whl", hash = "sha256:da1b2c884dbf56cc3ece07940a7b654f41a93b9fc40ee1ed21a76da25a05989c"},
+    {file = "boto3-1.17.62.tar.gz", hash = "sha256:d856a71d74351649ca8dd59ad17c8c3e79ea57734ff4a38a97611e1e10b06863"},
 ]
 botocore = [
-    {file = "botocore-1.20.44-py2.py3-none-any.whl", hash = "sha256:8a7f85bf05ad62551b0e6dfeeec471147b330cb2b5c7f48795057e811e6a2e77"},
-    {file = "botocore-1.20.44.tar.gz", hash = "sha256:2958e3912939558fd789a64b23a10039d8b0c0c84a23b573f3f2e3154de357ad"},
+    {file = "botocore-1.20.62-py2.py3-none-any.whl", hash = "sha256:e4f8cb923edf035c2ae5f6169c70e77e31df70b88919b92b826a6b9bd14511b1"},
+    {file = "botocore-1.20.62.tar.gz", hash = "sha256:f7c2c5c5ed5212b2628d8fb1c587b31c6e8d413ecbbd1a1cdf6f96ed6f5c8d5e"},
 ]
 bs4 = [
     {file = "bs4-0.0.1.tar.gz", hash = "sha256:36ecea1fd7cc5c0c6e4a1ff075df26d50da647b75376626cc186e2212886dd3a"},
@@ -2802,27 +2835,28 @@ curlylint = [
     {file = "curlylint-0.12.2.tar.gz", hash = "sha256:76b557cf8d007bd92df2dae61a02e65f8aa2ff3e05c6398b1314d92692fbb0d8"},
 ]
 decorator = [
-    {file = "decorator-5.0.5-py3-none-any.whl", hash = "sha256:b7157d62ea3c2c0c57b81a05e4569853e976a3dda5dd7a1cb86be78978c3c5f8"},
-    {file = "decorator-5.0.5.tar.gz", hash = "sha256:acda948ffcfe4bd0c4a57834b74ad968b91925b8201b740ca9d46fb8c5c618ce"},
+    {file = "decorator-5.0.7-py3-none-any.whl", hash = "sha256:945d84890bb20cc4a2f4a31fc4311c0c473af65ea318617f13a7257c9a58bc98"},
+    {file = "decorator-5.0.7.tar.gz", hash = "sha256:6f201a6c4dac3d187352661f508b9364ec8091217442c9478f1f83c003a0f060"},
 ]
 dj-database-url = [
     {file = "dj-database-url-0.5.0.tar.gz", hash = "sha256:4aeaeb1f573c74835b0686a2b46b85990571159ffc21aa57ecd4d1e1cb334163"},
     {file = "dj_database_url-0.5.0-py2.py3-none-any.whl", hash = "sha256:851785365761ebe4994a921b433062309eb882fedd318e1b0fcecc607ed02da9"},
 ]
 django = [
-    {file = "Django-3.1.7-py3-none-any.whl", hash = "sha256:baf099db36ad31f970775d0be5587cc58a6256a6771a44eb795b554d45f211b8"},
-    {file = "Django-3.1.7.tar.gz", hash = "sha256:32ce792ee9b6a0cbbec340123e229ac9f765dff8c2a4ae9247a14b2ba3a365a7"},
+    {file = "Django-3.2-py3-none-any.whl", hash = "sha256:0604e84c4fb698a5e53e5857b5aea945b2f19a18f25f10b8748dbdf935788927"},
+    {file = "Django-3.2.tar.gz", hash = "sha256:21f0f9643722675976004eb683c55d33c05486f94506672df3d6a141546f389d"},
 ]
 django-any-js = [
-    {file = "django-any-js-1.0.3.post1.tar.gz", hash = "sha256:32306643d4989b3cdbbf6a87bb43ca4d5ca35863c96ad96a8bc0d50bcf9d4ab4"},
+    {file = "django-any-js-1.1.tar.gz", hash = "sha256:2972946902ba049f73bf8bb87e0a0118f77a8c9dca89438f193598bff758422f"},
+    {file = "django_any_js-1.1-py3-none-any.whl", hash = "sha256:1499934e293bbcaad29b8edaaefca87dda79eb3df1faeaaea67b80e2866ae1f8"},
 ]
 django-appconf = [
     {file = "django-appconf-1.0.4.tar.gz", hash = "sha256:be58deb54a43d77d2e1621fe59f787681376d3cd0b8bd8e4758ef6c3a6453380"},
     {file = "django_appconf-1.0.4-py2.py3-none-any.whl", hash = "sha256:1b1d0e1069c843ebe8ae5aa48ec52403b1440402b320c3e3a206a0907e97bb06"},
 ]
 django-auth-ldap = [
-    {file = "django-auth-ldap-2.3.0.tar.gz", hash = "sha256:5894317122a086c9955ed366562869a81459cf6b663636b152857bb5d3a0a3b7"},
-    {file = "django_auth_ldap-2.3.0-py3-none-any.whl", hash = "sha256:cbbb476eff2504b5ab4fdf1fa92d93d2d3408fd9c8bc0c426169d987d0733153"},
+    {file = "django-auth-ldap-2.4.0.tar.gz", hash = "sha256:60fcbfc3141c99c3c49d3ccd7311a3992a231c319d94b6d2c143968f63676676"},
+    {file = "django_auth_ldap-2.4.0-py3-none-any.whl", hash = "sha256:2d869955da8a0c9a4448671bd9826b9f87458f6a9fc20278e84de8a81200a2be"},
 ]
 django-bleach = [
     {file = "django-bleach-0.6.1.tar.gz", hash = "sha256:674709c26040618aff0741ce8261fd151e5ead405bd50568c2034662d69daac3"},
@@ -2856,6 +2890,10 @@ django-ckeditor = [
     {file = "django-ckeditor-6.0.0.tar.gz", hash = "sha256:29fd1a333cb9741ac2c3fd4e427a5c00115ed33a2389716a09af7656022dcdde"},
     {file = "django_ckeditor-6.0.0-py2.py3-none-any.whl", hash = "sha256:cc2d377f1bdcd4ca1540caeebe85f7e2cd006198d57328ef6c718d3eaa5a0846"},
 ]
+django-cleanup = [
+    {file = "django-cleanup-5.2.0.tar.gz", hash = "sha256:909d10ff574f5ce1a40fa63bd5c94c9ed866fd7ae770994c46cdf66c3db3e846"},
+    {file = "django_cleanup-5.2.0-py2.py3-none-any.whl", hash = "sha256:193cf69de54b9fc0a0f4547edbb3a63bbe01728cb029f9f4b7912098cc1bced7"},
+]
 django-colorfield = [
     {file = "django-colorfield-0.4.1.tar.gz", hash = "sha256:63a542c417b72d0dac898a0f61a2a00aed3c9aabc2f5057c926efccf421f7887"},
     {file = "django_colorfield-0.4.1-py3-none-any.whl", hash = "sha256:e38f8b9dabbab48a6dab3d1eb5bd802decb92970d56a28128c9a70cdbf383e30"},
@@ -2864,16 +2902,16 @@ django-dbbackup = [
     {file = "django-dbbackup-3.3.0.tar.gz", hash = "sha256:bb109735cae98b64ad084e5b461b7aca2d7b39992f10c9ed9435e3ebb6fb76c8"},
 ]
 django-debug-toolbar = [
-    {file = "django-debug-toolbar-3.2.tar.gz", hash = "sha256:84e2607d900dbd571df0a2acf380b47c088efb787dce9805aefeb407341961d2"},
-    {file = "django_debug_toolbar-3.2-py3-none-any.whl", hash = "sha256:9e5a25d0c965f7e686f6a8ba23613ca9ca30184daa26487706d4829f5cfb697a"},
+    {file = "django-debug-toolbar-3.2.1.tar.gz", hash = "sha256:a5ff2a54f24bf88286f9872836081078f4baa843dc3735ee88524e89f8821e33"},
+    {file = "django_debug_toolbar-3.2.1-py3-none-any.whl", hash = "sha256:e759e63e3fe2d3110e0e519639c166816368701eab4a47fed75d7de7018467b9"},
 ]
 django-dynamic-preferences = [
     {file = "django-dynamic-preferences-1.10.1.tar.gz", hash = "sha256:e4b2bb7b2563c5064ba56dd76441c77e06b850ff1466a386a1cd308909a6c7de"},
     {file = "django_dynamic_preferences-1.10.1-py2.py3-none-any.whl", hash = "sha256:9419fa925fd2cbb665269ae72059eb3058bf080913d853419b827e4e7a141902"},
 ]
 django-extensions = [
-    {file = "django-extensions-3.1.1.tar.gz", hash = "sha256:674ad4c3b1587a884881824f40212d51829e662e52f85b012cd83d83fe1271d9"},
-    {file = "django_extensions-3.1.1-py3-none-any.whl", hash = "sha256:9507f8761ee760748938fd8af766d0608fb2738cf368adfa1b2451f61c15ae35"},
+    {file = "django-extensions-3.1.3.tar.gz", hash = "sha256:5f0fea7bf131ca303090352577a9e7f8bfbf5489bd9d9c8aea9401db28db34a0"},
+    {file = "django_extensions-3.1.3-py3-none-any.whl", hash = "sha256:50de8977794a66a91575dd40f87d5053608f679561731845edbd325ceeb387e3"},
 ]
 django-favicon-plus-reloaded = [
     {file = "django-favicon-plus-reloaded-1.0.4.tar.gz", hash = "sha256:90c761c636a338e6e9fb1d086649d82095085f92cff816c9cf074607f28c85a5"},
@@ -2884,8 +2922,8 @@ django-filter = [
     {file = "django_filter-2.4.0-py3-none-any.whl", hash = "sha256:e00d32cebdb3d54273c48f4f878f898dced8d5dfaad009438fe61ebdf535ace1"},
 ]
 django-formtools = [
-    {file = "django-formtools-2.2.tar.gz", hash = "sha256:c5272c03c1cd51b2375abf7397a199a3148a9fbbf2f100e186467a84025d13b2"},
-    {file = "django_formtools-2.2-py2.py3-none-any.whl", hash = "sha256:304fa777b8ef9e0693ce7833f885cb89ba46b0e46fc23b01176900a93f46742f"},
+    {file = "django-formtools-2.3.tar.gz", hash = "sha256:9663b6eca64777b68d6d4142efad8597fe9a685924673b25aa8a1dcff4db00c3"},
+    {file = "django_formtools-2.3-py3-none-any.whl", hash = "sha256:4699937e19ee041d803943714fe0c1c7ad4cab802600eb64bbf4cdd0a1bfe7d9"},
 ]
 django-guardian = [
     {file = "django-guardian-2.3.0.tar.gz", hash = "sha256:ed2de26e4defb800919c5749fb1bbe370d72829fbd72895b6cf4f7f1a7607e1b"},
@@ -2899,8 +2937,8 @@ django-haystack = [
     {file = "django-haystack-3.0.tar.gz", hash = "sha256:d490f920afa85471dd1fa5000bc8eff4b704daacbe09aee1a64e75cbc426f3be"},
 ]
 django-health-check = [
-    {file = "django-health-check-3.16.3.tar.gz", hash = "sha256:a6aa6ea423eae4fd0665f6372b826af1ed20dfc3e88cf52789d0b49cfb64969c"},
-    {file = "django_health_check-3.16.3-py2.py3-none-any.whl", hash = "sha256:d0628ffc11aee7e62e73b58ff39179ea2a9ca5abfbc92cb345ceca268593dd71"},
+    {file = "django-health-check-3.16.4.tar.gz", hash = "sha256:334bcbbb9273a6dbd9c928e78474306e623dfb38cc442281cb9fd230a20a7fdb"},
+    {file = "django_health_check-3.16.4-py2.py3-none-any.whl", hash = "sha256:86a8869d67e72394a1dd73e37819a7d2cfd915588b96927fda611d7451fd4735"},
 ]
 django-impersonate = [
     {file = "django-impersonate-1.7.3.tar.gz", hash = "sha256:282003957577c7143fe31e5861f8fffdf6fe0c25557aedb28fcf8b11474eaa23"},
@@ -2925,15 +2963,12 @@ django-maintenance-mode = [
     {file = "django_maintenance_mode-0.16.0-py3-none-any.whl", hash = "sha256:88287573b4e95285052f664d4f08e15ac4c350c1a6c77bc743ca3fc6e1f6410c"},
 ]
 django-material = [
-    {file = "django-material-1.7.6.tar.gz", hash = "sha256:5488e8fe24069cc6682801692ad05293a4b60a637a87a31e0ebd9f3319cd371d"},
-    {file = "django_material-1.7.6-py2.py3-none-any.whl", hash = "sha256:b5496505da7dd92f23ca694bc411c6bf0ff584fc30f4239d890ab29f9260160c"},
+    {file = "django-material-1.9.0.tar.gz", hash = "sha256:5a7144d1029b4a2bfee2e5d0d8d00f30742dd7e3f868b3787d8cd61e54f26437"},
+    {file = "django_material-1.9.0-py2.py3-none-any.whl", hash = "sha256:816513170771bcb2540b5ce314fbef1a906906220587a9cb9521e61092a6f610"},
 ]
 django-menu-generator-ng = [
     {file = "django-menu-generator-ng-1.2.3.tar.gz", hash = "sha256:0c21a094b094add909655728b6b2d4a8baa5a2047da8f649be52589bea0e3ba2"},
 ]
-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.1.1.tar.gz", hash = "sha256:eb5dd05ef7d7ce6bc79cae54ea7c4a221f6f81e2aad7722933aee66489e7264b"},
     {file = "django_model_utils-4.1.1-py3-none-any.whl", hash = "sha256:ef7c440024e797796a3811432abdd2be8b5225ae64ef346f8bfc6de7d8e5d73c"},
@@ -2943,16 +2978,16 @@ django-oauth-toolkit = [
     {file = "django_oauth_toolkit-1.5.0-py3-none-any.whl", hash = "sha256:b2e346a7c1e222774bfb370f21b556b92b408395b4c23914e2d1b241b2e5376a"},
 ]
 django-otp = [
-    {file = "django-otp-1.0.3.tar.gz", hash = "sha256:381a15e65293b8b06d47b7d6b306e0b7af2e104137ac92f6c566d3b9b90b6244"},
-    {file = "django_otp-1.0.3-py3-none-any.whl", hash = "sha256:f4ab096b424c33ffe69453620356e1b7517f30dfb9ba13bfeaa1d1f20faddc13"},
+    {file = "django-otp-1.0.4.tar.gz", hash = "sha256:04852c5301befb02d1d8ba4a31d375eb08d7c2cb6fe86b5f840867435ab1309c"},
+    {file = "django_otp-1.0.4-py3-none-any.whl", hash = "sha256:3916fc7652c2f934b1cf3807dd8ed257ce7605c10dfefa27fadda5628d9a9c9e"},
 ]
 django-otp-yubikey = [
-    {file = "django-otp-yubikey-1.0.0.tar.gz", hash = "sha256:fbd409277892229b7e3578faa4f63ea766e242659456939164c8f71b845287b6"},
-    {file = "django_otp_yubikey-1.0.0-py2.py3-none-any.whl", hash = "sha256:07743473024900c3b7a14647039f2cf66148cf6243d6aee0853ba45516c224a4"},
+    {file = "django-otp-yubikey-1.0.0.post1.tar.gz", hash = "sha256:1da060257611d06e681848b7923fd788d878a79e8c358a373374deab13a085af"},
+    {file = "django_otp_yubikey-1.0.0.post1-py2.py3-none-any.whl", hash = "sha256:613c96be211c1267400a5a78ae63f212c722f82dffb9daef3c8b1df370abb9be"},
 ]
 django-phonenumber-field = [
-    {file = "django-phonenumber-field-5.0.0.tar.gz", hash = "sha256:1eb7af3a108744665f7c3939d38aa15b3728c57d13d45d656b0a2aa11e8cdc3c"},
-    {file = "django_phonenumber_field-5.0.0-py3-none-any.whl", hash = "sha256:adb46905cc4ecb19d8494424e1c4352f24946bb472340a2a17257d44bf8228e6"},
+    {file = "django-phonenumber-field-5.1.0.tar.gz", hash = "sha256:9eda963ac15b363393f677cc084efd45c3bd97bb5a0cdb4a06409ac99e05dd4b"},
+    {file = "django_phonenumber_field-5.1.0-py3-none-any.whl", hash = "sha256:48724ba235ee8248a474204faa0934c5baf9536f429859d05cb131fbd6b1c695"},
 ]
 django-polymorphic = [
     {file = "django-polymorphic-3.0.0.tar.gz", hash = "sha256:9d886f19f031d26bb1391c055ed9be06fb226a04a4cec1842b372c58873b3caa"},
@@ -2979,11 +3014,12 @@ django-reversion = [
     {file = "django_reversion-3.0.9-py3-none-any.whl", hash = "sha256:1b57127a136b969f4b843a915c72af271febe7f336469db6c27121f8adcad35c"},
 ]
 django-sass-processor = [
-    {file = "django-sass-processor-1.0.0.tar.gz", hash = "sha256:cb90efee38cd7b0fe727c78d8993ad7804de33f40328200dfc1a481307ef0466"},
+    {file = "django-sass-processor-1.0.1.tar.gz", hash = "sha256:dcaad47c591a2d52689c1bd209259e922e902d886293f0d5c9e0d1a4eb85eda2"},
+    {file = "django_sass_processor-1.0.1-py3-none-any.whl", hash = "sha256:1f043180c47754018e803a77da003377f5ea6558de57cd6946eb27a32e9c16a2"},
 ]
 django-select2 = [
-    {file = "django-select2-7.7.0.tar.gz", hash = "sha256:26b4c59cbeba57aea1737187b930a83c8070788286b4236b13f7873c01b32684"},
-    {file = "django_select2-7.7.0-py2.py3-none-any.whl", hash = "sha256:e56bfe3074d6b87524c5dbc139884c18c74a5e7324d66f0b93e42b6012ea0dc0"},
+    {file = "django-select2-7.7.1.tar.gz", hash = "sha256:dd091342e99436818b3fa98783ae6c24fb2a0cbc37ebd3faa0aef68422b6e416"},
+    {file = "django_select2-7.7.1-py2.py3-none-any.whl", hash = "sha256:8c54984bb931d842eab6a46d1b427c6883e5f5347529cda27dcd942fb37d87b9"},
 ]
 django-settings-context-processor = [
     {file = "django-settings-context-processor-0.2.tar.gz", hash = "sha256:d37c853d69a3069f5abbf94c7f4f6fc0fac38bbd0524190cd5a250ba800e496a"},
@@ -2993,8 +3029,12 @@ django-storages = [
     {file = "django_storages-1.11.1-py3-none-any.whl", hash = "sha256:f28765826d507a0309cfaa849bd084894bc71d81bf0d09479168d44785396f80"},
 ]
 django-stubs = [
-    {file = "django-stubs-1.7.0.tar.gz", hash = "sha256:ddd190aca5b9adb4d30760d5c64f67cb3658703f5f42c3bb0c2c71ff4d752c39"},
-    {file = "django_stubs-1.7.0-py3-none-any.whl", hash = "sha256:30a7d99c694acf79c5d93d69a5a8e4b54d2a8c11dd672aa869006789e2189fa6"},
+    {file = "django-stubs-1.8.0.tar.gz", hash = "sha256:717967d7fee0a6af0746724a0be80d72831a982a40fa8f245a6a46f4cafd157b"},
+    {file = "django_stubs-1.8.0-py3-none-any.whl", hash = "sha256:bde9e44e3c4574c2454e74a3e607cc3bc23b0441bb7d1312cd677d5e30984b74"},
+]
+django-stubs-ext = [
+    {file = "django-stubs-ext-0.2.0.tar.gz", hash = "sha256:c14f297835a42c1122421ec7e2d06579996b29d33b8016002762afa5d78863af"},
+    {file = "django_stubs_ext-0.2.0-py3-none-any.whl", hash = "sha256:bd4a1e36ef2ba0ef15801933c85c68e59b383302c873795c6ecfc25950c7ecdb"},
 ]
 django-tables2 = [
     {file = "django-tables2-2.3.4.tar.gz", hash = "sha256:50ccadbd13740a996d8a4d4f144ef80134745cd0b5ec278061537e341f5ef7a2"},
@@ -3022,8 +3062,8 @@ django-yarnpkg = [
     {file = "django-yarnpkg-6.0.1.tar.gz", hash = "sha256:aa059347b246c6f242401581d2c129bdcb45aa726be59fe2f288762a9843348a"},
 ]
 docutils = [
-    {file = "docutils-0.17-py2.py3-none-any.whl", hash = "sha256:a71042bb7207c03d5647f280427f14bfbd1a65c9eb84f4b341d85fafb6bb4bdf"},
-    {file = "docutils-0.17.tar.gz", hash = "sha256:e2ffeea817964356ba4470efba7c2f42b6b0de0b04e66378507e3e2504bbff4c"},
+    {file = "docutils-0.16-py2.py3-none-any.whl", hash = "sha256:0c5b78adfbf7762415433f5515cd5c9e762339e23369dbe8000d84a4bf4ab3af"},
+    {file = "docutils-0.16.tar.gz", hash = "sha256:c2de3a60e9e7d07be26b7f2b00ca0309c207e06c100f9cc2a94931fc75a478fc"},
 ]
 dparse = [
     {file = "dparse-0.5.1-py3-none-any.whl", hash = "sha256:e953a25e44ebb60a5c6efc2add4420c177f1d8404509da88da9729202f306994"},
@@ -3034,18 +3074,19 @@ dynaconf = [
     {file = "dynaconf-3.1.4.tar.gz", hash = "sha256:b2f472d83052f809c5925565b8a2ba76a103d5dc1dbb9748b693ed67212781b9"},
 ]
 faker = [
-    {file = "Faker-7.0.1-py3-none-any.whl", hash = "sha256:08c4cfbfd498c0e90aff6741771c01803d894013df858db6a573182c6a47951f"},
-    {file = "Faker-7.0.1.tar.gz", hash = "sha256:20c6e4253b73ef2a783d38e085e7c8d8916295fff31c7403116d2af8f908f7ca"},
+    {file = "Faker-8.1.2-py3-none-any.whl", hash = "sha256:156854f36d4086bb21ff85a79b4d6a6403a240cd2c17a33a44b8ea4ff4e957c2"},
+    {file = "Faker-8.1.2.tar.gz", hash = "sha256:a2ed065342e91a7672407325848cd5728d5e5eb4928d0a1c478fd4f0dd97d1f7"},
 ]
 flake8 = [
-    {file = "flake8-3.9.0-py2.py3-none-any.whl", hash = "sha256:12d05ab02614b6aee8df7c36b97d1a3b2372761222b19b58621355e82acddcff"},
-    {file = "flake8-3.9.0.tar.gz", hash = "sha256:78873e372b12b093da7b5e5ed302e8ad9e988b38b063b61ad937f26ca58fc5f0"},
+    {file = "flake8-3.9.1-py2.py3-none-any.whl", hash = "sha256:3b9f848952dddccf635be78098ca75010f073bfe14d2c6bda867154bea728d2a"},
+    {file = "flake8-3.9.1.tar.gz", hash = "sha256:1aa8990be1e689d96c745c5682b687ea49f2e05a443aff1f8251092b0014e378"},
 ]
 flake8-bandit = [
     {file = "flake8_bandit-2.1.2.tar.gz", hash = "sha256:687fc8da2e4a239b206af2e54a90093572a60d0954f3054e23690739b0b0de3b"},
 ]
 flake8-black = [
     {file = "flake8-black-0.2.1.tar.gz", hash = "sha256:f26651bc10db786c03f4093414f7c9ea982ed8a244cec323c984feeffdf4c118"},
+    {file = "flake8_black-0.2.1-py3-none-any.whl", hash = "sha256:941514149cb8b489cb17a4bb1cf18d84375db3b34381bb018de83509437931a0"},
 ]
 flake8-builtins = [
     {file = "flake8-builtins-1.5.3.tar.gz", hash = "sha256:09998853b2405e98e61d2ff3027c47033adbdc17f9fe44ca58443d876eb00f3b"},
@@ -3076,15 +3117,20 @@ flake8-polyfill = [
     {file = "flake8_polyfill-1.0.2-py2.py3-none-any.whl", hash = "sha256:12be6a34ee3ab795b19ca73505e7b55826d5f6ad7230d31b18e106400169b9e9"},
 ]
 flake8-rst-docstrings = [
-    {file = "flake8-rst-docstrings-0.0.14.tar.gz", hash = "sha256:8f8bcb18f1408b506dd8ba2c99af3eac6128f6911d4bf6ff874b94caa70182a2"},
+    {file = "flake8-rst-docstrings-0.1.2.tar.gz", hash = "sha256:7d34d2175a0cd92aba0872ade74268b2f2c12582c7267d4a0e6ef1c32a828ce3"},
+    {file = "flake8_rst_docstrings-0.1.2-py3-none-any.whl", hash = "sha256:73b5db2fd9d4d7c7e6b7767931730c4570e5a89f34a1501b837afb3b6d31537a"},
+]
+freezegun = [
+    {file = "freezegun-1.1.0-py2.py3-none-any.whl", hash = "sha256:2ae695f7eb96c62529f03a038461afe3c692db3465e215355e1bb4b0ab408712"},
+    {file = "freezegun-1.1.0.tar.gz", hash = "sha256:177f9dd59861d871e27a484c3332f35a6e3f5d14626f2bf91be37891f18927f3"},
 ]
 gitdb = [
     {file = "gitdb-4.0.7-py3-none-any.whl", hash = "sha256:6c4cc71933456991da20917998acbe6cf4fb41eeaab7d6d67fbc05ecd4c865b0"},
     {file = "gitdb-4.0.7.tar.gz", hash = "sha256:96bf5c08b157a666fec41129e6d327235284cca4c81e92109260f353ba138005"},
 ]
 gitpython = [
-    {file = "GitPython-3.1.14-py3-none-any.whl", hash = "sha256:3283ae2fba31c913d857e12e5ba5f9a7772bbc064ae2bb09efafa71b0dd4939b"},
-    {file = "GitPython-3.1.14.tar.gz", hash = "sha256:be27633e7509e58391f10207cd32b2a6cf5b908f92d9cd30da2e514e1137af61"},
+    {file = "GitPython-3.1.15-py3-none-any.whl", hash = "sha256:a77824e516d3298b04fb36ec7845e92747df8fcfee9cacc32dd6239f9652f867"},
+    {file = "GitPython-3.1.15.tar.gz", hash = "sha256:05af150f47a5cca3f4b0af289b73aef8cf3c4fe2385015b06220cbcdee48bb6e"},
 ]
 html2text = [
     {file = "html2text-2020.1.16-py3-none-any.whl", hash = "sha256:c7c629882da0cf377d66f073329ccf34a12ed2adf0169b9285ae4e63ef54c82b"},
@@ -3099,8 +3145,8 @@ imagesize = [
     {file = "imagesize-1.2.0.tar.gz", hash = "sha256:b1f6b5a4eab1f73479a50fb79fcf729514a900c341d8503d62a62dbc4127a2b1"},
 ]
 importlib-metadata = [
-    {file = "importlib_metadata-3.10.0-py3-none-any.whl", hash = "sha256:d2d46ef77ffc85cbf7dac7e81dd663fde71c45326131bea8033b9bad42268ebe"},
-    {file = "importlib_metadata-3.10.0.tar.gz", hash = "sha256:c9db46394197244adf2f0b08ec5bc3cf16757e9590b02af1fca085c16c0d600a"},
+    {file = "importlib_metadata-4.0.1-py3-none-any.whl", hash = "sha256:d7eb1dea6d6a6086f8be21784cc9e3bcfa55872b52309bc5fad53a8ea444465d"},
+    {file = "importlib_metadata-4.0.1.tar.gz", hash = "sha256:8c501196e49fb9df5df43833bdb1e4328f64847763ec8a50703148b73784d581"},
 ]
 iniconfig = [
     {file = "iniconfig-1.1.1-py2.py3-none-any.whl", hash = "sha256:011e24c64b7f47f6ebd835bb12a743f2fbe9a26d4cecaa7f53bc4f35ee9da8b3"},
@@ -3264,8 +3310,8 @@ pathspec = [
     {file = "pathspec-0.8.1.tar.gz", hash = "sha256:86379d6b86d75816baba717e64b1a3a3469deb93bb76d613c9ce79edc5cb68fd"},
 ]
 pbr = [
-    {file = "pbr-5.5.1-py2.py3-none-any.whl", hash = "sha256:b236cde0ac9a6aedd5e3c34517b423cd4fd97ef723849da6b0d2231142d89c00"},
-    {file = "pbr-5.5.1.tar.gz", hash = "sha256:5fad80b613c402d5b7df7bd84812548b2a61e9977387a80a5fc5c396492b13c9"},
+    {file = "pbr-5.6.0-py2.py3-none-any.whl", hash = "sha256:c68c661ac5cc81058ac94247278eeda6d2e6aecb3e227b0387c30d277e7ef8d4"},
+    {file = "pbr-5.6.0.tar.gz", hash = "sha256:42df03e7797b796625b1029c0400279c7c34fd7df24a7d7818a1abb5b38710dd"},
 ]
 persisting-theory = [
     {file = "persisting-theory-0.2.1.tar.gz", hash = "sha256:00ff7dcc8f481ff75c770ca5797d968e8725b6df1f77fe0cf7d20fa1e5790c0a"},
@@ -3275,12 +3321,12 @@ pexpect = [
     {file = "pexpect-4.8.0.tar.gz", hash = "sha256:fc65a43959d153d0114afe13997d439c22823a27cefceb5ff35c2178c6784c0c"},
 ]
 pg8000 = [
-    {file = "pg8000-1.19.1-py3-none-any.whl", hash = "sha256:35c1f3db6e5540456aa38da5f42589274e7266c25365d2829dc8d52513520abe"},
-    {file = "pg8000-1.19.1.tar.gz", hash = "sha256:cb7ace8c582b7000a5ee428efa8ff6c82a7d710cc0f7d2d76258703a2aa7afe3"},
+    {file = "pg8000-1.19.3-py3-none-any.whl", hash = "sha256:2f9599f455f813e9298982a741f40b62b7bdcf6816e51031d4adc2deda6525e1"},
+    {file = "pg8000-1.19.3.tar.gz", hash = "sha256:f547572d12de77b65888e4e72922e8ad55b88791c486df93f3d71207aea7f25f"},
 ]
 phonenumbers = [
-    {file = "phonenumbers-8.12.20-py2.py3-none-any.whl", hash = "sha256:7c2b26ee026f765a8032fc2a333b46fa1860445c7ce6df3b717b9f6985106084"},
-    {file = "phonenumbers-8.12.20.tar.gz", hash = "sha256:ee5a8508c4a414262abad92ec33f050347f681973ed0fb36e98b52bfe159f6b8"},
+    {file = "phonenumbers-8.12.22-py2.py3-none-any.whl", hash = "sha256:f9cb4882e5c7daeaa183af7b0390c44a02604731c7aab561d9456e1df4582207"},
+    {file = "phonenumbers-8.12.22.tar.gz", hash = "sha256:b20765cb9d1392308071a3462c06edcaa953cb383e3565f5fc0a6fb93f240150"},
 ]
 pickleshare = [
     {file = "pickleshare-0.7.5-py2.py3-none-any.whl", hash = "sha256:9649af414d74d4df115d5d718f82acb59c9d418196b7b4290ed47a12ce62df56"},
@@ -3326,8 +3372,8 @@ pluggy = [
     {file = "pluggy-0.13.1.tar.gz", hash = "sha256:15b2acde666561e1298d71b523007ed7364de07029219b604cf808bfa1c765b0"},
 ]
 prometheus-client = [
-    {file = "prometheus_client-0.10.0-py2.py3-none-any.whl", hash = "sha256:c5843b3e1b4689a3599a2463e5b5850d110d1a7e28a94bdc2c6f5bb6585cfb18"},
-    {file = "prometheus_client-0.10.0.tar.gz", hash = "sha256:1e7bc14fd6ca9c3fc07309b73a7a3469920dfe88ca9f331c02258cc62736cbc2"},
+    {file = "prometheus_client-0.10.1-py2.py3-none-any.whl", hash = "sha256:030e4f9df5f53db2292eec37c6255957eb76168c6f974e4176c711cf91ed34aa"},
+    {file = "prometheus_client-0.10.1.tar.gz", hash = "sha256:b6c5a9643e3545bcbfd9451766cbaa5d9c67e7303c7bc32c750b6fa70ecb107d"},
 ]
 prompt-toolkit = [
     {file = "prompt_toolkit-3.0.18-py3-none-any.whl", hash = "sha256:bf00f22079f5fadc949f42ae8ff7f05702826a97059ffcc6281036ad40ac6f04"},
@@ -3487,8 +3533,8 @@ pytest-cov = [
     {file = "pytest_cov-2.11.1-py2.py3-none-any.whl", hash = "sha256:bdb9fdb0b85a7cc825269a4c56b48ccaa5c7e365054b6038772c32ddcdc969da"},
 ]
 pytest-django = [
-    {file = "pytest-django-4.1.0.tar.gz", hash = "sha256:26f02c16d36fd4c8672390deebe3413678d89f30720c16efb8b2a6bf63b9041f"},
-    {file = "pytest_django-4.1.0-py3-none-any.whl", hash = "sha256:10e384e6b8912ded92db64c58be8139d9ae23fb8361e5fc139d8e4f8fc601bc2"},
+    {file = "pytest-django-4.2.0.tar.gz", hash = "sha256:80f8875226ec4dc0b205f0578072034563879d98d9b1bec143a80b9045716cb0"},
+    {file = "pytest_django-4.2.0-py3-none-any.whl", hash = "sha256:a51150d8962200250e850c6adcab670779b9c2aa07271471059d1fb92a843fa9"},
 ]
 pytest-django-testing-postgresql = [
     {file = "pytest-django-testing-postgresql-0.1.post0.tar.gz", hash = "sha256:78b0c58930084cb4393407b2e5a2a3b8734c627b841ecef7d62d39bbfb8e8a45"},
@@ -3601,8 +3647,8 @@ restructuredtext-lint = [
     {file = "restructuredtext_lint-1.3.2.tar.gz", hash = "sha256:d3b10a1fe2ecac537e51ae6d151b223b78de9fafdd50e5eb6b08c243df173c80"},
 ]
 "ruamel.yaml" = [
-    {file = "ruamel.yaml-0.17.2-py3-none-any.whl", hash = "sha256:0850def9ebca23b3a8c64c4b4115ebb6b364a10d49f89d289a26ee965e1e7d9d"},
-    {file = "ruamel.yaml-0.17.2.tar.gz", hash = "sha256:8f1e15421668b9edf30ed02899f5f81aff9808a4271935776f61a99a569a13da"},
+    {file = "ruamel.yaml-0.17.4-py3-none-any.whl", hash = "sha256:ac79fb25f5476e8e9ed1c53b8a2286d2c3f5dde49eb37dbcee5c7eb6a8415a22"},
+    {file = "ruamel.yaml-0.17.4.tar.gz", hash = "sha256:44bc6b54fddd45e4bc0619059196679f9e8b79c027f4131bb072e6a22f4d5e28"},
 ]
 "ruamel.yaml.clib" = [
     {file = "ruamel.yaml.clib-0.2.2-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:28116f204103cb3a108dfd37668f20abe6e3cafd0d3fd40dba126c732457b3cc"},
@@ -3641,16 +3687,16 @@ rules = [
     {file = "rules-2.2.tar.gz", hash = "sha256:9bae429f9d4f91a375402990da1541f9e093b0ac077221d57124d06eeeca4405"},
 ]
 s3transfer = [
-    {file = "s3transfer-0.3.6-py2.py3-none-any.whl", hash = "sha256:5d48b1fd2232141a9d5fb279709117aaba506cacea7f86f11bc392f06bfa8fc2"},
-    {file = "s3transfer-0.3.6.tar.gz", hash = "sha256:c5dadf598762899d8cfaecf68eba649cd25b0ce93b6c954b156aaa3eed160547"},
+    {file = "s3transfer-0.4.2-py2.py3-none-any.whl", hash = "sha256:9b3752887a2880690ce628bc263d6d13a3864083aeacff4890c1c9839a5eb0bc"},
+    {file = "s3transfer-0.4.2.tar.gz", hash = "sha256:cb022f4b16551edebbb31a377d3f09600dbada7363d8c5db7976e7f47732e1b2"},
 ]
 safety = [
     {file = "safety-1.10.3-py2.py3-none-any.whl", hash = "sha256:5f802ad5df5614f9622d8d71fedec2757099705c2356f862847c58c6dfe13e84"},
     {file = "safety-1.10.3.tar.gz", hash = "sha256:30e394d02a20ac49b7f65292d19d38fa927a8f9582cdfd3ad1adbbc66c641ad5"},
 ]
 scramp = [
-    {file = "scramp-1.3.0-py3-none-any.whl", hash = "sha256:6d73eae03e7a3d647a8c36ca95dc8082fe56496db6f803b561ab231627022f82"},
-    {file = "scramp-1.3.0.tar.gz", hash = "sha256:f56208b544387b98e9d39735cc054e273d060efcdf44bb4a20935180772d1ccf"},
+    {file = "scramp-1.4.0-py3-none-any.whl", hash = "sha256:27349d6839038fe3b56c641ea2a8703df065c1d605fdee67275857c0a82122b4"},
+    {file = "scramp-1.4.0.tar.gz", hash = "sha256:d27d768408c6fc025a0e567eed84325b0aaf24364c81ea5974e8334ae3c4fda3"},
 ]
 selenium = [
     {file = "selenium-3.141.0-py2.py3-none-any.whl", hash = "sha256:2d7131d7bc5a5b99a2d9b04aaf2612c411b03b8ca1b1ee8d3de5845a9be2cb3c"},
@@ -3677,12 +3723,12 @@ spdx-license-list = [
     {file = "spdx_license_list-0.5.2.tar.gz", hash = "sha256:952996f72ab807972dc2278bb9b91e5294767211e51f09aad9c0e2ff5b82a31b"},
 ]
 sphinx = [
-    {file = "Sphinx-3.5.3-py3-none-any.whl", hash = "sha256:3f01732296465648da43dec8fb40dc451ba79eb3e2cc5c6d79005fd98197107d"},
-    {file = "Sphinx-3.5.3.tar.gz", hash = "sha256:ce9c228456131bab09a3d7d10ae58474de562a6f79abb3dc811ae401cf8c1abc"},
+    {file = "Sphinx-3.5.4-py3-none-any.whl", hash = "sha256:2320d4e994a191f4b4be27da514e46b3d6b420f2ff895d064f52415d342461e8"},
+    {file = "Sphinx-3.5.4.tar.gz", hash = "sha256:19010b7b9fa0dc7756a6e105b2aacd3a80f798af3c25c273be64d7beeb482cb1"},
 ]
 sphinx-autodoc-typehints = [
-    {file = "sphinx-autodoc-typehints-1.11.1.tar.gz", hash = "sha256:244ba6d3e2fdb854622f643c7763d6f95b6886eba24bec28e86edf205e4ddb20"},
-    {file = "sphinx_autodoc_typehints-1.11.1-py3-none-any.whl", hash = "sha256:da049791d719f4c9813642496ee4764203e317f0697eb75446183fa2a68e3f77"},
+    {file = "sphinx-autodoc-typehints-1.12.0.tar.gz", hash = "sha256:193617d9dbe0847281b1399d369e74e34cd959c82e02c7efde077fca908a9f52"},
+    {file = "sphinx_autodoc_typehints-1.12.0-py3-none-any.whl", hash = "sha256:5e81776ec422dd168d688ab60f034fccfafbcd94329e9537712c93003bddc04a"},
 ]
 sphinxcontrib-applehelp = [
     {file = "sphinxcontrib-applehelp-1.0.2.tar.gz", hash = "sha256:a072735ec80e7675e3f432fcae8610ecf509c5f1869d17e2eecff44389cdbc58"},
@@ -3744,47 +3790,47 @@ toml = [
     {file = "toml-0.10.2.tar.gz", hash = "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f"},
 ]
 tqdm = [
-    {file = "tqdm-4.59.0-py2.py3-none-any.whl", hash = "sha256:9fdf349068d047d4cfbe24862c425883af1db29bcddf4b0eeb2524f6fbdb23c7"},
-    {file = "tqdm-4.59.0.tar.gz", hash = "sha256:d666ae29164da3e517fcf125e41d4fe96e5bb375cd87ff9763f6b38b5592fe33"},
+    {file = "tqdm-4.60.0-py2.py3-none-any.whl", hash = "sha256:daec693491c52e9498632dfbe9ccfc4882a557f5fa08982db1b4d3adbe0887c3"},
+    {file = "tqdm-4.60.0.tar.gz", hash = "sha256:ebdebdb95e3477ceea267decfc0784859aa3df3e27e22d23b83e9b272bf157ae"},
 ]
 traitlets = [
     {file = "traitlets-5.0.5-py3-none-any.whl", hash = "sha256:69ff3f9d5351f31a7ad80443c2674b7099df13cc41fc5fa6e2f6d3b0330b0426"},
     {file = "traitlets-5.0.5.tar.gz", hash = "sha256:178f4ce988f69189f7e523337a3e11d91c786ded9360174a3d9ca83e79bc5396"},
 ]
 twilio = [
-    {file = "twilio-6.55.0.tar.gz", hash = "sha256:766555e9f3bdfe9eb2fad9e2efa701f6f7644337a3f6b31a660293d2fbd54331"},
+    {file = "twilio-6.57.0.tar.gz", hash = "sha256:3a9a0e3882897185bf3a997408a43662bcfdec84250a701d06aff5b89bbaa9a1"},
 ]
 typed-ast = [
-    {file = "typed_ast-1.4.2-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:7703620125e4fb79b64aa52427ec192822e9f45d37d4b6625ab37ef403e1df70"},
-    {file = "typed_ast-1.4.2-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:c9aadc4924d4b5799112837b226160428524a9a45f830e0d0f184b19e4090487"},
-    {file = "typed_ast-1.4.2-cp35-cp35m-manylinux2014_aarch64.whl", hash = "sha256:9ec45db0c766f196ae629e509f059ff05fc3148f9ffd28f3cfe75d4afb485412"},
-    {file = "typed_ast-1.4.2-cp35-cp35m-win32.whl", hash = "sha256:85f95aa97a35bdb2f2f7d10ec5bbdac0aeb9dafdaf88e17492da0504de2e6400"},
-    {file = "typed_ast-1.4.2-cp35-cp35m-win_amd64.whl", hash = "sha256:9044ef2df88d7f33692ae3f18d3be63dec69c4fb1b5a4a9ac950f9b4ba571606"},
-    {file = "typed_ast-1.4.2-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:c1c876fd795b36126f773db9cbb393f19808edd2637e00fd6caba0e25f2c7b64"},
-    {file = "typed_ast-1.4.2-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:5dcfc2e264bd8a1db8b11a892bd1647154ce03eeba94b461effe68790d8b8e07"},
-    {file = "typed_ast-1.4.2-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:8db0e856712f79c45956da0c9a40ca4246abc3485ae0d7ecc86a20f5e4c09abc"},
-    {file = "typed_ast-1.4.2-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:d003156bb6a59cda9050e983441b7fa2487f7800d76bdc065566b7d728b4581a"},
-    {file = "typed_ast-1.4.2-cp36-cp36m-win32.whl", hash = "sha256:4c790331247081ea7c632a76d5b2a265e6d325ecd3179d06e9cf8d46d90dd151"},
-    {file = "typed_ast-1.4.2-cp36-cp36m-win_amd64.whl", hash = "sha256:d175297e9533d8d37437abc14e8a83cbc68af93cc9c1c59c2c292ec59a0697a3"},
-    {file = "typed_ast-1.4.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:cf54cfa843f297991b7388c281cb3855d911137223c6b6d2dd82a47ae5125a41"},
-    {file = "typed_ast-1.4.2-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:b4fcdcfa302538f70929eb7b392f536a237cbe2ed9cba88e3bf5027b39f5f77f"},
-    {file = "typed_ast-1.4.2-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:987f15737aba2ab5f3928c617ccf1ce412e2e321c77ab16ca5a293e7bbffd581"},
-    {file = "typed_ast-1.4.2-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:37f48d46d733d57cc70fd5f30572d11ab8ed92da6e6b28e024e4a3edfb456e37"},
-    {file = "typed_ast-1.4.2-cp37-cp37m-win32.whl", hash = "sha256:36d829b31ab67d6fcb30e185ec996e1f72b892255a745d3a82138c97d21ed1cd"},
-    {file = "typed_ast-1.4.2-cp37-cp37m-win_amd64.whl", hash = "sha256:8368f83e93c7156ccd40e49a783a6a6850ca25b556c0fa0240ed0f659d2fe496"},
-    {file = "typed_ast-1.4.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:963c80b583b0661918718b095e02303d8078950b26cc00b5e5ea9ababe0de1fc"},
-    {file = "typed_ast-1.4.2-cp38-cp38-manylinux1_i686.whl", hash = "sha256:e683e409e5c45d5c9082dc1daf13f6374300806240719f95dc783d1fc942af10"},
-    {file = "typed_ast-1.4.2-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:84aa6223d71012c68d577c83f4e7db50d11d6b1399a9c779046d75e24bed74ea"},
-    {file = "typed_ast-1.4.2-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:a38878a223bdd37c9709d07cd357bb79f4c760b29210e14ad0fb395294583787"},
-    {file = "typed_ast-1.4.2-cp38-cp38-win32.whl", hash = "sha256:a2c927c49f2029291fbabd673d51a2180038f8cd5a5b2f290f78c4516be48be2"},
-    {file = "typed_ast-1.4.2-cp38-cp38-win_amd64.whl", hash = "sha256:c0c74e5579af4b977c8b932f40a5464764b2f86681327410aa028a22d2f54937"},
-    {file = "typed_ast-1.4.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:07d49388d5bf7e863f7fa2f124b1b1d89d8aa0e2f7812faff0a5658c01c59aa1"},
-    {file = "typed_ast-1.4.2-cp39-cp39-manylinux1_i686.whl", hash = "sha256:240296b27397e4e37874abb1df2a608a92df85cf3e2a04d0d4d61055c8305ba6"},
-    {file = "typed_ast-1.4.2-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:d746a437cdbca200622385305aedd9aef68e8a645e385cc483bdc5e488f07166"},
-    {file = "typed_ast-1.4.2-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:14bf1522cdee369e8f5581238edac09150c765ec1cb33615855889cf33dcb92d"},
-    {file = "typed_ast-1.4.2-cp39-cp39-win32.whl", hash = "sha256:cc7b98bf58167b7f2db91a4327da24fb93368838eb84a44c472283778fc2446b"},
-    {file = "typed_ast-1.4.2-cp39-cp39-win_amd64.whl", hash = "sha256:7147e2a76c75f0f64c4319886e7639e490fee87c9d25cb1d4faef1d8cf83a440"},
-    {file = "typed_ast-1.4.2.tar.gz", hash = "sha256:9fc0b3cb5d1720e7141d103cf4819aea239f7d136acf9ee4a69b047b7986175a"},
+    {file = "typed_ast-1.4.3-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:2068531575a125b87a41802130fa7e29f26c09a2833fea68d9a40cf33902eba6"},
+    {file = "typed_ast-1.4.3-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:c907f561b1e83e93fad565bac5ba9c22d96a54e7ea0267c708bffe863cbe4075"},
+    {file = "typed_ast-1.4.3-cp35-cp35m-manylinux2014_aarch64.whl", hash = "sha256:1b3ead4a96c9101bef08f9f7d1217c096f31667617b58de957f690c92378b528"},
+    {file = "typed_ast-1.4.3-cp35-cp35m-win32.whl", hash = "sha256:dde816ca9dac1d9c01dd504ea5967821606f02e510438120091b84e852367428"},
+    {file = "typed_ast-1.4.3-cp35-cp35m-win_amd64.whl", hash = "sha256:777a26c84bea6cd934422ac2e3b78863a37017618b6e5c08f92ef69853e765d3"},
+    {file = "typed_ast-1.4.3-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:f8afcf15cc511ada719a88e013cec87c11aff7b91f019295eb4530f96fe5ef2f"},
+    {file = "typed_ast-1.4.3-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:52b1eb8c83f178ab787f3a4283f68258525f8d70f778a2f6dd54d3b5e5fb4341"},
+    {file = "typed_ast-1.4.3-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:01ae5f73431d21eead5015997ab41afa53aa1fbe252f9da060be5dad2c730ace"},
+    {file = "typed_ast-1.4.3-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:c190f0899e9f9f8b6b7863debfb739abcb21a5c054f911ca3596d12b8a4c4c7f"},
+    {file = "typed_ast-1.4.3-cp36-cp36m-win32.whl", hash = "sha256:398e44cd480f4d2b7ee8d98385ca104e35c81525dd98c519acff1b79bdaac363"},
+    {file = "typed_ast-1.4.3-cp36-cp36m-win_amd64.whl", hash = "sha256:bff6ad71c81b3bba8fa35f0f1921fb24ff4476235a6e94a26ada2e54370e6da7"},
+    {file = "typed_ast-1.4.3-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:0fb71b8c643187d7492c1f8352f2c15b4c4af3f6338f21681d3681b3dc31a266"},
+    {file = "typed_ast-1.4.3-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:760ad187b1041a154f0e4d0f6aae3e40fdb51d6de16e5c99aedadd9246450e9e"},
+    {file = "typed_ast-1.4.3-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:5feca99c17af94057417d744607b82dd0a664fd5e4ca98061480fd8b14b18d04"},
+    {file = "typed_ast-1.4.3-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:95431a26309a21874005845c21118c83991c63ea800dd44843e42a916aec5899"},
+    {file = "typed_ast-1.4.3-cp37-cp37m-win32.whl", hash = "sha256:aee0c1256be6c07bd3e1263ff920c325b59849dc95392a05f258bb9b259cf39c"},
+    {file = "typed_ast-1.4.3-cp37-cp37m-win_amd64.whl", hash = "sha256:9ad2c92ec681e02baf81fdfa056fe0d818645efa9af1f1cd5fd6f1bd2bdfd805"},
+    {file = "typed_ast-1.4.3-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:b36b4f3920103a25e1d5d024d155c504080959582b928e91cb608a65c3a49e1a"},
+    {file = "typed_ast-1.4.3-cp38-cp38-manylinux1_i686.whl", hash = "sha256:067a74454df670dcaa4e59349a2e5c81e567d8d65458d480a5b3dfecec08c5ff"},
+    {file = "typed_ast-1.4.3-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:7538e495704e2ccda9b234b82423a4038f324f3a10c43bc088a1636180f11a41"},
+    {file = "typed_ast-1.4.3-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:af3d4a73793725138d6b334d9d247ce7e5f084d96284ed23f22ee626a7b88e39"},
+    {file = "typed_ast-1.4.3-cp38-cp38-win32.whl", hash = "sha256:f2362f3cb0f3172c42938946dbc5b7843c2a28aec307c49100c8b38764eb6927"},
+    {file = "typed_ast-1.4.3-cp38-cp38-win_amd64.whl", hash = "sha256:dd4a21253f42b8d2b48410cb31fe501d32f8b9fbeb1f55063ad102fe9c425e40"},
+    {file = "typed_ast-1.4.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:f328adcfebed9f11301eaedfa48e15bdece9b519fb27e6a8c01aa52a17ec31b3"},
+    {file = "typed_ast-1.4.3-cp39-cp39-manylinux1_i686.whl", hash = "sha256:2c726c276d09fc5c414693a2de063f521052d9ea7c240ce553316f70656c84d4"},
+    {file = "typed_ast-1.4.3-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:cae53c389825d3b46fb37538441f75d6aecc4174f615d048321b716df2757fb0"},
+    {file = "typed_ast-1.4.3-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:b9574c6f03f685070d859e75c7f9eeca02d6933273b5e69572e5ff9d5e3931c3"},
+    {file = "typed_ast-1.4.3-cp39-cp39-win32.whl", hash = "sha256:209596a4ec71d990d71d5e0d312ac935d86930e6eecff6ccc7007fe54d703808"},
+    {file = "typed_ast-1.4.3-cp39-cp39-win_amd64.whl", hash = "sha256:9c6d1a54552b5330bc657b7ef0eae25d00ba7ffe85d9ea8ae6540d2197a3788c"},
+    {file = "typed_ast-1.4.3.tar.gz", hash = "sha256:fb1bbeac803adea29cedd70781399c99138358c26d05fcbd23c13016b7f5ec65"},
 ]
 typing-extensions = [
     {file = "typing_extensions-3.7.4.3-py2-none-any.whl", hash = "sha256:dafc7639cde7f1b6e1acc0f457842a83e722ccca8eef5270af2d74792619a89f"},
diff --git a/pyproject.toml b/pyproject.toml
index f2cafb552b79679de75626f5bdd588ff9048f16c..b5d24d7e844e23779a94c2c374579d1c873c49ad 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -34,14 +34,13 @@ secondary = true
 
 [tool.poetry.dependencies]
 python = "^3.7"
-Django = "^3.1.7"
-django-any-js = "^1.0"
+Django = "^3.2"
+django-any-js = "^1.1"
 django-debug-toolbar = "^3.2"
-django-middleware-global-request = "^0.1.2"
 django-menu-generator-ng = "^1.2.3"
 django-tables2 = "^2.1"
 Pillow = "^8.0"
-django-phonenumber-field = {version = "<5.1", extras = ["phonenumbers"]}
+django-phonenumber-field = {version = "<5.2", extras = ["phonenumbers"]}
 django-sass-processor = "^1.0"
 libsass = "^0.20.0"
 colour = "^0.1.5"
@@ -90,7 +89,7 @@ psutil = "^5.7.0"
 celery-progress = "^0.1.0"
 django-cachalot = "^2.3.2"
 django-prometheus = "^2.1.0"
-importlib-metadata = {version = "^3.0.0", python = "<3.9"}
+importlib-metadata = {version = "^4.0.0", python = "<3.9"}
 django-model-utils = "^4.0.0"
 bs4 = "^0.0.1"
 django-uwsgi-ng = "^1.1.0"
@@ -100,6 +99,7 @@ django-oauth-toolkit = "^1.5.0"
 django-redis = "^4.12.1"
 django-storages = {version = "^1.11.1", optional = true}
 boto3 = {version = "^1.17.33", optional = true}
+django-cleanup = "^5.1.0"
 
 [tool.poetry.extras]
 ldap = ["django-auth-ldap"]