diff --git a/.gitignore b/.gitignore
index 767581f76153ad5ac9f16422ee895a14b7a67f32..0faf3e4c3ecc7ff5de08a7c56ee313c488b183f6 100644
--- a/.gitignore
+++ b/.gitignore
@@ -65,6 +65,7 @@ docs/_build/
 aleksis/node_modules/
 aleksis/static/
 aleksis/whoosh_index/
+poetry.lock
 
 .coverage
 .mypy_cache/
diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index af5d314dbcc470d8e5aa3791d3ebe3957f26996c..564449be1717a97282b53b2d94f9f234b34261c2 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -1,6 +1,8 @@
 include:
     - project: "AlekSIS/official/AlekSIS"
       file: /ci/general.yml
+    - project: "AlekSIS/official/AlekSIS"
+      file: /ci/prepare/lock.yml
     - project: "AlekSIS/official/AlekSIS"
       file: /ci/test/test.yml
     - project: "AlekSIS/official/AlekSIS"
@@ -17,5 +19,3 @@ include:
       file: "/ci/deploy/review.yml"
     - project: "AlekSIS/official/AlekSIS"
       file: "/ci/deploy/trigger_dist.yml"
-    - project: "AlekSIS/official/AlekSIS"
-      file: /ci/deploy/pages.yml
diff --git a/.mailmap b/.mailmap
index f813351a7160f9a16f64e1e26868a139ce60ae56..c41b8aad5213d38c135d502ffc643f79a036bcf6 100644
--- a/.mailmap
+++ b/.mailmap
@@ -8,7 +8,9 @@ Jonathan Weth <git@jonathanweth.de> Jonathan Weth <joniweth@gmx.de>
 Jonathan Weth <git@jonathanweth.de> Jonathan Weth <mail@jonathanweth.de>
 Jonathan Weth <git@jonathanweth.de> Jonathan Weth <wethjo@katharineum.de>
 Julian Leucker <leuckerj@gmail.com> Julian <leuckerj@gmail.com>
+Lloyd Meins <git@lloydmeins.de> Aithus <lloydmeins@gmx.net>
 Silas Della Contrada <s.developer@4-dc.de> sdcieo0330 <silasdc0@gmail.com>
+Tom Teichler <tom.teichler@teckids.org> Tom Teichler <t.teichler@babiel.com>
 mirabilos <thorsten.glaser@teckids.org> mirabilos <mirabilos@evolvis.org>
 mirabilos <thorsten.glaser@teckids.org> mirabilos <t.glaser@tarent.de>
 root (Skolelinux) <root@tjener.intern> root <root@tjener.intern>
diff --git a/CHANGELOG.rst b/CHANGELOG.rst
index 4eab2dad571201d406bc83f0c8b974bf17699c24..6a3521ecb9c3e5fd9d5880c38022d03668c7ca4b 100644
--- a/CHANGELOG.rst
+++ b/CHANGELOG.rst
@@ -9,17 +9,359 @@ and this project adheres to `Semantic Versioning`_.
 Unreleased
 ----------
 
+Fixed
+~~~~~
+
+* ACCOUNT_ADAPTER was overriden by invitations
+
+Added
+~~~~~
+
+* Use identicons where avatars are missing.
+* Display personal photos instead of avatars based on a site preference.
+* Add an account menu in the top navbar.
+* Create a reusable snippet for avatar content.
+* Allow to configure if additional field is required
+* Allow to configure description of additional fields
+* Allow configuring regex for allowed usernames
+* [Dev] Support scheduled notifications.
+* Implement StaticContentWidget
+* Allow to enable password change independently of password reset
+
+Changed
+~~~~~~~
+
+* Added a `Retry` button to the server error page
+
+Fixed
+~~~~~
+
+* The ``reset password`` button on the login site used to overflow the card on smaller devices.
+
+`2.7.4`_ - 2022-02-09
+---------------------
+
+Changed
+~~~~~~~
+
+* Allow disabling query caching with cachalot
+* Add invitation key to success message when a person without e-mail address is invited by id
+
+Fixed
+~~~~~
+
+* Only exactly one person without e-mail address could be invited
+* No person was created and linked to the PersonInvitation object when invite by e-mail is used
+* No valid data in the second e-mail field of the signup form when it was disabled
+* Invitation options were displayed to superusers even when the feature was disabled
+* Inviting newly created persons for registration failed
+* Invited person was not displayed correctly in list of sent invitations
+* [Docker] Do not clear cache in migration container die to session invalidation issues
+* Notification email about user changes was broken
+* SQL cache invalidation could fail when hitting OOT database
+
+`2.7.3`_ - 2022-02-03
+---------------------
+
+Fixed
+~~~~~
+
+* Migration added in 2.7.2 did not work in all scenarios
+* [Dev] Field change tracking API for Person was broken in 2.7.2
+* [OAuth] Automatic clean-up of expired OAuth tokens could fail
+* Allow maskable icons for non-masked use
+* Add missing documentation
+
+Known issues
+~~~~~~~~~~~~
+
+* Maskable and non-masked icons *purpose) any cannot be separated
+
+`2.7.2`_ - 2022-01-31
+---------------------
+
+Changed
+~~~~~~~
+
+* [Dev] The (undocumented) setting PDF_CONTEXT_PROCESSORS is now named NON_REQUEST_CONTEXT_PROCESSORS
+* [Docker] Cache is now cleared if migrations are applied
+* Update German translations.
+
+Fixed
+~~~~~
+
+* Celery progress could be inaccurate if recording progress during a transaction
+
+
+`2.7.1`_ - 2022-01-28
+---------------------
+
+Changed
+~~~~~~~
+
+* PWA icons can now be marked maskable
+* [OAuth] Expired tokens are now cleared in a periodic task
+* PDF file jobs are now automatically expired
+* Data checks are now scheduled every 15 minutes by default
+
+Fixed
+~~~~~
+
+* PDF generation failed with S3 storage due to incompatibility with boto3
+* PWA theme colour defaulted to red
+* Form for editing group type displayed irrelevant fields
+* Permission groups could get outdated if re-assigning a user account to a different person
+* User preferences didn't work correctly sometimes due to race conditions.
+
+`2.7`_ - 2022-01-24
+-------------------
+
+Added
+~~~~~
+
+* Periodic tasks can now have a default schedule, which is automatically created
+
+Fixed
+~~~~~
+
+* Signup was forbidden even if it was enabled in settings
+* Phone numbers were not properly linked and suboptimally formatted on person page
+* Favicon upload failed with S3 storage.
+* Some combinations of allowed self-edit fields on persons could cause errors
+* Some preferences were required when they shouldn't, and vice versa.
+* IO errors on accessing backup directory in health check are now properly reported
+* Date picker was not properly initialized if field was already filled.
+* The menu item for entering an invitation code received offline was missing
+* CleaveJS was not loaded properly when using an external CDN
+
+Changed
+-------
+
+* Allow non-superusers with permission to invite persons
+
+`2.6`_ - 2022-01-10
+-------------------
+
+Added
+~~~~~
+
+* Add option to open entry in new tab for sidebar navigation menu.
+* Add preference for configuring the default phone number country code.
+* Persons and groups now have two image fields: official photo and public avatar
+* Admins recieve an mail for celery tasks with status "FAILURE"
+* OpenID Connect RSA keys can now be passed as string in config files
+* Views filtering for person names now also search the username of a linked user
+* OAuth2 applications now take an icon which is shown in the authorization progress.
+* Add support for hiding the main side nav in ``base.html``.
+* Provide base template and function for sending emails with a template.
+
+Fixed
+~~~~~
+
+* Changing the favicon did not result in all icons being replaced in some cases
+* Superusers with a dummy person were able to access the dashboard edit page.
+* GroupManager.get_queryset() returned an incomplete QuerySet
+* OAuth was broken by a non-semver-adhering django-oauth-toolkit update
+* Too long texts in chips didn't result in a larger chip.
+* The ``Person`` model had an ``is_active`` flag that was used in unclear ways; it is now removed
+* The data check results list view didn't work if a related object had been deleted in the meanwhile.
+* Socialaccount login template was not overriden
+* Atomic transactions now cause only one Haystack update task to run
+* Too long headlines didn't break in another line.
+
+Changed
+~~~~~~~
+
+* Configuration files are now deep merged by default
+* Improvements for shell_plus module loading
+
+  * core.Group model now takes precedence over auth.Group
+  * Name collisions are resolved by prefixing with the app label
+  * Apps can extend SHELL_PLUS_APP_PREFIXES and SHELL_PLUS_DONT_LOAD
+
+* [Docker] Base image now contains curl, grep, less, sed, and pspg
+* Views raising a 404 error can now customise the message that is displayed on the error page
+* OpenID Connect is enabled by default now, without RSA support
+* Login and authorization pages for OAuth2/OpenID Connect now indicate that the user is in progress
+  to authorize an external application.
+* Tables can be scrolled horizontally.
+* Overhauled person detail page
+* Use common base template for all emails.
+
+`2.5`_ – 2022-01-02
+-------------------
+
+Added
+~~~~~
+
+* Recursive helper methods for group hierarchies
+
+Fixed
+~~~~~
+
+* Remove left-over reference to preferences in a form definition that caused
+  form extensions in downstream apps to break
+* Allow non-LDAP users to authenticate if LDAP is used with password handling
+* Additional button on progress page for background tasks was shown even if the task failed.
+* Register preference for available allowed oauth grants.
+
+`2.4`_ – 2021-12-24
+-------------------
+
+Added
+~~~~~
+
+* Allow configuration of database options
+* User invitations with invite codes and targeted invites for existing
+  persons
+
+Fixed
+~~~~~
+
+* Correctly update theme colours on change again
+* Use correct favicon as default AlekSIS favicon
+* Show all years in a 200 year range around the current year in date pickers
+* Imprint is now called "Imprint" and not "Impress".
+* Logo files weren't uploaded to public namespace.
+* Limit LDAP network timeouts to not hang indefinitely on login if LDAP
+  server is unreachable
+
+Changed
+~~~~~~~
+
+* Modified the appearance of tables for mobile users to be more user friendly
+* [Dev] Remove lock file; locking dependencies is the distribution's
+  responsibility
+
+Removed
+~~~~~~~
+
+* Remove old generated AlekSIS icons
+
+`2.3.1`_ – 2021-12-17
+---------------------
+
+Fixed
+~~~~~
+
+* Small files could fail to upload to S3 storage due to MemoryFileUploadHandler
+* Corrected typos in previous changelog
+
+`2.3`_ – 2021-12-15
+-------------------
+
+Added
+~~~~~
+
+* [OAuth] Allow apps to fill in their own claim data matching their scopes
+
+Fixed
+~~~~~
+
+* View for assigning permissions didn't work with some global permissions.
+* PDFs generated in background didn't contain logo or site title.
+* Admins were redirected to their user preferences
+  while they wanted to edit the preferences of another user.
+* Some CharFields were using NULL values in database when field is empty
+* Optional dependecy `sentry-sdk` was not optional
+
+Changed
+~~~~~~~
+
+* Docker base image ships PostgreSQL 14 client binaries for maximum compatibility
+* Docker base image contains Sentry client by default (disabled in config by default)
+
+Removed
+~~~~~~~
+
+* Remove impersonation page. Use the impersonation button on the person
+  detail view instead.
+
+`2.2.1`_ – 2021-12-02
+--------------------
+
+Fixed
+~~~~~
+
+* [Docker] Stop initialisation if migrations fail
+* [OAuth] Register `groups` scope and fix claim
+* [OAuth] Fix OAuth claims for follow-up requests (e.g. UserInfo)
+* [OAuth] Fix grant types checking failing on wrong types under some circumstances
+* [OAuth] Re-introduce missing algorithm field in application form
+* Remove errornous backup folder check for S3
+
+`2.2`_ - 2021-11-29
+-------------------
+
+Added
+~~~~~
+
+* Support config files in sub-directories
+* Provide views for assigning/managing permissions in frontend
+* Support (icon) tabs in the top navbar.
+
+Changed
+~~~~~~~
+
+* Update German translations.
+
+Fixed
+~~~~~
+
+* Use new MaterializeCSS fork because the old version is no longer maintained.
+* Sender wasn't displayed for notifications on dashboard.
+* Notifications and activities on dashboard weren't sorted from old to new.
+
+`2.1.1`_ - 2021-11-14
+---------------------
+
+Added
+~~~~~
+
+* Provide ``SITE_PREFERENCES`` template variable for easier and request-independent access on all site preferences.
+
+Fixed
+~~~~~
+
+* Make style.css and favicons cachable.
+* Import model extensions from other apps before form extensions.
+* Recreate backwards compatiblity for OAuth URLs by using ``oauth/`` again.
+* Show correct logo and school title in print template if created in the background.
+
+Removed
+~~~~~~~
+
+* Remove fallback code from optional Celery as it's now non-optional.
+
+`2.1`_ - 2021-11-05
+-------------------
+
 Added
 ~~~~~
 
 * Provide an ``ExtensiblePolymorphicModel`` to support the features of extensible models for polymorphic models and vice-versa.
 * Implement optional Sentry integration for error and performance tracing.
+* Option to limit allowed scopes per application, including mixin to enforce that limit on OAuth resource views
+* Support trusted OAuth applications that leave out the authorisation screen.
+* Add birthplace to Person model.
 
 Changed
 ~~~~~~~
 
 * Replace dev.sh helper script with tox environments.
 * OAuth Grant Flows are now configured system-wide instead of per app.
+* Refactor OAuth2 application management views.
+
+Fixed
+~~~~~
+
+* Fix default admin contacts
+
+Credits
+~~~~~~~
+
+* We welcome new contributor 🐧 Jonathan Krüger!
+* We welcome new contributor 🐭 Lukas Weichelt!
 
 `2.0`_ - 2021-10-29
 -------------------
@@ -426,3 +768,17 @@ Fixed
 .. _2.0rc6: https://edugit.org/AlekSIS/Official/AlekSIS/-/tags/2.0rc6
 .. _2.0rc7: https://edugit.org/AlekSIS/Official/AlekSIS/-/tags/2.0rc7
 .. _2.0: https://edugit.org/AlekSIS/Official/AlekSIS/-/tags/2.0
+.. _2.1: https://edugit.org/AlekSIS/Official/AlekSIS/-/tags/2.1
+.. _2.1.1: https://edugit.org/AlekSIS/Official/AlekSIS/-/tags/2.1.1
+.. _2.2: https://edugit.org/AlekSIS/Official/AlekSIS/-/tags/2.2
+.. _2.2.1: https://edugit.org/AlekSIS/Official/AlekSIS/-/tags/2.2.1
+.. _2.3: https://edugit.org/AlekSIS/Official/AlekSIS/-/tags/2.3
+.. _2.3.1: https://edugit.org/AlekSIS/Official/AlekSIS/-/tags/2.3.1
+.. _2.4: https://edugit.org/AlekSIS/Official/AlekSIS/-/tags/2.4
+.. _2.5: https://edugit.org/AlekSIS/Official/AlekSIS/-/tags/2.5
+.. _2.6: https://edugit.org/AlekSIS/Official/AlekSIS/-/tags/2.6
+.. _2.7: https://edugit.org/AlekSIS/Official/AlekSIS/-/tags/2.7
+.. _2.7.1: https://edugit.org/AlekSIS/Official/AlekSIS/-/tags/2.7.1
+.. _2.7.2: https://edugit.org/AlekSIS/Official/AlekSIS/-/tags/2.7.2
+.. _2.7.3: https://edugit.org/AlekSIS/Official/AlekSIS/-/tags/2.7.3
+.. _2.7.4: https://edugit.org/AlekSIS/Official/AlekSIS/-/tags/2.7.4
diff --git a/Dockerfile b/Dockerfile
index a44a0ce8021f06d34e39c7d6c594602cb623f690..acb08f98ffe79dccc32f048590bb123c2763302f 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -1,7 +1,7 @@
 FROM debian:bullseye-slim AS core
 
 # Build arguments
-ARG EXTRAS="ldap,s3"
+ARG EXTRAS="ldap,s3,sentry"
 ARG APP_VERSION=""
 
 # Configure Python to be nice inside Docker and pip to stfu
@@ -19,20 +19,25 @@ ENV ALEKSIS_static__root /usr/share/aleksis/static
 ENV ALEKSIS_media__root /var/lib/aleksis/media
 ENV ALEKSIS_backup__location /var/lib/aleksis/backups
 ENV ALEKSIS_dev__uwsgi__celery false
+ENV PSQL_PAGER=pspg
 
 # Install necessary Debian and PyPI packages for build and runtime
 RUN apt-get -y update && \
-    apt-get -y install eatmydata && \
+    apt-get -y install eatmydata gnupg postgresql-common && \
+    /usr/share/postgresql-common/pgdg/apt.postgresql.org.sh -y && \
     eatmydata apt-get -y upgrade && \
     eatmydata apt-get install -y --no-install-recommends \
         build-essential \
         chromium \
+        curl \
 	dumb-init \
 	gettext \
-	libpq5 \
+        grep \
+        less \
 	libpq-dev \
 	libssl-dev \
-	postgresql-client \
+	postgresql-client-14 \
+        pspg \
 	python3-dev \
 	python3-magic \
 	python3-pip \
@@ -76,6 +81,7 @@ RUN set -e; \
     eatmydata apt-get remove --purge -y \
         build-essential \
         gettext \
+        gnupg \
         libpq-dev \
         libssl-dev \
         libldap2-dev \
diff --git a/LICENCE.rst b/LICENCE.rst
index 3b61c80ee24e4d1cd69db2ddbd6236118850a233..338334c3c2bf3f0bfac4bd807a0b2067c09b2851 100644
--- a/LICENCE.rst
+++ b/LICENCE.rst
@@ -121,7 +121,7 @@ been modified and the date of modification.
 of the Original Works or Derivative Works, this Distribution or
 Communication will be done under the terms of this Licence or of a
 later version of this Licence unless the Original Work is expressly
-distributed only under this version of the Licence — for example by
+distributed only under this version of the Licence — for example by
 communicating ‘EUPL v. 1.2 only’. The Licensee (becoming Licensor)
 cannot offer or impose any additional terms or conditions on the Work
 or Derivative Work that alter or restrict the terms of the Licence.
@@ -306,7 +306,7 @@ Appendix
 * Creative Commons Attribution-ShareAlike v. 3.0 Unported
   (CC BY-SA 3.0) for works other than software
 * European Union Public Licence (EUPL) v. 1.1, v. 1.2
-* Québec Free and Open-Source Licence — Reciprocity (LiLiQ-R)
+* Québec Free and Open-Source Licence — Reciprocity (LiLiQ-R)
   or Strong Reciprocity (LiLiQ-R+)
 
 The European Commission may update this Appendix to later versions of
diff --git a/README.rst b/README.rst
index de1fb1fe17c9e8e57f49ad94987e8847f6263a08..a6732de3b038f5d1d4f5b59d2c05f3c77840c2b9 100644
--- a/README.rst
+++ b/README.rst
@@ -6,7 +6,7 @@ This is the core of the AlekSIS framework and the official distribution
 developers and administrators.
 
 If you are looking for the AlekSIS standard distribution, i.e. the complete
-software product ready for installation and usage, please visit the `AlekSIS`_
+software product ready for installation and usage, please visit the `AlekSIS®`_
 website or the distribution repository on `EduGit`_.
 
 Features
@@ -16,58 +16,61 @@ The AlekSIS core currently provides the following features:
 
 * For users:
 
- * Authentication via OAuth applications
- * Configurable dashboard
- * Custom menu entries (e.g. in footer)
- * Global preferences
+ * Authentication via local account, LDAP, or social accounts
+ * Two factor authentication via Yubikey, OTP or SMS
+ * Configurable dashboard with widgets
+ * User-specific preferences
  * Global search
- * Group types
  * Manage announcements
- * Manage groups
+ * Manage groups and types of groups
+ * Manage roles and additional, informative fields per group
  * Manage persons
- * Notifications via SMS email or dashboard
+ * Notifications via SMS, email or dashboard
  * PWA with offline caching
- * Rules and permissions for users, objects and pages
- * Two factor authentication via Yubikey, OTP or SMS
- * User preferences
  * User registration, password changes and password reset
+ * User invitations with invite codes and targeted invites
 
 * For admins
 
- * Asynchronous tasks with celery
- * Authentication via LDAP
+ * `aleksis-admin` script to wrap django-admin with pre-configured settings
+ * Manage school terms
+ * Custom menu entries (e.g. in footer)
  * Automatic backup of database, static and media files
- * Generic PDF generation with chromium
  * OAuth2 and OpenID Connect provider support
  * Serve prometheus metrics
  * System health and data checks
+ * Configuration of low-level settings via configuration files
+ * System-wide preferenes
+ * Creating dashboard widgets for external links/apps
 
 * For developers
 
- * `aleksis-admin` script to wrap django-admin with pre-configured settings
+ * Generic PDF generation with chromium
  * Caching with Redis
  * Django REST framework for apps to use at own discretion
  * Injection of fields, methods, permissions and properties via custom `ExtensibleModel`
  * K8s compatible, read-only Docker image
  * Object-level permissions and rules with `django-guardian` and `django-rules`
  * Query caching with `django-cachalot`
- * Search with `django-haystack` and `Whoosh` backend
  * uWSGI and Celery via `django-uwsgi` in development
+ * Extensible dashbaord widget system
+ * Extensible OAuth/OpenID Connect scope and claims system
 
 Licence
 -------
 
 ::
 
-  Copyright © 2017, 2018, 2019, 2020, 2021 Jonathan Weth <dev@jonathanweth.de>
+  Copyright © 2017, 2018, 2019, 2020, 2021, 2022 Jonathan Weth <dev@jonathanweth.de>
   Copyright © 2017, 2018, 2019, 2020 Frank Poetzsch-Heffter <p-h@katharineum.de>
+  Copyright © 2018, 2019, 2020, 2021, 2022 Hangzhi Yu <yuha@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, 2020, 2021, 2022 Dominik George <dominik.george@teckids.org>
+  Copyright © 2019, 2020, 2021, 2022 Tom Teichler <tom.teichler@teckids.org>
   Copyright © 2019 mirabilos <thorsten.glaser@teckids.org>
   Copyright © 2021 Lloyd Meins <meinsll@katharineum.de>
   Copyright © 2021 magicfelix <felix@felix-zauberer.de>
+  Copyright © 2022 Benedict Suska <benedict.suska@teckids.org>
 
   Licenced under the EUPL, version 1.2 or later, by Teckids e.V. (Bonn, Germany).
 
@@ -76,6 +79,14 @@ full licence text or on the `European Union Public Licence`_ website
 https://joinup.ec.europa.eu/collection/eupl/guidelines-users-and-developers
 (including all other official language versions).
 
-.. _AlekSIS: https://aleksis.org
+Trademark
+---------
+
+AlekSIS® is a registered trademark of the AlekSIS open source project, represented
+by Teckids e.V. Please refer to the `trademark policy`_ for hints on using the trademark
+AlekSIS®.
+
+.. _AlekSIS®: https://aleksis.org
 .. _European Union Public Licence: https://eupl.eu/
 .. _EduGit: https://edugit.org/AlekSIS/official/AlekSIS
+.. _trademark policy: https://aleksis.org/pages/about
diff --git a/aleksis/core/__init__.py b/aleksis/core/__init__.py
index 79928246f83965c5c39479326114e3a7a89e27cb..66d1ef788b034aea3d1518bc009e5be0ca05b3f2 100644
--- a/aleksis/core/__init__.py
+++ b/aleksis/core/__init__.py
@@ -1,10 +1,6 @@
 from importlib import metadata
 
-try:
-    from .celery import app as celery_app
-except ModuleNotFoundError:
-    # Celery is not available
-    celery_app = None
+from .celery import app as celery_app  # noqa
 
 try:
     __version__ = metadata.distribution("AlekSIS-Core").version
diff --git a/aleksis/core/apps.py b/aleksis/core/apps.py
index b549995264811d731cb61a8ed934e579dcefc33e..77e4b2a6327d9b88254b160375b1ce85be73ed68 100644
--- a/aleksis/core/apps.py
+++ b/aleksis/core/apps.py
@@ -9,6 +9,7 @@ from django.utils.translation import gettext as _
 
 from dynamic_preferences.registries import preference_models
 from health_check.plugins import plugin_dir
+from oauthlib.common import Request as OauthlibRequest
 
 from .registries import (
     group_preferences_registry,
@@ -16,7 +17,12 @@ from .registries import (
     site_preferences_registry,
 )
 from .util.apps import AppConfig
-from .util.core_helpers import get_or_create_favicon, has_person
+from .util.core_helpers import (
+    create_default_celery_schedule,
+    get_or_create_favicon,
+    get_site_preferences,
+    has_person,
+)
 from .util.sass_helpers import clean_scss
 
 
@@ -30,15 +36,16 @@ class CoreConfig(AppConfig):
     }
     licence = "EUPL-1.2+"
     copyright_info = (
-        ([2017, 2018, 2019, 2020, 2021], "Jonathan Weth", "wethjo@katharineum.de"),
+        ([2017, 2018, 2019, 2020, 2021, 2022], "Jonathan Weth", "wethjo@katharineum.de"),
         ([2017, 2018, 2019, 2020], "Frank Poetzsch-Heffter", "p-h@katharineum.de"),
+        ([2018, 2019, 2020, 2021, 2022], "Hangzhi Yu", "yuha@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, 2020, 2021, 2022], "Dominik George", "dominik.george@teckids.org"),
+        ([2019, 2020, 2021, 2022], "Tom Teichler", "tom.teichler@teckids.org"),
         ([2019], "mirabilos", "thorsten.glaser@teckids.org"),
         ([2021], "Lloyd Meins", "meinsll@katharineum.de"),
         ([2021], "magicfelix", "felix@felix-zauberer.de"),
+        ([2022], "Benedict Suska", "benedict.suska@teckids.org"),
     )
 
     def ready(self):
@@ -47,7 +54,7 @@ class CoreConfig(AppConfig):
         from django.conf import settings  # noqa
 
         # Autodiscover various modules defined by AlekSIS
-        autodiscover_modules("form_extensions", "model_extensions", "checks")
+        autodiscover_modules("model_extensions", "form_extensions", "checks")
 
         sitepreferencemodel = self.get_model("SitePreferenceModel")
         personpreferencemodel = self.get_model("PersonPreferenceModel")
@@ -96,14 +103,20 @@ class CoreConfig(AppConfig):
             if name in ("primary", "secondary"):
                 clean_scss()
             elif name in ("favicon", "pwa_icon"):
-                from favicon.models import Favicon  # noqa
+                from favicon.models import Favicon, FaviconImg  # noqa
 
                 is_favicon = name == "favicon"
 
                 if new_value:
-                    Favicon.on_site.update_or_create(
-                        title=name, defaults={"isFavicon": is_favicon, "faviconImage": new_value},
-                    )
+                    # Get file object from preferences instead of using new_value
+                    # to prevent problems with special file storages
+                    file_obj = get_site_preferences()[f"{section}__{name}"]
+
+                    favicon = Favicon.on_site.update_or_create(
+                        title=name,
+                        defaults={"isFavicon": is_favicon, "faviconImage": file_obj},
+                    )[0]
+                    FaviconImg.objects.filter(faviconFK=favicon).delete()
                 else:
                     Favicon.on_site.filter(title=name, isFavicon=is_favicon).delete()
                     if name in settings.DEFAULT_FAVICON_PATHS:
@@ -132,6 +145,9 @@ class CoreConfig(AppConfig):
         for name, default in settings.DEFAULT_FAVICON_PATHS.items():
             get_or_create_favicon(name, default, is_favicon=name == "favicon")
 
+        # Create default periodic tasks
+        create_default_celery_schedule()
+
     def user_logged_in(
         self, sender: type, request: Optional[HttpRequest], user: "User", **kwargs
     ) -> None:
@@ -152,5 +168,52 @@ class CoreConfig(AppConfig):
                 "address": _("Full home postal address"),
                 "email": _("Email address"),
                 "phone": _("Home and mobile phone"),
+                "groups": _("Groups"),
             }
         return scopes
+
+    @classmethod
+    def get_additional_claims(cls, scopes: list[str], request: OauthlibRequest) -> dict[str, Any]:
+        django_request = HttpRequest()
+        django_request.META = request.headers
+
+        claims = {
+            "preferred_username": request.user.username,
+        }
+
+        if "profile" in scopes:
+            if has_person(request.user):
+                claims["given_name"] = request.user.person.first_name
+                claims["family_name"] = request.user.person.last_name
+                claims["profile"] = django_request.build_absolute_uri(
+                    request.user.person.get_absolute_url()
+                )
+                if request.user.person.photo:
+                    claims["picture"] = django_request.build_absolute_uri(
+                        request.user.person.photo.url
+                    )
+            else:
+                claims["given_name"] = request.user.first_name
+                claims["family_name"] = request.user.last_name
+
+        if "email" in scopes:
+            if has_person(request.user):
+                claims["email"] = request.user.person.email
+            else:
+                claims["email"] = request.user.email
+
+        if "address" in scopes and has_person(request.user):
+            claims["address"] = {
+                "street_address": request.user.person.street
+                + " "
+                + request.user.person.housenumber,
+                "locality": request.user.person.place,
+                "postal_code": request.user.person.postal_code,
+            }
+
+        if "groups" in scopes and has_person(request.user):
+            claims["groups"] = list(
+                request.user.person.member_of.values_list("name", flat=True).all()
+            )
+
+        return claims
diff --git a/aleksis/core/celery.py b/aleksis/core/celery.py
index 27a67c539babfc61701cc9521b42187ebebc5787..ab78cfb080ad1abc79f92e873641ef36c22432c9 100644
--- a/aleksis/core/celery.py
+++ b/aleksis/core/celery.py
@@ -1,9 +1,37 @@
 import os
+from traceback import format_exception
+
+from django.conf import settings
 
 from celery import Celery
+from celery.signals import task_failure
+
+from .util.core_helpers import get_site_preferences
+from .util.email import send_email
 
 os.environ.setdefault("DJANGO_SETTINGS_MODULE", "aleksis.core.settings")
 
 app = Celery("aleksis")  # noqa
 app.config_from_object("django.conf:settings", namespace="CELERY")
 app.autodiscover_tasks()
+
+
+@task_failure.connect
+def task_failure_notifier(
+    sender=None, task_id=None, exception=None, args=None, kwargs=None, traceback=None, **__
+):
+    recipient_list = [e[1] for e in settings.ADMINS]
+    send_email(
+        template_name="celery_failure",
+        from_email=get_site_preferences()["mail__address"],
+        recipient_list=recipient_list,
+        context={
+            "task_name": sender.name,
+            "task": str(sender),
+            "task_id": str(task_id),
+            "exception": str(exception),
+            "args": args,
+            "kwargs": kwargs,
+            "traceback": "".join(format_exception(type(exception), exception, traceback)),
+        },
+    )
diff --git a/aleksis/core/data_checks.py b/aleksis/core/data_checks.py
index 234c82c0ca12b46c7e1aebd7151fb2f2a1c0c2ca..d82c30d5ddf0b5ab11e7b1b5f8c2a595838c07e1 100644
--- a/aleksis/core/data_checks.py
+++ b/aleksis/core/data_checks.py
@@ -1,4 +1,5 @@
 import logging
+from datetime import timedelta
 
 from django.apps import apps
 from django.contrib.contenttypes.models import ContentType
@@ -8,10 +9,10 @@ from django.utils.translation import gettext as _
 
 import reversion
 from reversion import set_comment
-from templated_email import send_templated_mail
 
 from .util.celery_progress import ProgressRecorder, recorded_task
 from .util.core_helpers import get_site_preferences
+from .util.email import send_email
 
 
 class SolveOption:
@@ -236,7 +237,7 @@ class DataCheckRegistry:
         return [(check.name, check.verbose_name) for check in cls.data_checks]
 
 
-@recorded_task
+@recorded_task(run_every=timedelta(minutes=15))
 def check_data(recorder: ProgressRecorder):
     """Execute all registered data checks and send email if activated."""
     for check in recorder.iterate(DataCheckRegistry.data_checks):
@@ -274,9 +275,8 @@ def send_emails_for_data_checks():
         for group in get_site_preferences()["general__data_checks_recipient_groups"]:
             recipient_list += [p.mail_sender for p in group.announcement_recipients if p.email]
 
-        send_templated_mail(
+        send_email(
             template_name="data_checks",
-            from_email=get_site_preferences()["mail__address"],
             recipient_list=recipient_list,
             context={"results": results_with_checks},
         )
diff --git a/aleksis/core/filters.py b/aleksis/core/filters.py
index aa287690b3d72d7f4e72f1a41708e38401f2b0db..0bccc2664489f3698fc77bbe7299a9210ed81dac 100644
--- a/aleksis/core/filters.py
+++ b/aleksis/core/filters.py
@@ -1,9 +1,14 @@
 from typing import Sequence
 
+from django.contrib.auth.models import Group as DjangoGroup
+from django.contrib.auth.models import Permission, User
+from django.contrib.contenttypes.models import ContentType
 from django.db.models import Q
 from django.utils.translation import gettext as _
 
 from django_filters import CharFilter, FilterSet, ModelChoiceFilter, ModelMultipleChoiceFilter
+from django_select2.forms import ModelSelect2Widget
+from guardian.models import GroupObjectPermission, UserObjectPermission
 from material import Layout, Row
 
 from aleksis.core.models import Group, GroupType, Person, SchoolTerm
@@ -49,6 +54,7 @@ class PersonFilter(FilterSet):
             "additional_name__icontains",
             "last_name__icontains",
             "short_name__icontains",
+            "user__username__icontains",
         ],
         label=_("Search by name"),
     )
@@ -67,8 +73,99 @@ class PersonFilter(FilterSet):
 
     def __init__(self, *args, **kwargs):
         super().__init__(*args, **kwargs)
-        self.form.layout = Layout(Row("name", "contact"), Row("is_active", "sex", "primary_group"))
+        self.form.layout = Layout(Row("name", "contact"), Row("sex", "primary_group"))
 
     class Meta:
         model = Person
-        fields = ["sex", "is_active", "primary_group"]
+        fields = ["sex", "primary_group"]
+
+
+class PermissionFilter(FilterSet):
+    """Common filter for permissions."""
+
+    permission = ModelChoiceFilter(
+        queryset=Permission.objects.all(),
+        widget=ModelSelect2Widget(
+            search_fields=["name__icontains", "codename__icontains"],
+            attrs={"data-minimum-input-length": 0, "class": "browser-default"},
+        ),
+        label=_("Permission"),
+    )
+    permission__content_type = ModelChoiceFilter(
+        queryset=ContentType.objects.all(),
+        widget=ModelSelect2Widget(
+            search_fields=["app_label__icontains", "model__icontains"],
+            attrs={"data-minimum-input-length": 0, "class": "browser-default"},
+        ),
+        label=_("Content type"),
+    )
+
+
+class UserPermissionFilter(PermissionFilter):
+    """Common filter for user permissions."""
+
+    user = ModelChoiceFilter(
+        queryset=User.objects.all(),
+        widget=ModelSelect2Widget(
+            search_fields=["username__icontains", "first_name__icontains", "last_name__icontains"],
+            attrs={"data-minimum-input-length": 0, "class": "browser-default"},
+        ),
+        label=_("User"),
+    )
+
+    def __init__(self, *args, **kwargs):
+        super().__init__(*args, **kwargs)
+        self.form.layout = Layout(Row("user", "permission", "permission__content_type"))
+
+    class Meta:
+        fields = ["user", "permission", "permission__content_type"]
+
+
+class GroupPermissionFilter(PermissionFilter):
+    """Common filter for group permissions."""
+
+    group = ModelChoiceFilter(
+        queryset=DjangoGroup.objects.all(),
+        widget=ModelSelect2Widget(
+            search_fields=[
+                "name__icontains",
+            ],
+            attrs={"data-minimum-input-length": 0, "class": "browser-default"},
+        ),
+        label=_("Group"),
+    )
+
+    def __init__(self, *args, **kwargs):
+        super().__init__(*args, **kwargs)
+        self.form.layout = Layout(Row("group", "permission", "permission__content_type"))
+
+    class Meta:
+        fields = ["group", "permission", "permission__content_type"]
+
+
+class UserGlobalPermissionFilter(UserPermissionFilter):
+    """Filter for global user permissions."""
+
+    class Meta(UserPermissionFilter.Meta):
+        model = User.user_permissions.through
+
+
+class GroupGlobalPermissionFilter(GroupPermissionFilter):
+    """Filter for global group permissions."""
+
+    class Meta(GroupPermissionFilter.Meta):
+        model = DjangoGroup.permissions.through
+
+
+class UserObjectPermissionFilter(UserPermissionFilter):
+    """Filter for object user permissions."""
+
+    class Meta(UserPermissionFilter.Meta):
+        model = UserObjectPermission
+
+
+class GroupObjectPermissionFilter(GroupPermissionFilter):
+    """Filter for object group permissions."""
+
+    class Meta(GroupPermissionFilter.Meta):
+        model = GroupObjectPermission
diff --git a/aleksis/core/forms.py b/aleksis/core/forms.py
index d8f5d0deeb8e9320d5f6f0981975e142399d0b5a..ef4184812f73d22bb1dcfa1ff84f74f722c3c774 100644
--- a/aleksis/core/forms.py
+++ b/aleksis/core/forms.py
@@ -1,18 +1,24 @@
 from datetime import datetime, time
-from typing import Callable, Sequence
+from typing import Any, Callable, Dict, Sequence
 
 from django import forms
 from django.conf import settings
-from django.core.exceptions import ValidationError
+from django.contrib.auth import get_user_model
+from django.contrib.auth.models import Permission
+from django.contrib.sites.models import Site
+from django.core.exceptions import SuspiciousOperation, ValidationError
 from django.db.models import QuerySet
 from django.http import HttpRequest
 from django.utils.translation import gettext_lazy as _
 
 from allauth.account.adapter import get_adapter
 from allauth.account.forms import SignupForm
-from allauth.account.utils import get_user_model, setup_user_email
+from allauth.account.utils import setup_user_email
+from dj_cleavejs import CleaveWidget
 from django_select2.forms import ModelSelect2MultipleWidget, ModelSelect2Widget, Select2Widget
 from dynamic_preferences.forms import PreferenceForm
+from guardian.shortcuts import assign_perm
+from invitations.forms import InviteForm
 from material import Fieldset, Layout, Row
 
 from .mixins import ExtensibleForm, SchoolTermRelatedExtensibleForm
@@ -24,6 +30,7 @@ from .models import (
     GroupType,
     OAuthApplication,
     Person,
+    PersonInvitation,
     SchoolTerm,
 )
 from .registries import (
@@ -31,6 +38,7 @@ from .registries import (
     person_preferences_registry,
     site_preferences_registry,
 )
+from .util.auth_helpers import AppScopes
 from .util.core_helpers import get_site_preferences
 
 
@@ -42,13 +50,16 @@ class PersonForm(ExtensibleForm):
             _("Base data"),
             "short_name",
             Row("user", "primary_group"),
-            "is_active",
             Row("first_name", "additional_name", "last_name"),
         ),
         Fieldset(_("Address"), Row("street", "housenumber"), Row("postal_code", "place")),
         Fieldset(_("Contact data"), "email", Row("phone_number", "mobile_number")),
         Fieldset(
-            _("Advanced personal data"), Row("sex", "date_of_birth"), Row("photo"), "guardians",
+            _("Advanced personal data"),
+            Row("date_of_birth", "place_of_birth"),
+            Row("sex"),
+            Row("photo", "avatar"),
+            "guardians",
         ),
     )
 
@@ -56,7 +67,6 @@ class PersonForm(ExtensibleForm):
         model = Person
         fields = [
             "user",
-            "is_active",
             "first_name",
             "last_name",
             "additional_name",
@@ -69,8 +79,10 @@ class PersonForm(ExtensibleForm):
             "mobile_number",
             "email",
             "date_of_birth",
+            "place_of_birth",
             "sex",
             "photo",
+            "avatar",
             "guardians",
             "primary_group",
         ]
@@ -98,21 +110,16 @@ class PersonForm(ExtensibleForm):
         request = kwargs.pop("request", None)
         super().__init__(*args, **kwargs)
 
-        # Disable non-editable fields
-        allowed_person_fields = get_site_preferences()["account__editable_fields_person"]
-
         if (
             request
             and self.instance
             and not request.user.has_perm("core.change_person", self.instance)
         ):
-            # First, disable all fields
+            # Disable non-editable fields
+            allowed_person_fields = get_site_preferences()["account__editable_fields_person"]
             for field in self.fields:
-                self.fields[field].disabled = True
-
-            # Then, activate allowed fields
-            for field in allowed_person_fields:
-                self.fields[field].disabled = False
+                if field not in allowed_person_fields:
+                    self.fields[field].disabled = True
 
     def clean(self) -> None:
         user = get_user_model()
@@ -147,6 +154,7 @@ class EditGroupForm(SchoolTermRelatedExtensibleForm):
         Fieldset(_("Common data"), "name", "short_name", "group_type"),
         Fieldset(_("Persons"), "members", "owners", "parent_groups"),
         Fieldset(_("Additional data"), "additional_fields"),
+        Fieldset(_("Photo"), "photo", "avatar"),
     )
 
     class Meta:
@@ -174,7 +182,9 @@ class EditGroupForm(SchoolTermRelatedExtensibleForm):
                 attrs={"data-minimum-input-length": 0, "class": "browser-default"},
             ),
             "additional_fields": ModelSelect2MultipleWidget(
-                search_fields=["title__icontains",],
+                search_fields=[
+                    "title__icontains",
+                ],
                 attrs={"data-minimum-input-length": 0, "class": "browser-default"},
             ),
         }
@@ -210,7 +220,10 @@ class AnnouncementForm(ExtensibleForm):
         label=_("Groups"),
         required=False,
         widget=ModelSelect2MultipleWidget(
-            search_fields=["name__icontains", "short_name__icontains",],
+            search_fields=[
+                "name__icontains",
+                "short_name__icontains",
+            ],
             attrs={"data-minimum-input-length": 0, "class": "browser-default"},
         ),
     )
@@ -342,7 +355,7 @@ class EditGroupTypeForm(forms.ModelForm):
 
     class Meta:
         model = GroupType
-        exclude = []
+        fields = ["name", "description"]
 
 
 class SchoolTermForm(ExtensibleForm):
@@ -357,7 +370,8 @@ class SchoolTermForm(ExtensibleForm):
 
 class DashboardWidgetOrderForm(ExtensibleForm):
     pk = forms.ModelChoiceField(
-        queryset=None, widget=forms.HiddenInput(attrs={"class": "pk-input"}),
+        queryset=None,
+        widget=forms.HiddenInput(attrs={"class": "pk-input"}),
     )
     order = forms.IntegerField(initial=0, widget=forms.HiddenInput(attrs={"class": "order-input"}))
 
@@ -377,76 +391,256 @@ DashboardWidgetOrderFormSet = forms.formset_factory(
 )
 
 
-class AccountRegisterForm(SignupForm, ExtensibleForm):
-    """Form to register new user accounts."""
+class InvitationCodeForm(forms.Form):
+    """Form to enter an invitation code."""
 
-    class Meta:
-        model = Group
-        fields = []
+    code = forms.CharField(
+        label=_("Invitation code"),
+        help_text=_("Please enter your invitation code."),
+    )
+
+    def __init__(self, *args, **kwargs):
+        super().__init__(*args, **kwargs)
+
+        # Calculate number of fields
+        length = get_site_preferences()["auth__invite_code_length"]
+        packet_size = get_site_preferences()["auth__invite_code_packet_size"]
+        blocks = [
+            packet_size,
+        ] * length
+
+        self.fields["code"].widget = CleaveWidget(blocks=blocks, delimiter="-", uppercase=True)
+
+
+class PersonCreateInviteForm(InviteForm):
+    """Custom form to create a person and invite them."""
+
+    first_name = forms.CharField(label=_("First name"), required=True)
+    last_name = forms.CharField(label=_("Last name"), required=True)
 
     layout = Layout(
-        Fieldset(_("Base data"), Row("first_name", "last_name"),),
-        Fieldset(
-            _("Account data"), "username", Row("email", "email2"), Row("password1", "password2"),
+        Row("first_name", "last_name"),
+        Row("email"),
+    )
+
+    def clean_email(self):
+        if Person.objects.filter(email=self.cleaned_data["email"]).exists():
+            raise ValidationError(_("A person is using this e-mail address"))
+        return super().clean_email()
+
+    def save(self, email):
+        person = Person.objects.create(
+            first_name=self.cleaned_data["first_name"],
+            last_name=self.cleaned_data["last_name"],
+            email=email,
+        )
+        return PersonInvitation.create(email=email, person=person)
+
+
+class SelectPermissionForm(forms.Form):
+    """Select a permission to assign."""
+
+    selected_permission = forms.ModelChoiceField(
+        queryset=Permission.objects.all(),
+        widget=ModelSelect2Widget(
+            search_fields=["name__icontains", "codename__icontains"],
+            attrs={"data-minimum-input-length": 0, "class": "browser-default"},
         ),
-        Fieldset(_("Consents"), Row("privacy_policy"),),
     )
 
-    def __init__(self, *args, **kwargs):
-        super(AccountRegisterForm, self).__init__(*args, **kwargs)
-        self.fields["password1"] = forms.CharField(label=_("Password"), widget=forms.PasswordInput)
 
-        privacy_policy = get_site_preferences()["footer__privacy_url"]
+class AssignPermissionForm(forms.Form):
+    """Assign a permission to user/groups for all/some objects."""
+
+    layout = Layout(
+        Fieldset(_("Who should get the permission?"), "groups", "persons"),
+        Fieldset(_("On what?"), "objects", "all_objects"),
+    )
+    groups = forms.ModelMultipleChoiceField(
+        queryset=Group.objects.all(),
+        widget=ModelSelect2MultipleWidget(
+            search_fields=["name__icontains", "short_name__icontains"],
+            attrs={"data-minimum-input-length": 0, "class": "browser-default"},
+        ),
+        required=False,
+    )
+    persons = forms.ModelMultipleChoiceField(
+        queryset=Person.objects.all(),
+        widget=ModelSelect2MultipleWidget(
+            search_fields=[
+                "first_name__icontains",
+                "last_name__icontains",
+                "short_name__icontains",
+            ],
+            attrs={"data-minimum-input-length": 0, "class": "browser-default"},
+        ),
+        required=False,
+    )
+
+    objects = forms.ModelMultipleChoiceField(
+        queryset=None,
+        required=False,
+        label=_("Select objects which the permission should be granted for:"),
+    )
+    all_objects = forms.BooleanField(
+        required=False, label=_("Grant the permission for all objects")
+    )
+
+    def clean(self) -> Dict[str, Any]:
+        """Clean form to ensure that at least one target and one type is selected."""
+        cleaned_data = super().clean()
+        if not cleaned_data.get("persons") and not cleaned_data.get("groups"):
+            raise ValidationError(
+                _("You must select at least one group or person which should get the permission.")
+            )
 
-        if settings.SIGNUP_PASSWORD_ENTER_TWICE:
-            self.fields["password2"] = forms.CharField(
-                label=_("Password (again)"), widget=forms.PasswordInput
+        if not cleaned_data.get("objects") and not cleaned_data.get("all_objects"):
+            raise ValidationError(
+                _("You must grant the permission to all objects and/" "or to some objects.")
             )
+        return cleaned_data
 
-        self.fields["first_name"] = forms.CharField(required=True,)
+    def __init__(self, *args, permission: Permission, **kwargs):
+        self.permission = permission
+        super().__init__(*args, **kwargs)
 
-        self.fields["last_name"] = forms.CharField(required=True,)
+        model_class = self.permission.content_type.model_class()
+        if model_class._meta.managed and not model_class._meta.abstract:
+            queryset = model_class.objects.all()
+        else:
+            # The following queryset is just a dummy one. It has no real meaning.
+            # We need it as there are permissions without real objects,
+            # but we want to use the same form.
+            queryset = Site.objects.none()
+        self.fields["objects"].queryset = queryset
+        search_fields = getattr(model_class, "get_filter_fields", lambda: [])()
+
+        # Use select2 only if there are any searchable fields as it can't work without
+        if search_fields:
+            self.fields["objects"].widget = ModelSelect2MultipleWidget(
+                search_fields=search_fields,
+                queryset=queryset,
+                attrs={"data-minimum-input-length": 0, "class": "browser-default"},
+            )
 
-        self.fields["privacy_policy"] = forms.BooleanField(
-            help_text=_(
-                f"I have read the <a href='{privacy_policy}'>Privacy policy</a>"
-                " and agree with them."
-            ),
-            required=True,
-        )
+    def save_perms(self):
+        """Save permissions for selected user/groups and selected/all objects."""
+        persons = self.cleaned_data["persons"]
+        groups = self.cleaned_data["groups"]
+        all_objects = self.cleaned_data["all_objects"]
+        objects = self.cleaned_data["objects"]
+        permission_name = f"{self.permission.content_type.app_label}.{self.permission.codename}"
+        created = 0
+
+        # Create permissions for users
+        for person in persons:
+            if getattr(person, "user", None):
+                # Global permission
+                if all_objects:
+                    assign_perm(permission_name, person.user)
+                # Object permissions
+                for instance in objects:
+                    assign_perm(permission_name, person.user, instance)
+
+        # Create permissions for users
+        for group in groups:
+            django_group = group.django_group
+            # Global permission
+            if all_objects:
+                assign_perm(permission_name, django_group)
+            # Object permissions
+            for instance in objects:
+                assign_perm(permission_name, django_group, instance)
 
-    def clean(self):
-        super(AccountRegisterForm, self).clean()
 
-        dummy_user = get_user_model()
-        password = self.cleaned_data.get("password1")
-        if password:
+class AccountRegisterForm(SignupForm, ExtensibleForm):
+    """Form to register new user accounts."""
+
+    class Meta:
+        model = Person
+        fields = [
+            "first_name",
+            "additional_name",
+            "last_name",
+            "street",
+            "housenumber",
+            "postal_code",
+            "place",
+            "date_of_birth",
+            "place_of_birth",
+            "sex",
+            "photo",
+            "mobile_number",
+            "phone_number",
+            "short_name",
+            "description",
+        ]
+
+    layout = Layout(
+        Fieldset(
+            _("Base data"),
+            Row("first_name", "additional_name", "last_name"),
+            "short_name",
+        ),
+        Fieldset(
+            _("Adress data"),
+            Row("street", "housenumber"),
+            Row("postal_code", "place"),
+        ),
+        Fieldset(_("Contact data"), Row("mobile_number", "phone_number")),
+        Fieldset(
+            _("Additional data"),
+            Row("date_of_birth", "place_of_birth"),
+            Row("sex", "photo"),
+            "description",
+        ),
+        Fieldset(
+            _("Account data"),
+            "username",
+            Row("email", "email2"),
+            Row("password1", "password2"),
+        ),
+    )
+
+    password1 = forms.CharField(label=_("Password"), widget=forms.PasswordInput)
+
+    if settings.SIGNUP_PASSWORD_ENTER_TWICE:
+        password2 = forms.CharField(label=_("Password (again)"), widget=forms.PasswordInput)
+
+    def __init__(self, *args, **kwargs):
+        request = kwargs.pop("request", None)
+        super(AccountRegisterForm, self).__init__(*args, **kwargs)
+
+        if request.session.get("account_verified_email"):
+            email = request.session["account_verified_email"]
+
             try:
-                get_adapter().clean_password(password, user=dummy_user)
-            except forms.ValidationError as e:
-                self.add_error("password1", e)
+                person = Person.objects.get(email=email)
+            except (Person.DoesNotExist, Person.MultipleObjectsReturned):
+                raise SuspiciousOperation()
 
-        if (
-            settings.SIGNUP_PASSWORD_ENTER_TWICE
-            and "password1" in self.cleaned_data
-            and "password2" in self.cleaned_data
-        ):
-            if self.cleaned_data["password1"] != self.cleaned_data["password2"]:
-                self.add_error(
-                    "password2", _("You must type the same password each time."),
-                )
-        return self.cleaned_data
+            self.fields["email"].disabled = True
+            self.fields["email2"].disabled = True
+
+            if person:
+                available_fields = [field.name for field in Person._meta.get_fields()]
+                self.fields["email2"].initial = person.email
+                for field in self.fields:
+                    if field in available_fields and getattr(person, field):
+                        self.fields[field].disabled = True
+                        self.fields[field].initial = getattr(person, field)
 
     def save(self, request):
         adapter = get_adapter(request)
         user = adapter.new_user(request)
         adapter.save_user(request, user, self)
-        Person.objects.create(
-            first_name=self.cleaned_data["first_name"],
-            last_name=self.cleaned_data["last_name"],
-            email=self.cleaned_data["email"],
-            user=user,
-        )
+        # Create person
+        data = {}
+        for field in Person._meta.get_fields():
+            if field.name in self.cleaned_data:
+                data[field.name] = self.cleaned_data[field.name]
+        if not Person.objects.filter(email=data["email"]):
+            _person, created = Person.objects.update_or_create(user=user, **data)
         self.custom_signup(request, user)
         setup_user_email(request, user, [])
         return user
@@ -594,12 +788,22 @@ class ListActionForm(ActionForm):
 
 
 class OAuthApplicationForm(forms.ModelForm):
+    def __init__(self, *args, **kwargs):
+        super().__init__(*args, **kwargs)
+        self.fields["allowed_scopes"].widget = forms.SelectMultiple(
+            choices=list(AppScopes().get_all_scopes().items())
+        )
+
     class Meta:
         model = OAuthApplication
         fields = (
             "name",
+            "icon",
             "client_id",
             "client_secret",
             "client_type",
+            "algorithm",
+            "allowed_scopes",
             "redirect_uris",
+            "skip_authorization",
         )
diff --git a/aleksis/core/health_checks.py b/aleksis/core/health_checks.py
index 200a6eaf0a35a75c29ca4cf7e8c564925b1e29e8..555a67cfed731f6853c0ee0c52cb9d72d33882b8 100644
--- a/aleksis/core/health_checks.py
+++ b/aleksis/core/health_checks.py
@@ -33,10 +33,13 @@ class BaseBackupHealthCheck(BaseHealthCheckBackend):
 
     def check_status(self):
         storage = get_storage()
-        backups = storage.list_backups(content_type=self.content_type)
-        if not storage.storage.exists(""):
-            self.add_error(_("The backup folder doesn't exist."))
+
+        try:
+            backups = storage.list_backups(content_type=self.content_type)
+        except Exception as ex:
+            self.add_error(_("Error accessing backup storage: {}").format(str(ex)))
             return
+
         if backups:
             last_backup = backups[:1]
             last_backup_time = dbbackup_utils.filename_to_date(last_backup[0])
@@ -44,7 +47,7 @@ class BaseBackupHealthCheck(BaseHealthCheckBackend):
 
             # Check if backup is older than configured time
             if time_gone_since_backup.seconds > self.configured_seconds:
-                self.add_error(_(f"Last backup {time_gone_since_backup}!"))
+                self.add_error(_("Last backup {}!").format(time_gone_since_backup))
         else:
             self.add_error(_("No backup found!"))
 
@@ -75,4 +78,4 @@ class BackupJobHealthCheck(BaseHealthCheckBackend):
         if not task:
             self.add_error(_("No backup result found!"))
         elif task and task.status != "SUCCESS":
-            self.add_error(_(f"{task.status} - {task.result}"))
+            self.add_error(f"{task.status} - {task.result}")
diff --git a/aleksis/core/locale/ar/LC_MESSAGES/django.po b/aleksis/core/locale/ar/LC_MESSAGES/django.po
index 29ee921af8694936e102e32d65b310ac44979fbc..a382635f245835c9c747b72cb4b661bd39eb7e04 100644
--- a/aleksis/core/locale/ar/LC_MESSAGES/django.po
+++ b/aleksis/core/locale/ar/LC_MESSAGES/django.po
@@ -8,7 +8,7 @@ msgid ""
 msgstr ""
 "Project-Id-Version: AlekSIS (School Information System) 0.1\n"
 "Report-Msgid-Bugs-To: \n"
-"POT-Creation-Date: 2021-10-28 16:18+0200\n"
+"POT-Creation-Date: 2022-02-08 23:16+0000\n"
 "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
 "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
 "Language-Team: LANGUAGE <LL@li.org>\n"
@@ -18,31 +18,38 @@ msgstr ""
 "Content-Transfer-Encoding: 8bit\n"
 "Plural-Forms: nplurals=6; plural=n==0 ? 0 : n==1 ? 1 : n==2 ? 2 : n%100>=3 && n%100<=10 ? 3 : n%100>=11 && n%100<=99 ? 4 : 5;\n"
 
-#: aleksis/core/apps.py:150
+#: aleksis/core/apps.py:166
 msgid "OpenID Connect scope"
 msgstr ""
 
-#: aleksis/core/apps.py:151
+#: aleksis/core/apps.py:167
 msgid "Given name, family name, link to profile and picture if existing."
 msgstr ""
 
-#: aleksis/core/apps.py:152
+#: aleksis/core/apps.py:168
 msgid "Full home postal address"
 msgstr ""
 
-#: aleksis/core/apps.py:153
+#: aleksis/core/apps.py:169
 msgid "Email address"
 msgstr ""
 
-#: aleksis/core/apps.py:154
+#: aleksis/core/apps.py:170
 msgid "Home and mobile phone"
 msgstr ""
 
-#: aleksis/core/data_checks.py:55
+#: aleksis/core/apps.py:171 aleksis/core/forms.py:220 aleksis/core/menus.py:265
+#: aleksis/core/models.py:462 aleksis/core/templates/core/group/list.html:8
+#: aleksis/core/templates/core/group/list.html:9
+#: aleksis/core/templates/core/person/full.html:250
+msgid "Groups"
+msgstr ""
+
+#: aleksis/core/data_checks.py:56
 msgid "Ignore problem"
 msgstr ""
 
-#: aleksis/core/data_checks.py:184
+#: aleksis/core/data_checks.py:185
 #, python-brace-format
 msgid "Solve option '{solve_option_obj.verbose_name}' "
 msgstr ""
@@ -59,8 +66,8 @@ msgstr ""
 msgid "The DashboardWidget was reported broken automatically."
 msgstr ""
 
-#: aleksis/core/filters.py:37 aleksis/core/templates/core/base.html:83
-#: aleksis/core/templates/core/base.html:84
+#: aleksis/core/filters.py:42 aleksis/core/templates/core/base.html:105
+#: aleksis/core/templates/core/base.html:106
 #: aleksis/core/templates/core/group/list.html:20
 #: aleksis/core/templates/core/person/list.html:24
 #: aleksis/core/templates/search/search.html:7
@@ -68,132 +75,181 @@ msgstr ""
 msgid "Search"
 msgstr ""
 
-#: aleksis/core/filters.py:53
+#: aleksis/core/filters.py:59
 msgid "Search by name"
 msgstr ""
 
-#: aleksis/core/filters.py:65
+#: aleksis/core/filters.py:71
 msgid "Search by contact details"
 msgstr ""
 
-#: aleksis/core/forms.py:41 aleksis/core/forms.py:387
+#: aleksis/core/filters.py:92
+msgid "Permission"
+msgstr ""
+
+#: aleksis/core/filters.py:100
+msgid "Content type"
+msgstr ""
+
+#: aleksis/core/filters.py:113 aleksis/core/models.py:688
+msgid "User"
+msgstr ""
+
+#: aleksis/core/filters.py:135 aleksis/core/models.py:461
+msgid "Group"
+msgstr ""
+
+#: aleksis/core/forms.py:50 aleksis/core/forms.py:581
 msgid "Base data"
 msgstr ""
 
-#: aleksis/core/forms.py:47
+#: aleksis/core/forms.py:55
 msgid "Address"
 msgstr ""
 
-#: aleksis/core/forms.py:48
+#: aleksis/core/forms.py:56 aleksis/core/forms.py:590
 msgid "Contact data"
 msgstr ""
 
-#: aleksis/core/forms.py:50
+#: aleksis/core/forms.py:58
 msgid "Advanced personal data"
 msgstr ""
 
-#: aleksis/core/forms.py:93
+#: aleksis/core/forms.py:106
 msgid "New user"
 msgstr ""
 
-#: aleksis/core/forms.py:93
+#: aleksis/core/forms.py:106
 msgid "Create a new account"
 msgstr ""
 
-#: aleksis/core/forms.py:124
+#: aleksis/core/forms.py:132
 msgid "You cannot set a new username when also selecting an existing user."
 msgstr ""
 
-#: aleksis/core/forms.py:128
+#: aleksis/core/forms.py:136
 msgid "This username is already in use."
 msgstr ""
 
-#: aleksis/core/forms.py:145 aleksis/core/models.py:117
+#: aleksis/core/forms.py:153 aleksis/core/models.py:130
 msgid "School term"
 msgstr ""
 
-#: aleksis/core/forms.py:146
+#: aleksis/core/forms.py:154
 msgid "Common data"
 msgstr ""
 
-#: aleksis/core/forms.py:147 aleksis/core/forms.py:196
-#: aleksis/core/menus.py:238 aleksis/core/models.py:140
+#: aleksis/core/forms.py:155 aleksis/core/forms.py:207
+#: aleksis/core/menus.py:254 aleksis/core/models.py:153
 #: aleksis/core/templates/core/person/list.html:8
 #: aleksis/core/templates/core/person/list.html:9
 msgid "Persons"
 msgstr ""
 
-#: aleksis/core/forms.py:148
+#: aleksis/core/forms.py:156 aleksis/core/forms.py:592
 msgid "Additional data"
 msgstr ""
 
-#: aleksis/core/forms.py:188 aleksis/core/forms.py:191
-#: aleksis/core/models.py:60
-msgid "Date"
+#: aleksis/core/forms.py:157 aleksis/core/models.py:206
+#: aleksis/core/models.py:514
+msgid "Photo"
 msgstr ""
 
-#: aleksis/core/forms.py:189 aleksis/core/forms.py:192
-#: aleksis/core/models.py:68
-msgid "Time"
+#: aleksis/core/forms.py:199 aleksis/core/forms.py:202
+#: aleksis/core/models.py:73
+msgid "Date"
 msgstr ""
 
-#: aleksis/core/forms.py:209 aleksis/core/menus.py:249
-#: aleksis/core/models.py:398 aleksis/core/templates/core/group/list.html:8
-#: aleksis/core/templates/core/group/list.html:9
-#: aleksis/core/templates/core/person/full.html:144
-msgid "Groups"
+#: aleksis/core/forms.py:200 aleksis/core/forms.py:203
+#: aleksis/core/models.py:81
+msgid "Time"
 msgstr ""
 
-#: aleksis/core/forms.py:219
+#: aleksis/core/forms.py:233
 msgid "From when until when should the announcement be displayed?"
 msgstr ""
 
-#: aleksis/core/forms.py:222
+#: aleksis/core/forms.py:236
 msgid "Who should see the announcement?"
 msgstr ""
 
-#: aleksis/core/forms.py:223
+#: aleksis/core/forms.py:237
 msgid "Write your announcement:"
 msgstr ""
 
-#: aleksis/core/forms.py:262
+#: aleksis/core/forms.py:276
 msgid "You are not allowed to create announcements which are only valid in the past."
 msgstr ""
 
-#: aleksis/core/forms.py:266
+#: aleksis/core/forms.py:280
 msgid "The from date and time must be earlier then the until date and time."
 msgstr ""
 
-#: aleksis/core/forms.py:275
+#: aleksis/core/forms.py:289
 msgid "You need at least one recipient."
 msgstr ""
 
-#: aleksis/core/forms.py:389
-msgid "Account data"
+#: aleksis/core/forms.py:398
+msgid "Invitation code"
 msgstr ""
 
-#: aleksis/core/forms.py:391
-msgid "Consents"
+#: aleksis/core/forms.py:399
+msgid "Please enter your invitation code."
 msgstr ""
 
-#: aleksis/core/forms.py:396
-msgid "Password"
+#: aleksis/core/forms.py:418 aleksis/core/models.py:181
+msgid "First name"
 msgstr ""
 
-#: aleksis/core/forms.py:402
-msgid "Password (again)"
+#: aleksis/core/forms.py:419 aleksis/core/models.py:182
+msgid "Last name"
 msgstr ""
 
-#: aleksis/core/forms.py:411
-#, python-brace-format
-msgid "I have read the <a href='{privacy_policy}'>Privacy policy</a> and agree with them."
+#: aleksis/core/forms.py:428
+msgid "A person is using this e-mail address"
+msgstr ""
+
+#: aleksis/core/forms.py:456
+msgid "Who should get the permission?"
+msgstr ""
+
+#: aleksis/core/forms.py:457
+msgid "On what?"
+msgstr ""
+
+#: aleksis/core/forms.py:483
+msgid "Select objects which the permission should be granted for:"
+msgstr ""
+
+#: aleksis/core/forms.py:486
+msgid "Grant the permission for all objects"
+msgstr ""
+
+#: aleksis/core/forms.py:494
+msgid "You must select at least one group or person which should get the permission."
+msgstr ""
+
+#: aleksis/core/forms.py:499
+msgid "You must grant the permission to all objects and/or to some objects."
 msgstr ""
 
-#: aleksis/core/forms.py:435
-msgid "You must type the same password each time."
+#: aleksis/core/forms.py:586
+msgid "Adress data"
 msgstr ""
 
-#: aleksis/core/forms.py:580
+#: aleksis/core/forms.py:598
+msgid "Account data"
+msgstr ""
+
+#: aleksis/core/forms.py:605
+msgid "Password"
+msgstr ""
+
+#: aleksis/core/forms.py:608
+msgid "Password (again)"
+msgstr ""
+
+#: aleksis/core/forms.py:775
 msgid "No valid selection."
 msgstr ""
 
@@ -201,67 +257,65 @@ msgstr ""
 msgid "There are unresolved data problems."
 msgstr ""
 
-#: aleksis/core/health_checks.py:38
-msgid "The backup folder doesn't exist."
+#: aleksis/core/health_checks.py:40
+msgid "Error accessing backup storage: {}"
 msgstr ""
 
-#: aleksis/core/health_checks.py:47
-#, python-brace-format
-msgid "Last backup {time_gone_since_backup}!"
+#: aleksis/core/health_checks.py:50
+msgid "Last backup {}!"
 msgstr ""
 
-#: aleksis/core/health_checks.py:49
+#: aleksis/core/health_checks.py:52
 msgid "No backup found!"
 msgstr ""
 
-#: aleksis/core/health_checks.py:76
+#: aleksis/core/health_checks.py:79
 msgid "No backup result found!"
 msgstr ""
 
-#: aleksis/core/health_checks.py:78
-#, python-brace-format
-msgid "{task.status} - {task.result}"
-msgstr ""
-
 #: aleksis/core/menus.py:9 aleksis/core/templates/two_factor/core/login.html:6
-#: aleksis/core/templates/two_factor/core/login.html:22
-#: aleksis/core/templates/two_factor/core/login.html:76
+#: aleksis/core/templates/two_factor/core/login.html:32
+#: aleksis/core/templates/two_factor/core/login.html:95
 msgid "Login"
 msgstr ""
 
-#: aleksis/core/menus.py:15 aleksis/core/templates/account/signup.html:20
+#: aleksis/core/menus.py:15 aleksis/core/templates/account/signup.html:22
 #: aleksis/core/templates/socialaccount/signup.html:23
 msgid "Sign up"
 msgstr ""
 
-#: aleksis/core/menus.py:24
+#: aleksis/core/menus.py:24 aleksis/core/templates/invitations/enter.html:7
+msgid "Accept invitation"
+msgstr ""
+
+#: aleksis/core/menus.py:33
 msgid "Dashboard"
 msgstr ""
 
-#: aleksis/core/menus.py:32 aleksis/core/models.py:605
-#: aleksis/core/preferences.py:26
+#: aleksis/core/menus.py:41 aleksis/core/models.py:734
+#: aleksis/core/preferences.py:29
 #: aleksis/core/templates/core/notifications.html:4
 #: aleksis/core/templates/core/notifications.html:5
 msgid "Notifications"
 msgstr ""
 
-#: aleksis/core/menus.py:41
+#: aleksis/core/menus.py:53
 msgid "Account"
 msgstr ""
 
-#: aleksis/core/menus.py:48
+#: aleksis/core/menus.py:60
 msgid "Stop impersonation"
 msgstr ""
 
-#: aleksis/core/menus.py:57 aleksis/core/templates/core/base.html:62
+#: aleksis/core/menus.py:69 aleksis/core/templates/core/base.html:80
 msgid "Logout"
 msgstr ""
 
-#: aleksis/core/menus.py:63
+#: aleksis/core/menus.py:75
 msgid "2FA"
 msgstr ""
 
-#: aleksis/core/menus.py:69
+#: aleksis/core/menus.py:83
 #: aleksis/core/templates/account/password_change.html:5
 #: aleksis/core/templates/account/password_change.html:6
 #: aleksis/core/templates/account/password_change.html:19
@@ -273,847 +327,875 @@ msgstr ""
 msgid "Change password"
 msgstr ""
 
-#: aleksis/core/menus.py:81
+#: aleksis/core/menus.py:95
 msgid "Me"
 msgstr ""
 
-#: aleksis/core/menus.py:90
+#: aleksis/core/menus.py:104
 #: aleksis/core/templates/dynamic_preferences/form.html:5
 msgid "Preferences"
 msgstr ""
 
-#: aleksis/core/menus.py:99
+#: aleksis/core/menus.py:113
 msgid "Third-party accounts"
 msgstr ""
 
-#: aleksis/core/menus.py:108
+#: aleksis/core/menus.py:122
 #: aleksis/core/templates/oauth2_provider/authorized-tokens.html:5
 #: aleksis/core/templates/oauth2_provider/authorized-tokens.html:6
 msgid "Authorized applications"
 msgstr ""
 
-#: aleksis/core/menus.py:119
+#: aleksis/core/menus.py:133
 msgid "Admin"
 msgstr ""
 
-#: aleksis/core/menus.py:127 aleksis/core/models.py:704
+#: aleksis/core/menus.py:141 aleksis/core/models.py:834
 #: aleksis/core/templates/core/announcement/list.html:7
 #: aleksis/core/templates/core/announcement/list.html:8
 msgid "Announcements"
 msgstr ""
 
-#: aleksis/core/menus.py:138 aleksis/core/models.py:118
+#: aleksis/core/menus.py:152 aleksis/core/models.py:131
 #: aleksis/core/templates/core/school_term/list.html:8
 #: aleksis/core/templates/core/school_term/list.html:9
 msgid "School terms"
 msgstr ""
 
-#: aleksis/core/menus.py:149
+#: aleksis/core/menus.py:163
 #: aleksis/core/templates/core/dashboard_widget/list.html:8
 #: aleksis/core/templates/core/dashboard_widget/list.html:9
 msgid "Dashboard widgets"
 msgstr ""
 
-#: aleksis/core/menus.py:160
+#: aleksis/core/menus.py:174
 #: aleksis/core/templates/core/management/data_management.html:6
 #: aleksis/core/templates/core/management/data_management.html:7
 msgid "Data management"
 msgstr ""
 
-#: aleksis/core/menus.py:171
+#: aleksis/core/menus.py:185
 #: aleksis/core/templates/core/pages/system_status.html:5
 #: aleksis/core/templates/core/pages/system_status.html:7
 msgid "System status"
 msgstr ""
 
-#: aleksis/core/menus.py:182
-msgid "Impersonation"
-msgstr ""
-
-#: aleksis/core/menus.py:193
+#: aleksis/core/menus.py:196
 msgid "Configuration"
 msgstr ""
 
-#: aleksis/core/menus.py:204 aleksis/core/templates/core/data_check/list.html:9
+#: aleksis/core/menus.py:207 aleksis/core/templates/core/data_check/list.html:9
 #: aleksis/core/templates/core/data_check/list.html:10
 msgid "Data checks"
 msgstr ""
 
-#: aleksis/core/menus.py:210
+#: aleksis/core/menus.py:213 aleksis/core/templates/core/perms/list.html:13
+#: aleksis/core/templates/core/perms/list.html:14
+msgid "Manage permissions"
+msgstr ""
+
+#: aleksis/core/menus.py:224
 msgid "Backend Admin"
 msgstr ""
 
-#: aleksis/core/menus.py:216
-#: aleksis/core/templates/oauth2_provider/application_detail.html:5
-#: aleksis/core/templates/oauth2_provider/application_list.html:5
+#: aleksis/core/menus.py:232
+#: aleksis/core/templates/oauth2_provider/application/list.html:5
+#: aleksis/core/templates/oauth2_provider/application/list.html:6
 msgid "OAuth2 Applications"
 msgstr ""
 
-#: aleksis/core/menus.py:229
+#: aleksis/core/menus.py:245
 msgid "People"
 msgstr ""
 
-#: aleksis/core/menus.py:260 aleksis/core/models.py:958
+#: aleksis/core/menus.py:276 aleksis/core/models.py:1055
 #: aleksis/core/templates/core/group_type/list.html:8
 #: aleksis/core/templates/core/group_type/list.html:9
 msgid "Group types"
 msgstr ""
 
-#: aleksis/core/menus.py:271
+#: aleksis/core/menus.py:287
 msgid "Groups and child groups"
 msgstr ""
 
-#: aleksis/core/menus.py:282 aleksis/core/models.py:446
+#: aleksis/core/menus.py:298 aleksis/core/models.py:510
 #: aleksis/core/templates/core/additional_field/list.html:8
 #: aleksis/core/templates/core/additional_field/list.html:9
 msgid "Additional fields"
 msgstr ""
 
-#: aleksis/core/menus.py:297
+#: aleksis/core/menus.py:309
+msgid "Invite person"
+msgstr ""
+
+#: aleksis/core/menus.py:322
 #: aleksis/core/templates/core/group/child_groups.html:7
 #: aleksis/core/templates/core/group/child_groups.html:9
 msgid "Assign child groups to groups"
 msgstr ""
 
-#: aleksis/core/mixins.py:498
+#: aleksis/core/mixins.py:511
 msgid "Linked school term"
 msgstr ""
 
-#: aleksis/core/models.py:58
+#: aleksis/core/models.py:71
 msgid "Boolean (Yes/No)"
 msgstr ""
 
-#: aleksis/core/models.py:59
+#: aleksis/core/models.py:72
 msgid "Text (one line)"
 msgstr ""
 
-#: aleksis/core/models.py:61
+#: aleksis/core/models.py:74
 msgid "Date and time"
 msgstr ""
 
-#: aleksis/core/models.py:62
+#: aleksis/core/models.py:75
 msgid "Decimal number"
 msgstr ""
 
-#: aleksis/core/models.py:63 aleksis/core/models.py:186
+#: aleksis/core/models.py:76 aleksis/core/models.py:199
 msgid "E-mail address"
 msgstr ""
 
-#: aleksis/core/models.py:64
+#: aleksis/core/models.py:77
 msgid "Integer"
 msgstr ""
 
-#: aleksis/core/models.py:65
+#: aleksis/core/models.py:78
 msgid "IP address"
 msgstr ""
 
-#: aleksis/core/models.py:66
+#: aleksis/core/models.py:79
 msgid "Boolean or empty (Yes/No/Neither)"
 msgstr ""
 
-#: aleksis/core/models.py:67
+#: aleksis/core/models.py:80
 msgid "Text (multi-line)"
 msgstr ""
 
-#: aleksis/core/models.py:69
+#: aleksis/core/models.py:82
 msgid "URL / Link"
 msgstr ""
 
-#: aleksis/core/models.py:81 aleksis/core/models.py:927
+#: aleksis/core/models.py:94 aleksis/core/models.py:1024
 msgid "Name"
 msgstr ""
 
-#: aleksis/core/models.py:83
+#: aleksis/core/models.py:96
 msgid "Start date"
 msgstr ""
 
-#: aleksis/core/models.py:84
+#: aleksis/core/models.py:97
 msgid "End date"
 msgstr ""
 
-#: aleksis/core/models.py:103
+#: aleksis/core/models.py:116
 msgid "The start date must be earlier than the end date."
 msgstr ""
 
-#: aleksis/core/models.py:110
+#: aleksis/core/models.py:123
 msgid "There is already a school term for this time or a part of this time."
 msgstr ""
 
-#: aleksis/core/models.py:139 aleksis/core/models.py:876
+#: aleksis/core/models.py:152 aleksis/core/models.py:973
 msgid "Person"
 msgstr ""
 
-#: aleksis/core/models.py:142
+#: aleksis/core/models.py:155
 msgid "Can view address"
 msgstr ""
 
-#: aleksis/core/models.py:143
+#: aleksis/core/models.py:156
 msgid "Can view contact details"
 msgstr ""
 
-#: aleksis/core/models.py:144
+#: aleksis/core/models.py:157
 msgid "Can view photo"
 msgstr ""
 
-#: aleksis/core/models.py:145
+#: aleksis/core/models.py:158
+msgid "Can view avatar image"
+msgstr ""
+
+#: aleksis/core/models.py:159
 msgid "Can view persons groups"
 msgstr ""
 
-#: aleksis/core/models.py:146
+#: aleksis/core/models.py:160
 msgid "Can view personal details"
 msgstr ""
 
-#: aleksis/core/models.py:156
+#: aleksis/core/models.py:170
 msgid "female"
 msgstr ""
 
-#: aleksis/core/models.py:156
+#: aleksis/core/models.py:170
 msgid "male"
 msgstr ""
 
-#: aleksis/core/models.py:164
+#: aleksis/core/models.py:178 aleksis/core/models.py:1227
 msgid "Linked user"
 msgstr ""
 
-#: aleksis/core/models.py:166
-msgid "Is person active?"
-msgstr ""
-
-#: aleksis/core/models.py:168
-msgid "First name"
-msgstr ""
-
-#: aleksis/core/models.py:169
-msgid "Last name"
-msgstr ""
-
-#: aleksis/core/models.py:171
+#: aleksis/core/models.py:184
 msgid "Additional name(s)"
 msgstr ""
 
-#: aleksis/core/models.py:175 aleksis/core/models.py:415
+#: aleksis/core/models.py:188 aleksis/core/models.py:479
 msgid "Short name"
 msgstr ""
 
-#: aleksis/core/models.py:178
+#: aleksis/core/models.py:191
 msgid "Street"
 msgstr ""
 
-#: aleksis/core/models.py:179
+#: aleksis/core/models.py:192
 msgid "Street number"
 msgstr ""
 
-#: aleksis/core/models.py:180
+#: aleksis/core/models.py:193
 msgid "Postal code"
 msgstr ""
 
-#: aleksis/core/models.py:181
+#: aleksis/core/models.py:194
 msgid "Place"
 msgstr ""
 
-#: aleksis/core/models.py:183
+#: aleksis/core/models.py:196 aleksis/core/templates/core/person/full.html:172
 msgid "Home phone"
 msgstr ""
 
-#: aleksis/core/models.py:184
+#: aleksis/core/models.py:197 aleksis/core/templates/core/person/full.html:182
 msgid "Mobile phone"
 msgstr ""
 
-#: aleksis/core/models.py:188
+#: aleksis/core/models.py:201
 msgid "Date of birth"
 msgstr ""
 
-#: aleksis/core/models.py:189
+#: aleksis/core/models.py:202
+msgid "Place of birth"
+msgstr ""
+
+#: aleksis/core/models.py:203
 msgid "Sex"
 msgstr ""
 
-#: aleksis/core/models.py:191
-msgid "Photo"
+#: aleksis/core/models.py:210 aleksis/core/models.py:518
+msgid "This is an official photo, used for official documents and for internal use cases."
 msgstr ""
 
-#: aleksis/core/models.py:195 aleksis/core/templates/core/person/full.html:137
+#: aleksis/core/models.py:215 aleksis/core/models.py:522
+msgid "Display picture / Avatar"
+msgstr ""
+
+#: aleksis/core/models.py:218 aleksis/core/models.py:525
+msgid "This is a picture or an avatar for public display."
+msgstr ""
+
+#: aleksis/core/models.py:223 aleksis/core/templates/core/person/full.html:239
 msgid "Guardians / Parents"
 msgstr ""
 
-#: aleksis/core/models.py:202
+#: aleksis/core/models.py:230
 msgid "Primary group"
 msgstr ""
 
-#: aleksis/core/models.py:205 aleksis/core/models.py:563
-#: aleksis/core/models.py:587 aleksis/core/models.py:672
-#: aleksis/core/models.py:951 aleksis/core/templates/core/person/full.html:120
+#: aleksis/core/models.py:233 aleksis/core/models.py:692
+#: aleksis/core/models.py:716 aleksis/core/models.py:801
+#: aleksis/core/models.py:1048
 msgid "Description"
 msgstr ""
 
-#: aleksis/core/models.py:370
+#: aleksis/core/models.py:434
 msgid "Title of field"
 msgstr ""
 
-#: aleksis/core/models.py:372
+#: aleksis/core/models.py:436
 msgid "Type of field"
 msgstr ""
 
-#: aleksis/core/models.py:379
+#: aleksis/core/models.py:443
 msgid "Addtitional field for groups"
 msgstr ""
 
-#: aleksis/core/models.py:380
+#: aleksis/core/models.py:444
 msgid "Addtitional fields for groups"
 msgstr ""
 
-#: aleksis/core/models.py:397
-msgid "Group"
-msgstr ""
-
-#: aleksis/core/models.py:400
+#: aleksis/core/models.py:464
 msgid "Can assign child groups to groups"
 msgstr ""
 
-#: aleksis/core/models.py:401
+#: aleksis/core/models.py:465
 msgid "Can view statistics about group."
 msgstr ""
 
-#: aleksis/core/models.py:413
+#: aleksis/core/models.py:477
 msgid "Long name"
 msgstr ""
 
-#: aleksis/core/models.py:423 aleksis/core/templates/core/group/full.html:85
+#: aleksis/core/models.py:487 aleksis/core/templates/core/group/full.html:85
 msgid "Members"
 msgstr ""
 
-#: aleksis/core/models.py:426 aleksis/core/templates/core/group/full.html:82
+#: aleksis/core/models.py:490 aleksis/core/templates/core/group/full.html:82
 msgid "Owners"
 msgstr ""
 
-#: aleksis/core/models.py:433 aleksis/core/templates/core/group/full.html:55
+#: aleksis/core/models.py:497 aleksis/core/templates/core/group/full.html:55
 msgid "Parent groups"
 msgstr ""
 
-#: aleksis/core/models.py:441
+#: aleksis/core/models.py:505
 msgid "Type of group"
 msgstr ""
 
-#: aleksis/core/models.py:559
-msgid "User"
-msgstr ""
-
-#: aleksis/core/models.py:562 aleksis/core/models.py:586
-#: aleksis/core/models.py:671
+#: aleksis/core/models.py:691 aleksis/core/models.py:715
+#: aleksis/core/models.py:800
 #: aleksis/core/templates/core/announcement/list.html:18
 msgid "Title"
 msgstr ""
 
-#: aleksis/core/models.py:565
+#: aleksis/core/models.py:694
 msgid "Application"
 msgstr ""
 
-#: aleksis/core/models.py:571
+#: aleksis/core/models.py:700
 msgid "Activity"
 msgstr ""
 
-#: aleksis/core/models.py:572
+#: aleksis/core/models.py:701
 msgid "Activities"
 msgstr ""
 
-#: aleksis/core/models.py:578
+#: aleksis/core/models.py:707
 msgid "Sender"
 msgstr ""
 
-#: aleksis/core/models.py:583
+#: aleksis/core/models.py:712
 msgid "Recipient"
 msgstr ""
 
-#: aleksis/core/models.py:588 aleksis/core/models.py:928
+#: aleksis/core/models.py:717 aleksis/core/models.py:1025
 msgid "Link"
 msgstr ""
 
-#: aleksis/core/models.py:590
+#: aleksis/core/models.py:719
 msgid "Read"
 msgstr ""
 
-#: aleksis/core/models.py:591
+#: aleksis/core/models.py:720
 msgid "Sent"
 msgstr ""
 
-#: aleksis/core/models.py:604
+#: aleksis/core/models.py:733
 msgid "Notification"
 msgstr ""
 
-#: aleksis/core/models.py:673
+#: aleksis/core/models.py:802
 msgid "Link to detailed view"
 msgstr ""
 
-#: aleksis/core/models.py:676
+#: aleksis/core/models.py:805
 msgid "Date and time from when to show"
 msgstr ""
 
-#: aleksis/core/models.py:679
+#: aleksis/core/models.py:808
 msgid "Date and time until when to show"
 msgstr ""
 
-#: aleksis/core/models.py:703
+#: aleksis/core/models.py:833
 msgid "Announcement"
 msgstr ""
 
-#: aleksis/core/models.py:741
+#: aleksis/core/models.py:871
 msgid "Announcement recipient"
 msgstr ""
 
-#: aleksis/core/models.py:742
+#: aleksis/core/models.py:872
 msgid "Announcement recipients"
 msgstr ""
 
-#: aleksis/core/models.py:797
+#: aleksis/core/models.py:894
 msgid "Widget Title"
 msgstr ""
 
-#: aleksis/core/models.py:798
+#: aleksis/core/models.py:895
 msgid "Activate Widget"
 msgstr ""
 
-#: aleksis/core/models.py:799
+#: aleksis/core/models.py:896
 msgid "Widget is broken"
 msgstr ""
 
-#: aleksis/core/models.py:802
+#: aleksis/core/models.py:899
 msgid "Size on mobile devices"
 msgstr ""
 
-#: aleksis/core/models.py:803
+#: aleksis/core/models.py:900
 msgid "<= 600 px, 12 columns"
 msgstr ""
 
-#: aleksis/core/models.py:808
+#: aleksis/core/models.py:905
 msgid "Size on tablet devices"
 msgstr ""
 
-#: aleksis/core/models.py:809
+#: aleksis/core/models.py:906
 msgid "> 600 px, 12 columns"
 msgstr ""
 
-#: aleksis/core/models.py:814
+#: aleksis/core/models.py:911
 msgid "Size on desktop devices"
 msgstr ""
 
-#: aleksis/core/models.py:815
+#: aleksis/core/models.py:912
 msgid "> 992 px, 12 columns"
 msgstr ""
 
-#: aleksis/core/models.py:820
+#: aleksis/core/models.py:917
 msgid "Size on large desktop devices"
 msgstr ""
 
-#: aleksis/core/models.py:821
+#: aleksis/core/models.py:918
 msgid "> 1200 px>, 12 columns"
 msgstr ""
 
-#: aleksis/core/models.py:852
+#: aleksis/core/models.py:949
 msgid "Can edit default dashboard"
 msgstr ""
 
-#: aleksis/core/models.py:853
+#: aleksis/core/models.py:950
 msgid "Dashboard Widget"
 msgstr ""
 
-#: aleksis/core/models.py:854
+#: aleksis/core/models.py:951
 msgid "Dashboard Widgets"
 msgstr ""
 
-#: aleksis/core/models.py:860
+#: aleksis/core/models.py:957
 msgid "URL"
 msgstr ""
 
-#: aleksis/core/models.py:861
+#: aleksis/core/models.py:958
 msgid "Icon URL"
 msgstr ""
 
-#: aleksis/core/models.py:867
+#: aleksis/core/models.py:964
 msgid "External link widget"
 msgstr ""
 
-#: aleksis/core/models.py:868
+#: aleksis/core/models.py:965
 msgid "External link widgets"
 msgstr ""
 
-#: aleksis/core/models.py:873
+#: aleksis/core/models.py:970
 msgid "Dashboard widget"
 msgstr ""
 
-#: aleksis/core/models.py:878
+#: aleksis/core/models.py:975
 msgid "Order"
 msgstr ""
 
-#: aleksis/core/models.py:879
+#: aleksis/core/models.py:976
 msgid "Part of the default dashboard"
 msgstr ""
 
-#: aleksis/core/models.py:894
+#: aleksis/core/models.py:991
 msgid "Dashboard widget order"
 msgstr ""
 
-#: aleksis/core/models.py:895
+#: aleksis/core/models.py:992
 msgid "Dashboard widget orders"
 msgstr ""
 
-#: aleksis/core/models.py:901
+#: aleksis/core/models.py:998
 msgid "Menu ID"
 msgstr ""
 
-#: aleksis/core/models.py:914
+#: aleksis/core/models.py:1011
 msgid "Custom menu"
 msgstr ""
 
-#: aleksis/core/models.py:915
+#: aleksis/core/models.py:1012
 msgid "Custom menus"
 msgstr ""
 
-#: aleksis/core/models.py:925
+#: aleksis/core/models.py:1022
 msgid "Menu"
 msgstr ""
 
-#: aleksis/core/models.py:929
+#: aleksis/core/models.py:1026 aleksis/core/models.py:1274
+#: aleksis/core/templates/oauth2_provider/application/detail.html:26
 msgid "Icon"
 msgstr ""
 
-#: aleksis/core/models.py:935
+#: aleksis/core/models.py:1032
 msgid "Custom menu item"
 msgstr ""
 
-#: aleksis/core/models.py:936
+#: aleksis/core/models.py:1033
 msgid "Custom menu items"
 msgstr ""
 
-#: aleksis/core/models.py:950
+#: aleksis/core/models.py:1047
 msgid "Title of type"
 msgstr ""
 
-#: aleksis/core/models.py:957 aleksis/core/templates/core/group/full.html:47
+#: aleksis/core/models.py:1054 aleksis/core/templates/core/group/full.html:47
 msgid "Group type"
 msgstr ""
 
-#: aleksis/core/models.py:971
+#: aleksis/core/models.py:1068
 msgid "Can view system status"
 msgstr ""
 
-#: aleksis/core/models.py:972
+#: aleksis/core/models.py:1069
 msgid "Can manage data"
 msgstr ""
 
-#: aleksis/core/models.py:973
+#: aleksis/core/models.py:1070
 msgid "Can impersonate"
 msgstr ""
 
-#: aleksis/core/models.py:974
+#: aleksis/core/models.py:1071
 msgid "Can use search"
 msgstr ""
 
-#: aleksis/core/models.py:975
+#: aleksis/core/models.py:1072
 msgid "Can change site preferences"
 msgstr ""
 
-#: aleksis/core/models.py:976
+#: aleksis/core/models.py:1073
 msgid "Can change person preferences"
 msgstr ""
 
-#: aleksis/core/models.py:977
+#: aleksis/core/models.py:1074
 msgid "Can change group preferences"
 msgstr ""
 
-#: aleksis/core/models.py:978
-msgid "Can add oauth applications"
-msgstr ""
-
-#: aleksis/core/models.py:979
-msgid "Can list oauth applications"
-msgstr ""
-
-#: aleksis/core/models.py:980
-msgid "Can view oauth applications"
-msgstr ""
-
-#: aleksis/core/models.py:981
-msgid "Can update oauth applications"
-msgstr ""
-
-#: aleksis/core/models.py:982
-msgid "Can delete oauth applications"
+#: aleksis/core/models.py:1075
+msgid "Can test PDF generation"
 msgstr ""
 
-#: aleksis/core/models.py:983
-msgid "Can test PDF generation"
+#: aleksis/core/models.py:1076
+msgid "Can invite persons"
 msgstr ""
 
-#: aleksis/core/models.py:1019
+#: aleksis/core/models.py:1112
 msgid "Related data check task"
 msgstr ""
 
-#: aleksis/core/models.py:1027
+#: aleksis/core/models.py:1120
 msgid "Issue solved"
 msgstr ""
 
-#: aleksis/core/models.py:1028
+#: aleksis/core/models.py:1121
 msgid "Notification sent"
 msgstr ""
 
-#: aleksis/core/models.py:1041
+#: aleksis/core/models.py:1134
 msgid "Data check result"
 msgstr ""
 
-#: aleksis/core/models.py:1042
+#: aleksis/core/models.py:1135
 msgid "Data check results"
 msgstr ""
 
-#: aleksis/core/models.py:1044
+#: aleksis/core/models.py:1137
 msgid "Can run data checks"
 msgstr ""
 
-#: aleksis/core/models.py:1045
+#: aleksis/core/models.py:1138
 msgid "Can solve data check problems"
 msgstr ""
 
-#: aleksis/core/models.py:1060
+#: aleksis/core/models.py:1145
+msgid "E-Mail address"
+msgstr ""
+
+#: aleksis/core/models.py:1177
 msgid "Owner"
 msgstr ""
 
-#: aleksis/core/models.py:1064
+#: aleksis/core/models.py:1181
 msgid "File expires at"
 msgstr ""
 
-#: aleksis/core/models.py:1066
+#: aleksis/core/models.py:1183
 msgid "Generated HTML file"
 msgstr ""
 
-#: aleksis/core/models.py:1068
+#: aleksis/core/models.py:1185
 msgid "Generated PDF file"
 msgstr ""
 
-#: aleksis/core/models.py:1075
+#: aleksis/core/models.py:1192
 msgid "PDF file"
 msgstr ""
 
-#: aleksis/core/models.py:1076
+#: aleksis/core/models.py:1193
 msgid "PDF files"
 msgstr ""
 
-#: aleksis/core/models.py:1081
+#: aleksis/core/models.py:1198
 msgid "Task result"
 msgstr ""
 
-#: aleksis/core/models.py:1084
+#: aleksis/core/models.py:1201
 msgid "Task user"
 msgstr ""
 
-#: aleksis/core/models.py:1096
+#: aleksis/core/models.py:1213
 msgid "Task user assignment"
 msgstr ""
 
-#: aleksis/core/models.py:1097
+#: aleksis/core/models.py:1214
 msgid "Task user assignments"
 msgstr ""
 
-#: aleksis/core/preferences.py:22
+#: aleksis/core/models.py:1230
+msgid "Additional attributes"
+msgstr ""
+
+#: aleksis/core/models.py:1268
+msgid "Allowed scopes that clients can request"
+msgstr ""
+
+#: aleksis/core/models.py:1278
+msgid "This image will be shown as icon in the authorization flow. It should be squared."
+msgstr ""
+
+#: aleksis/core/preferences.py:25
 msgid "General"
 msgstr ""
 
-#: aleksis/core/preferences.py:23
+#: aleksis/core/preferences.py:26
 msgid "School"
 msgstr ""
 
-#: aleksis/core/preferences.py:24
+#: aleksis/core/preferences.py:27
 msgid "Theme"
 msgstr ""
 
-#: aleksis/core/preferences.py:25
+#: aleksis/core/preferences.py:28
 msgid "Mail"
 msgstr ""
 
-#: aleksis/core/preferences.py:27
+#: aleksis/core/preferences.py:30
 msgid "Footer"
 msgstr ""
 
-#: aleksis/core/preferences.py:28
+#: aleksis/core/preferences.py:31
 msgid "Accounts"
 msgstr ""
 
-#: aleksis/core/preferences.py:29
+#: aleksis/core/preferences.py:32
 msgid "Authentication"
 msgstr ""
 
-#: aleksis/core/preferences.py:30
+#: aleksis/core/preferences.py:33
 msgid "Internationalisation"
 msgstr ""
 
-#: aleksis/core/preferences.py:41
+#: aleksis/core/preferences.py:43
 msgid "Site title"
 msgstr ""
 
-#: aleksis/core/preferences.py:52
+#: aleksis/core/preferences.py:55
 msgid "Site description"
 msgstr ""
 
-#: aleksis/core/preferences.py:63
+#: aleksis/core/preferences.py:65
 msgid "Primary colour"
 msgstr ""
 
-#: aleksis/core/preferences.py:75
+#: aleksis/core/preferences.py:77
 msgid "Secondary colour"
 msgstr ""
 
-#: aleksis/core/preferences.py:86
+#: aleksis/core/preferences.py:89
 msgid "Logo"
 msgstr ""
 
-#: aleksis/core/preferences.py:96
+#: aleksis/core/preferences.py:100
 msgid "Favicon"
 msgstr ""
 
-#: aleksis/core/preferences.py:106
+#: aleksis/core/preferences.py:111
 msgid "PWA-Icon"
 msgstr ""
 
-#: aleksis/core/preferences.py:117
+#: aleksis/core/preferences.py:121
+msgid "PWA-Icon is maskable"
+msgstr ""
+
+#: aleksis/core/preferences.py:133
 msgid "Mail out name"
 msgstr ""
 
-#: aleksis/core/preferences.py:128
+#: aleksis/core/preferences.py:144
 msgid "Mail out address"
 msgstr ""
 
-#: aleksis/core/preferences.py:140
+#: aleksis/core/preferences.py:157
 msgid "Link to privacy policy"
 msgstr ""
 
-#: aleksis/core/preferences.py:152
+#: aleksis/core/preferences.py:169
 msgid "Link to imprint"
 msgstr ""
 
-#: aleksis/core/preferences.py:164
+#: aleksis/core/preferences.py:180
 msgid "Name format for addressing"
 msgstr ""
 
-#: aleksis/core/preferences.py:180
+#: aleksis/core/preferences.py:197
 msgid "Channels to use for notifications"
 msgstr ""
 
-#: aleksis/core/preferences.py:192
+#: aleksis/core/preferences.py:209
 msgid "Regular expression to match primary group, e.g. '^Class .*'"
 msgstr ""
 
-#: aleksis/core/preferences.py:203
+#: aleksis/core/preferences.py:220
 msgid "Field on person to match primary group against"
 msgstr ""
 
-#: aleksis/core/preferences.py:215
+#: aleksis/core/preferences.py:232
 msgid "Automatically create new persons for new users"
 msgstr ""
 
-#: aleksis/core/preferences.py:224
+#: aleksis/core/preferences.py:241
 msgid "Automatically link existing persons to new users by their e-mail address"
 msgstr ""
 
-#: aleksis/core/preferences.py:235
+#: aleksis/core/preferences.py:252
 msgid "Display name of the school"
 msgstr ""
 
-#: aleksis/core/preferences.py:246
+#: aleksis/core/preferences.py:263
 msgid "Official name of the school, e.g. as given by supervisory authority"
 msgstr ""
 
-#: aleksis/core/preferences.py:254
+#: aleksis/core/preferences.py:271
 msgid "Allow users to change their passwords"
 msgstr ""
 
-#: aleksis/core/preferences.py:262
+#: aleksis/core/preferences.py:279
 msgid "Enable signup"
 msgstr ""
 
-#: aleksis/core/preferences.py:273
+#: aleksis/core/preferences.py:287
+msgid "Enable invitations"
+msgstr ""
+
+#: aleksis/core/preferences.py:295
+msgid "Length of invite code. (Default 3: abcde-acbde-abcde)"
+msgstr ""
+
+#: aleksis/core/preferences.py:303
+msgid "Size of packets. (Default 5: abcde)"
+msgstr ""
+
+#: aleksis/core/preferences.py:314
+msgid "Allowed Grant Flows for OAuth applications"
+msgstr ""
+
+#: aleksis/core/preferences.py:328
 msgid "Available languages"
 msgstr ""
 
-#: aleksis/core/preferences.py:285
+#: aleksis/core/preferences.py:341
 msgid "Send emails if data checks detect problems"
 msgstr ""
 
-#: aleksis/core/preferences.py:296
+#: aleksis/core/preferences.py:352
 msgid "Email recipients for data checks problem emails"
 msgstr ""
 
-#: aleksis/core/preferences.py:307
+#: aleksis/core/preferences.py:363
 msgid "Email recipient groups for data checks problem emails"
 msgstr ""
 
-#: aleksis/core/preferences.py:316
+#: aleksis/core/preferences.py:372
 msgid "Show dashboard to users without login"
 msgstr ""
 
-#: aleksis/core/preferences.py:325
+#: aleksis/core/preferences.py:381
 msgid "Allow users to edit their dashboard"
 msgstr ""
 
-#: aleksis/core/preferences.py:336
+#: aleksis/core/preferences.py:392
 msgid "Fields on person model which are editable by themselves."
 msgstr ""
 
-#: aleksis/core/preferences.py:350
+#: aleksis/core/preferences.py:407
 msgid "Editable fields on person model which should trigger a notification on change"
 msgstr ""
 
-#: aleksis/core/preferences.py:363
+#: aleksis/core/preferences.py:421
 msgid "Contact for notification if a person changes their data"
 msgstr ""
 
-#: aleksis/core/preferences.py:373
+#: aleksis/core/preferences.py:432
 msgid "PDF file expiration duration"
 msgstr ""
 
-#: aleksis/core/preferences.py:374
+#: aleksis/core/preferences.py:433
 msgid "in minutes"
 msgstr ""
 
-#: aleksis/core/preferences.py:384
+#: aleksis/core/preferences.py:443
 msgid "Automatically update the dashboard and its widgets"
 msgstr ""
 
-#: aleksis/core/preferences.py:394
+#: aleksis/core/preferences.py:453
 msgid "Automatically update the dashboard and its widgets sitewide"
 msgstr ""
 
-#: aleksis/core/settings.py:452
+#: aleksis/core/preferences.py:463
+msgid "Country for phone number parsing"
+msgstr ""
+
+#: aleksis/core/settings.py:529
 msgid "English"
 msgstr ""
 
-#: aleksis/core/settings.py:453
+#: aleksis/core/settings.py:530
 msgid "German"
 msgstr ""
 
-#: aleksis/core/tables.py:19
+#: aleksis/core/tables.py:24
 #: aleksis/core/templates/core/announcement/list.html:36
 #: aleksis/core/templates/core/group/full.html:24
-#: aleksis/core/templates/core/person/full.html:23
-#: aleksis/core/templates/oauth2_provider/application_detail.html:17
+#: aleksis/core/templates/core/person/full.html:26
+#: aleksis/core/templates/core/person/full.html:98
+#: aleksis/core/templates/oauth2_provider/application/detail.html:17
 msgid "Edit"
 msgstr ""
 
-#: aleksis/core/tables.py:21 aleksis/core/tables.py:89
+#: aleksis/core/tables.py:26 aleksis/core/tables.py:94
+#: aleksis/core/tables.py:138
 #: aleksis/core/templates/core/announcement/list.html:22
 msgid "Actions"
 msgstr ""
 
-#: aleksis/core/tables.py:56 aleksis/core/tables.py:57
-#: aleksis/core/tables.py:71 aleksis/core/tables.py:87
+#: aleksis/core/tables.py:61 aleksis/core/tables.py:62
+#: aleksis/core/tables.py:76 aleksis/core/tables.py:92
+#: aleksis/core/tables.py:136
 #: aleksis/core/templates/core/announcement/list.html:42
 #: aleksis/core/templates/core/group/full.html:31
 #: aleksis/core/templates/core/pages/delete.html:22
-#: aleksis/core/templates/core/person/full.html:30
-#: aleksis/core/templates/oauth2_provider/application_confirm_delete.html:20
-#: aleksis/core/templates/oauth2_provider/application_detail.html:21
+#: aleksis/core/templates/core/person/full.html:33
+#: aleksis/core/templates/core/person/full.html:105
+#: aleksis/core/templates/oauth2_provider/application/detail.html:21
 msgid "Delete"
 msgstr ""
 
-#: aleksis/core/templates/403.html:14 aleksis/core/templates/404.html:10
-#: aleksis/core/templates/500.html:10
-#: aleksis/core/templates/oauth2_provider/authorize.html:47
+#: aleksis/core/templates/403.html:14 aleksis/core/templates/500.html:10
+#: aleksis/core/templates/oauth2_provider/authorize.html:54
 #: aleksis/core/templates/oauth2_provider/authorized-oob.html:24
 msgid "Error"
 msgstr ""
@@ -1124,7 +1206,7 @@ msgid ""
 "          object."
 msgstr ""
 
-#: aleksis/core/templates/403.html:19 aleksis/core/templates/404.html:17
+#: aleksis/core/templates/403.html:19 aleksis/core/templates/404.html:16
 msgid ""
 "\n"
 "            If you think this is an error in AlekSIS, please contact your site\n"
@@ -1132,13 +1214,7 @@ msgid ""
 "          "
 msgstr ""
 
-#: aleksis/core/templates/404.html:10
-msgid ""
-"The requested page or object was not\n"
-"          found."
-msgstr ""
-
-#: aleksis/core/templates/404.html:13
+#: aleksis/core/templates/404.html:12
 msgid ""
 "\n"
 "            If you were redirected by a link on an external page,\n"
@@ -1178,11 +1254,11 @@ msgstr ""
 msgid "Account inactive"
 msgstr ""
 
-#: aleksis/core/templates/account/account_inactive.html:13
+#: aleksis/core/templates/account/account_inactive.html:14
 msgid "Account inactive."
 msgstr ""
 
-#: aleksis/core/templates/account/account_inactive.html:15
+#: aleksis/core/templates/account/account_inactive.html:17
 msgid ""
 "\n"
 "            This account is currently inactive. If you think this is an\n"
@@ -1195,14 +1271,13 @@ msgid "Hello!"
 msgstr ""
 
 #: aleksis/core/templates/account/email/base_message.txt:9
-#: aleksis/core/templates/templated_email/notification.email:22
-#: aleksis/core/templates/templated_email/notification.email:46
 msgid "Your AlekSIS team"
 msgstr ""
 
 #: aleksis/core/templates/account/email_confirm.html:5
 #: aleksis/core/templates/account/email_confirm.html:6
 #: aleksis/core/templates/account/email_confirm.html:17
+#: aleksis/core/templates/socialaccount/login.html:17
 msgid "Confirm"
 msgstr ""
 
@@ -1247,7 +1322,7 @@ msgstr ""
 #: aleksis/core/templates/account/password_reset_done.html:5
 #: aleksis/core/templates/account/verification_email_required.html:5
 #: aleksis/core/templates/account/verification_email_required.html:6
-#: aleksis/core/templates/two_factor/core/login.html:81
+#: aleksis/core/templates/two_factor/core/login.html:100
 msgid "Reset password"
 msgstr ""
 
@@ -1334,11 +1409,11 @@ msgstr ""
 msgid "Signup closed"
 msgstr ""
 
-#: aleksis/core/templates/account/signup_closed.html:13
+#: aleksis/core/templates/account/signup_closed.html:14
 msgid "Signup closed."
 msgstr ""
 
-#: aleksis/core/templates/account/signup_closed.html:15
+#: aleksis/core/templates/account/signup_closed.html:17
 msgid ""
 "\n"
 "            This sign up is currently closed. If you think this is an\n"
@@ -1376,11 +1451,6 @@ msgid ""
 "          "
 msgstr ""
 
-#: aleksis/core/templates/account/verification_sent.html:30
-#, python-format
-msgid "<strong>Note:</strong> you can still <a href=\"%(email_url)s\">change your e-mail address</a>"
-msgstr ""
-
 #: aleksis/core/templates/core/additional_field/edit.html:6
 #: aleksis/core/templates/core/additional_field/edit.html:7
 msgid "Edit additional field"
@@ -1424,24 +1494,24 @@ msgstr ""
 msgid "There are no announcements."
 msgstr ""
 
-#: aleksis/core/templates/core/base.html:60
+#: aleksis/core/templates/core/base.html:78
 msgid "Logged in as"
 msgstr ""
 
-#: aleksis/core/templates/core/base.html:154
-msgid "About AlekSIS — The Free School Information System"
+#: aleksis/core/templates/core/base.html:179
+msgid "About AlekSIS® — The Free School Information System"
 msgstr ""
 
-#: aleksis/core/templates/core/base.html:162
-msgid "Impress"
+#: aleksis/core/templates/core/base.html:187
+msgid "Imprint"
 msgstr ""
 
-#: aleksis/core/templates/core/base.html:170
+#: aleksis/core/templates/core/base.html:195
 msgid "Privacy Policy"
 msgstr ""
 
-#: aleksis/core/templates/core/base_print.html:72
-msgid "Powered by AlekSIS"
+#: aleksis/core/templates/core/base_print.html:74
+msgid "Powered by AlekSIS®"
 msgstr ""
 
 #: aleksis/core/templates/core/dashboard_widget/create.html:8
@@ -1526,15 +1596,15 @@ msgstr ""
 msgid "Options to solve the problem"
 msgstr ""
 
-#: aleksis/core/templates/core/data_check/list.html:62
+#: aleksis/core/templates/core/data_check/list.html:63
 msgid "Show object"
 msgstr ""
 
-#: aleksis/core/templates/core/data_check/list.html:84
+#: aleksis/core/templates/core/data_check/list.html:86
 msgid "Registered checks"
 msgstr ""
 
-#: aleksis/core/templates/core/data_check/list.html:88
+#: aleksis/core/templates/core/data_check/list.html:90
 msgid ""
 "\n"
 "            The system will check for the following problems:\n"
@@ -1627,7 +1697,7 @@ msgstr ""
 
 #: aleksis/core/templates/core/group/child_groups.html:93
 #: aleksis/core/templates/core/group/child_groups.html:128
-#: aleksis/core/templates/oauth2_provider/application_detail.html:9
+#: aleksis/core/templates/oauth2_provider/application/detail.html:9
 #: aleksis/core/templates/two_factor/_wizard_actions.html:15
 #: aleksis/core/templates/two_factor/_wizard_actions.html:20
 msgid "Back"
@@ -1656,7 +1726,8 @@ msgid "Edit group"
 msgstr ""
 
 #: aleksis/core/templates/core/group/full.html:38
-#: aleksis/core/templates/core/person/full.html:37
+#: aleksis/core/templates/core/person/full.html:40
+#: aleksis/core/templates/core/person/full.html:112
 msgid "Change preferences"
 msgstr ""
 
@@ -1745,36 +1816,46 @@ msgid "No notifications available yet."
 msgstr ""
 
 #: aleksis/core/templates/core/pages/about.html:6
-#: aleksis/core/templates/core/pages/about.html:15
-msgid "About AlekSIS"
+msgid "About AlekSIS®"
 msgstr ""
 
 #: aleksis/core/templates/core/pages/about.html:7
-msgid "AlekSIS – The Free School Information System"
+msgid "AlekSIS® – The Free School Information System"
+msgstr ""
+
+#: aleksis/core/templates/core/pages/about.html:15
+msgid "About AlekSIS"
 msgstr ""
 
 #: aleksis/core/templates/core/pages/about.html:17
 msgid ""
 "\n"
-"              This platform is powered by AlekSIS, a web-based school information system (SIS) which can be used\n"
+"              This platform is powered by AlekSIS®, a web-based school information system (SIS) which can be used\n"
 "              to manage and/or publish organisational artifacts of educational institutions. AlekSIS is free software and\n"
 "              can be used by anyone.\n"
 "            "
 msgstr ""
 
-#: aleksis/core/templates/core/pages/about.html:25
+#: aleksis/core/templates/core/pages/about.html:24
+msgid ""
+"\n"
+"              AlekSIS® is a registered trademark of the AlekSIS open source project, represented by Teckids e.V.\n"
+"            "
+msgstr ""
+
+#: aleksis/core/templates/core/pages/about.html:30
 msgid "Website of AlekSIS"
 msgstr ""
 
-#: aleksis/core/templates/core/pages/about.html:26
+#: aleksis/core/templates/core/pages/about.html:31
 msgid "Source code"
 msgstr ""
 
-#: aleksis/core/templates/core/pages/about.html:35
+#: aleksis/core/templates/core/pages/about.html:40
 msgid "Licence information"
 msgstr ""
 
-#: aleksis/core/templates/core/pages/about.html:37
+#: aleksis/core/templates/core/pages/about.html:42
 msgid ""
 "\n"
 "              The core and the official apps of AlekSIS are licenced under the EUPL, version 1.2 or later. For licence\n"
@@ -1783,23 +1864,23 @@ msgid ""
 "            "
 msgstr ""
 
-#: aleksis/core/templates/core/pages/about.html:45
+#: aleksis/core/templates/core/pages/about.html:50
 msgid "Free/Open Source Licence"
 msgstr ""
 
-#: aleksis/core/templates/core/pages/about.html:46
+#: aleksis/core/templates/core/pages/about.html:51
 msgid "Other Licence"
 msgstr ""
 
-#: aleksis/core/templates/core/pages/about.html:50
+#: aleksis/core/templates/core/pages/about.html:55
 msgid "Full licence text"
 msgstr ""
 
-#: aleksis/core/templates/core/pages/about.html:51
+#: aleksis/core/templates/core/pages/about.html:56
 msgid "More information about the EUPL"
 msgstr ""
 
-#: aleksis/core/templates/core/pages/about.html:90
+#: aleksis/core/templates/core/pages/about.html:95
 #, python-format
 msgid ""
 "\n"
@@ -1903,6 +1984,8 @@ msgid "Celery task results"
 msgstr ""
 
 #: aleksis/core/templates/core/pages/system_status.html:112
+#: aleksis/core/templates/templated_email/celery_failure.email:9
+#: aleksis/core/templates/templated_email/celery_failure.email:28
 msgid "Task"
 msgstr ""
 
@@ -1984,6 +2067,51 @@ msgid ""
 "          "
 msgstr ""
 
+#: aleksis/core/templates/core/perms/assign.html:12
+#: aleksis/core/templates/core/perms/assign.html:13
+msgid "Assign permission"
+msgstr ""
+
+#: aleksis/core/templates/core/perms/assign.html:17
+msgid "Selected permission"
+msgstr ""
+
+#: aleksis/core/templates/core/perms/assign.html:26
+msgid "Assign"
+msgstr ""
+
+#: aleksis/core/templates/core/perms/list.html:21
+msgid "Assign a new permission"
+msgstr ""
+
+#: aleksis/core/templates/core/perms/list.html:25
+msgid "Select"
+msgstr ""
+
+#: aleksis/core/templates/core/perms/list.html:34
+msgid "Global (user)"
+msgstr ""
+
+#: aleksis/core/templates/core/perms/list.html:38
+msgid "Global (group)"
+msgstr ""
+
+#: aleksis/core/templates/core/perms/list.html:42
+msgid "Object (user)"
+msgstr ""
+
+#: aleksis/core/templates/core/perms/list.html:46
+msgid "Object (group)"
+msgstr ""
+
+#: aleksis/core/templates/core/perms/list.html:52
+msgid "Filter permissions"
+msgstr ""
+
+#: aleksis/core/templates/core/perms/list.html:58
+msgid "Update"
+msgstr ""
+
 #: aleksis/core/templates/core/person/create.html:12
 #: aleksis/core/templates/core/person/create.html:13
 #: aleksis/core/templates/core/person/list.html:17
@@ -1995,17 +2123,25 @@ msgstr ""
 msgid "Edit person"
 msgstr ""
 
-#: aleksis/core/templates/core/person/full.html:44
-#: aleksis/core/templates/impersonate/list_users.html:7
-#: aleksis/core/templates/impersonate/list_users.html:8
+#: aleksis/core/templates/core/person/full.html:47
+#: aleksis/core/templates/core/person/full.html:119
 msgid "Impersonate"
 msgstr ""
 
-#: aleksis/core/templates/core/person/full.html:50
+#: aleksis/core/templates/core/person/full.html:54
+#: aleksis/core/templates/core/person/full.html:126
+msgid "Invite user"
+msgstr ""
+
+#: aleksis/core/templates/core/person/full.html:133
 msgid "Contact details"
 msgstr ""
 
-#: aleksis/core/templates/core/person/full.html:130
+#: aleksis/core/templates/core/person/full.html:224
+msgid "This person didn't upload a personal photo."
+msgstr ""
+
+#: aleksis/core/templates/core/person/full.html:232
 msgid "Children"
 msgstr ""
 
@@ -2045,77 +2181,138 @@ msgstr ""
 msgid "Save preferences"
 msgstr ""
 
-#: aleksis/core/templates/oauth2_provider/application_confirm_delete.html:5
-#: aleksis/core/templates/oauth2_provider/application_confirm_delete.html:6
-msgid "Delete application"
+#: aleksis/core/templates/invitations/disabled.html:5
+msgid "The invite feature is disabled"
+msgstr ""
+
+#: aleksis/core/templates/invitations/disabled.html:13
+msgid "The invite feature is disabled."
+msgstr ""
+
+#: aleksis/core/templates/invitations/disabled.html:15
+msgid "To enable it, switch on the corresponding checkbox in the authentication section of the "
+msgstr ""
+
+#: aleksis/core/templates/invitations/disabled.html:16
+msgid "site preferences page"
+msgstr ""
+
+#: aleksis/core/templates/invitations/enter.html:21
+msgid "Accept your invitation"
 msgstr ""
 
-#: aleksis/core/templates/oauth2_provider/application_confirm_delete.html:12
+#: aleksis/core/templates/invitations/enter.html:25
+msgid ""
+"\n"
+"                Please enter your invitation code to register\n"
+"                your new user account:\n"
+"              "
+msgstr ""
+
+#: aleksis/core/templates/invitations/enter.html:37
+msgid "Accept invite"
+msgstr ""
+
+#: aleksis/core/templates/invitations/forms/_invite.html:9
+#: aleksis/core/templates/invitations/forms/_invite.html:10
+#: aleksis/core/templates/invitations/forms/_invite.html:21
+msgid "Invite"
+msgstr ""
+
+#: aleksis/core/templates/invitations/forms/_invite.html:17
+msgid "Invite by email address"
+msgstr ""
+
+#: aleksis/core/templates/invitations/forms/_invite.html:26
+msgid "Generate invitation code"
+msgstr ""
+
+#: aleksis/core/templates/invitations/forms/_invite.html:29
+msgid "Generate code"
+msgstr ""
+
+#: aleksis/core/templates/invitations/forms/_invite.html:33
+msgid "Invitations"
+msgstr ""
+
+#: aleksis/core/templates/invitations/messages/invite_accepted.txt:3
 #, python-format
-msgid "Are you sure to delete the application %(application_name)s?"
+msgid "The invitation for %(email)s has been accepted."
+msgstr ""
+
+#: aleksis/core/templates/oauth2_provider/application/create.html:5
+#: aleksis/core/templates/oauth2_provider/application/create.html:6
+msgid "Register OAuth2 Application"
 msgstr ""
 
-#: aleksis/core/templates/oauth2_provider/application_confirm_delete.html:24
-#: aleksis/core/templates/oauth2_provider/application_form.html:18
+#: aleksis/core/templates/oauth2_provider/application/create.html:14
+#: aleksis/core/templates/oauth2_provider/application/edit.html:14
 #: aleksis/core/templates/oauth2_provider/authorized-token-delete.html:24
 #: aleksis/core/templates/two_factor/_wizard_actions.html:6
 msgid "Cancel"
 msgstr ""
 
-#: aleksis/core/templates/oauth2_provider/application_detail.html:27
+#: aleksis/core/templates/oauth2_provider/application/detail.html:5
+msgid "OAuth2 Application"
+msgstr ""
+
+#: aleksis/core/templates/oauth2_provider/application/detail.html:39
 msgid "Client id"
 msgstr ""
 
-#: aleksis/core/templates/oauth2_provider/application_detail.html:35
+#: aleksis/core/templates/oauth2_provider/application/detail.html:47
 msgid "Client secret"
 msgstr ""
 
-#: aleksis/core/templates/oauth2_provider/application_detail.html:43
+#: aleksis/core/templates/oauth2_provider/application/detail.html:55
 msgid "Client type"
 msgstr ""
 
-#: aleksis/core/templates/oauth2_provider/application_detail.html:51
-msgid "Authorization Grant Type"
+#: aleksis/core/templates/oauth2_provider/application/detail.html:63
+msgid "Allowed scopes"
 msgstr ""
 
-#: aleksis/core/templates/oauth2_provider/application_detail.html:59
+#: aleksis/core/templates/oauth2_provider/application/detail.html:71
 msgid "Redirect URIs"
 msgstr ""
 
-#: aleksis/core/templates/oauth2_provider/application_form.html:5
-msgid "Create OAuth2 Application"
+#: aleksis/core/templates/oauth2_provider/application/detail.html:79
+msgid "Skip Authorisation"
 msgstr ""
 
-#: aleksis/core/templates/oauth2_provider/application_form.html:10
-msgid "Edit application"
+#: aleksis/core/templates/oauth2_provider/application/edit.html:5
+#: aleksis/core/templates/oauth2_provider/application/edit.html:6
+msgid "Edit OAuth2 Application"
 msgstr ""
 
-#: aleksis/core/templates/oauth2_provider/application_list.html:8
-msgid "OAuth2 applications"
+#: aleksis/core/templates/oauth2_provider/application/list.html:11
+msgid "Register new application"
 msgstr ""
 
-#: aleksis/core/templates/oauth2_provider/application_list.html:12
-msgid "Register new applications"
-msgstr ""
-
-#: aleksis/core/templates/oauth2_provider/application_list.html:23
+#: aleksis/core/templates/oauth2_provider/application/list.html:25
 msgid "No applications defined."
 msgstr ""
 
 #: aleksis/core/templates/oauth2_provider/authorize.html:5
-#: aleksis/core/templates/oauth2_provider/authorize.html:16
+#: aleksis/core/templates/socialaccount/login.html:5
+#: aleksis/core/templates/socialaccount/login.html:6
 msgid "Authorize"
 msgstr ""
 
-#: aleksis/core/templates/oauth2_provider/authorize.html:18
+#: aleksis/core/templates/oauth2_provider/authorize.html:23
+#, python-format
+msgid "Authorize %(name)s"
+msgstr ""
+
+#: aleksis/core/templates/oauth2_provider/authorize.html:25
 msgid "The application requests access to the following scopes:"
 msgstr ""
 
-#: aleksis/core/templates/oauth2_provider/authorize.html:33
+#: aleksis/core/templates/oauth2_provider/authorize.html:40
 msgid "Allow"
 msgstr ""
 
-#: aleksis/core/templates/oauth2_provider/authorize.html:36
+#: aleksis/core/templates/oauth2_provider/authorize.html:43
 msgid "Disallow"
 msgstr ""
 
@@ -2218,6 +2415,20 @@ msgstr ""
 msgid "Add a Third-party Account"
 msgstr ""
 
+#: aleksis/core/templates/socialaccount/login.html:12
+#, python-format
+msgid "You are about to connect a new third party account from %(provider)s."
+msgstr ""
+
+#: aleksis/core/templates/socialaccount/login.html:23
+#, python-format
+msgid "You are about to sign in using a third party account from %(provider)s."
+msgstr ""
+
+#: aleksis/core/templates/socialaccount/login.html:28
+msgid "Continue"
+msgstr ""
+
 #: aleksis/core/templates/socialaccount/login_cancelled.html:5
 #: aleksis/core/templates/socialaccount/login_cancelled.html:6
 #: aleksis/core/templates/socialaccount/login_cancelled.html:13
@@ -2262,26 +2473,60 @@ msgid ""
 "        "
 msgstr ""
 
-#: aleksis/core/templates/templated_email/data_checks.email:4
-msgid "The system detected some new problems with your data."
+#: aleksis/core/templates/templated_email/base.email:5
+#: aleksis/core/templates/templated_email/base.email:16
+msgid "Hello"
 msgstr ""
 
-#: aleksis/core/templates/templated_email/data_checks.email:8
-#: aleksis/core/templates/templated_email/data_checks.email:24
-#: aleksis/core/templates/templated_email/person_changed.email:8
-#: aleksis/core/templates/templated_email/person_changed.email:20
-msgid "Hello,"
+#: aleksis/core/templates/templated_email/celery_failure.email:4
+#, python-format
+msgid "Celery task %(task_name)s failed!"
 msgstr ""
 
-#: aleksis/core/templates/templated_email/data_checks.email:10
+#: aleksis/core/templates/templated_email/celery_failure.email:7
+#, python-format
+msgid "the celery task %(task_name)s failed with following information:"
+msgstr ""
+
+#: aleksis/core/templates/templated_email/celery_failure.email:10
+#: aleksis/core/templates/templated_email/celery_failure.email:29
+msgid "Task ID"
+msgstr ""
+
+#: aleksis/core/templates/templated_email/celery_failure.email:11
+#: aleksis/core/templates/templated_email/celery_failure.email:30
+msgid "Raised exception"
+msgstr ""
+
+#: aleksis/core/templates/templated_email/celery_failure.email:12
+#: aleksis/core/templates/templated_email/celery_failure.email:31
+msgid "Positional arguments"
+msgstr ""
+
+#: aleksis/core/templates/templated_email/celery_failure.email:15
+#: aleksis/core/templates/templated_email/celery_failure.email:38
+msgid "Keyword arguments"
+msgstr ""
+
+#: aleksis/core/templates/templated_email/celery_failure.email:22
+#, python-format
 msgid ""
 "\n"
-"  the system detected some new problems with your data.\n"
-"  Please take some time to inspect them and solve the issues or mark them as ignored.\n"
-" "
+"      the celery task %(task_name)s failed with following information:\n"
+"    "
 msgstr ""
 
-#: aleksis/core/templates/templated_email/data_checks.email:26
+#: aleksis/core/templates/templated_email/data_checks.email:3
+msgid "The system detected some new problems with your data."
+msgstr ""
+
+#: aleksis/core/templates/templated_email/data_checks.email:6
+msgid ""
+"the system detected some new problems with your data.\n"
+"Please take some time to inspect them and solve the issues or mark them as ignored."
+msgstr ""
+
+#: aleksis/core/templates/templated_email/data_checks.email:15
 msgid ""
 "\n"
 "   the system detected some new problems with your data.\n"
@@ -2289,43 +2534,40 @@ msgid ""
 "  "
 msgstr ""
 
-#: aleksis/core/templates/templated_email/data_checks.email:34
+#: aleksis/core/templates/templated_email/data_checks.email:23
 msgid "Problem description"
 msgstr ""
 
-#: aleksis/core/templates/templated_email/data_checks.email:35
+#: aleksis/core/templates/templated_email/data_checks.email:24
 msgid "Count of objects with new problems"
 msgstr ""
 
-#: aleksis/core/templates/templated_email/notification.email:3
+#: aleksis/core/templates/templated_email/notification.email:4
 msgid "New notification for"
 msgstr ""
 
 #: aleksis/core/templates/templated_email/notification.email:6
-#: aleksis/core/templates/templated_email/notification.email:27
+#: aleksis/core/templates/templated_email/notification.email:20
 #, python-format
-msgid "Dear %(notification_user)s,"
+msgid "Hello %(notification_user)s,"
 msgstr ""
 
-#: aleksis/core/templates/templated_email/notification.email:8
-#: aleksis/core/templates/templated_email/notification.email:29
+#: aleksis/core/templates/templated_email/notification.email:9
+#: aleksis/core/templates/templated_email/notification.email:23
 msgid "we got a new notification for you:"
 msgstr ""
 
 #: aleksis/core/templates/templated_email/notification.email:15
-#: aleksis/core/templates/templated_email/notification.email:35
+#: aleksis/core/templates/templated_email/notification.email:29
 msgid "More information"
 msgstr ""
 
-#: aleksis/core/templates/templated_email/notification.email:18
+#: aleksis/core/templates/templated_email/notification.email:17
 #, python-format
-msgid ""
-"\n"
-"        Sent by %(trans_sender)s at %(trans_created_at)s\n"
-"    "
+msgid "Sent by %(trans_sender)s at %(trans_created_at)s"
 msgstr ""
 
-#: aleksis/core/templates/templated_email/notification.email:40
+#: aleksis/core/templates/templated_email/notification.email:34
 #, python-format
 msgid ""
 "\n"
@@ -2338,15 +2580,12 @@ msgstr ""
 msgid "%(person)s changed their data!"
 msgstr ""
 
-#: aleksis/core/templates/templated_email/person_changed.email:10
+#: aleksis/core/templates/templated_email/person_changed.email:7
 #, python-format
-msgid ""
-"\n"
-"   the person %(person)s recently changed the following fields:\n"
-" "
+msgid "the person %(person)s recently changed the following fields:"
 msgstr ""
 
-#: aleksis/core/templates/templated_email/person_changed.email:22
+#: aleksis/core/templates/templated_email/person_changed.email:15
 #, python-format
 msgid ""
 "\n"
@@ -2397,21 +2636,30 @@ msgstr ""
 msgid "Generate Tokens"
 msgstr ""
 
-#: aleksis/core/templates/two_factor/core/login.html:20
+#: aleksis/core/templates/two_factor/core/login.html:27
+#, python-format
+msgid "Login for %(name)s"
+msgstr ""
+
+#: aleksis/core/templates/two_factor/core/login.html:30
 msgid "Login with username and password"
 msgstr ""
 
-#: aleksis/core/templates/two_factor/core/login.html:28
+#: aleksis/core/templates/two_factor/core/login.html:38
 msgid ""
 "You have no permission to view this page. Please login with an other\n"
 "                    account."
 msgstr ""
 
-#: aleksis/core/templates/two_factor/core/login.html:36
+#: aleksis/core/templates/two_factor/core/login.html:47
+msgid "Please login with your account to use the external application."
+msgstr ""
+
+#: aleksis/core/templates/two_factor/core/login.html:54
 msgid "Please login to see this page."
 msgstr ""
 
-#: aleksis/core/templates/two_factor/core/login.html:46
+#: aleksis/core/templates/two_factor/core/login.html:65
 msgid ""
 "\n"
 "                        We are calling your phone right now, please enter the\n"
@@ -2419,7 +2667,7 @@ msgid ""
 "                      "
 msgstr ""
 
-#: aleksis/core/templates/two_factor/core/login.html:51
+#: aleksis/core/templates/two_factor/core/login.html:70
 msgid ""
 "\n"
 "                        We sent you a text message, please enter the tokens we\n"
@@ -2427,7 +2675,7 @@ msgid ""
 "                      "
 msgstr ""
 
-#: aleksis/core/templates/two_factor/core/login.html:56
+#: aleksis/core/templates/two_factor/core/login.html:75
 msgid ""
 "\n"
 "                        Please enter the tokens generated by your token\n"
@@ -2435,7 +2683,7 @@ msgid ""
 "                      "
 msgstr ""
 
-#: aleksis/core/templates/two_factor/core/login.html:62
+#: aleksis/core/templates/two_factor/core/login.html:81
 msgid ""
 "\n"
 "                      Use this form for entering backup tokens for logging in.\n"
@@ -2444,23 +2692,23 @@ msgid ""
 "                    "
 msgstr ""
 
-#: aleksis/core/templates/two_factor/core/login.html:90
+#: aleksis/core/templates/two_factor/core/login.html:109
 msgid "Device currently not available?"
 msgstr ""
 
-#: aleksis/core/templates/two_factor/core/login.html:92
+#: aleksis/core/templates/two_factor/core/login.html:111
 msgid "Or, alternatively, use one of your backup phones:"
 msgstr ""
 
-#: aleksis/core/templates/two_factor/core/login.html:102
+#: aleksis/core/templates/two_factor/core/login.html:121
 msgid "As a last resort, you can use a backup token:"
 msgstr ""
 
-#: aleksis/core/templates/two_factor/core/login.html:105
+#: aleksis/core/templates/two_factor/core/login.html:124
 msgid "Use Backup Token"
 msgstr ""
 
-#: aleksis/core/templates/two_factor/core/login.html:116
+#: aleksis/core/templates/two_factor/core/login.html:135
 msgid "Use alternative login options"
 msgstr ""
 
@@ -2701,135 +2949,184 @@ msgid ""
 "      "
 msgstr ""
 
-#: aleksis/core/util/notifications.py:65
+#: aleksis/core/util/notifications.py:63
 msgid "E-Mail"
 msgstr ""
 
-#: aleksis/core/util/notifications.py:66
+#: aleksis/core/util/notifications.py:64
 msgid "SMS"
 msgstr ""
 
-#: aleksis/core/util/pdf.py:105
+#: aleksis/core/util/pdf.py:118
 msgid "Progress: Generate PDF file"
 msgstr ""
 
-#: aleksis/core/util/pdf.py:106
+#: aleksis/core/util/pdf.py:119
 msgid "Generating PDF file …"
 msgstr ""
 
-#: aleksis/core/util/pdf.py:107
+#: aleksis/core/util/pdf.py:120
 msgid "The PDF file has been generated successfully."
 msgstr ""
 
-#: aleksis/core/util/pdf.py:108
+#: aleksis/core/util/pdf.py:121
 msgid "There was a problem while generating the PDF file."
 msgstr ""
 
-#: aleksis/core/util/pdf.py:111
+#: aleksis/core/util/pdf.py:124
 msgid "Download PDF"
 msgstr ""
 
-#: aleksis/core/views.py:251
+#: aleksis/core/views.py:285
 msgid "The school term has been created."
 msgstr ""
 
-#: aleksis/core/views.py:263
+#: aleksis/core/views.py:297
 msgid "The school term has been saved."
 msgstr ""
 
-#: aleksis/core/views.py:387
+#: aleksis/core/views.py:417
 msgid "The child groups were successfully saved."
 msgstr ""
 
-#: aleksis/core/views.py:406 aleksis/core/views.py:416
+#: aleksis/core/views.py:436 aleksis/core/views.py:446
 msgid "The person has been saved."
 msgstr ""
 
-#: aleksis/core/views.py:466
+#: aleksis/core/views.py:496
 msgid "The group has been saved."
 msgstr ""
 
-#: aleksis/core/views.py:563
+#: aleksis/core/views.py:593
 msgid "The announcement has been saved."
 msgstr ""
 
-#: aleksis/core/views.py:579
+#: aleksis/core/views.py:609
 msgid "The announcement has been deleted."
 msgstr ""
 
-#: aleksis/core/views.py:663
+#: aleksis/core/views.py:677
+msgid "The requested preference registry does not exist"
+msgstr ""
+
+#: aleksis/core/views.py:696
 msgid "The preferences have been saved successfully."
 msgstr ""
 
-#: aleksis/core/views.py:687
+#: aleksis/core/views.py:720
 msgid "The person has been deleted."
 msgstr ""
 
-#: aleksis/core/views.py:701
+#: aleksis/core/views.py:734
 msgid "The group has been deleted."
 msgstr ""
 
-#: aleksis/core/views.py:733
+#: aleksis/core/views.py:766
 msgid "The additional_field has been saved."
 msgstr ""
 
-#: aleksis/core/views.py:767
+#: aleksis/core/views.py:800
 msgid "The additional field has been deleted."
 msgstr ""
 
-#: aleksis/core/views.py:792
+#: aleksis/core/views.py:825
 msgid "The group type has been saved."
 msgstr ""
 
-#: aleksis/core/views.py:822
+#: aleksis/core/views.py:855
 msgid "The group type has been deleted."
 msgstr ""
 
-#: aleksis/core/views.py:855
+#: aleksis/core/views.py:888
 msgid "Progress: Run data checks"
 msgstr ""
 
-#: aleksis/core/views.py:856
+#: aleksis/core/views.py:889
 msgid "Run data checks …"
 msgstr ""
 
-#: aleksis/core/views.py:857
+#: aleksis/core/views.py:890
 msgid "The data checks were run successfully."
 msgstr ""
 
-#: aleksis/core/views.py:858
+#: aleksis/core/views.py:891
 msgid "There was a problem while running data checks."
 msgstr ""
 
-#: aleksis/core/views.py:874
+#: aleksis/core/views.py:907
 #, python-brace-format
 msgid "The solve option '{solve_option_obj.verbose_name}' "
 msgstr ""
 
-#: aleksis/core/views.py:916
+#: aleksis/core/views.py:917
+msgid "The requested solve option does not exist"
+msgstr ""
+
+#: aleksis/core/views.py:949
 msgid "The dashboard widget has been saved."
 msgstr ""
 
-#: aleksis/core/views.py:946
+#: aleksis/core/views.py:979
 msgid "The dashboard widget has been created."
 msgstr ""
 
-#: aleksis/core/views.py:956
+#: aleksis/core/views.py:989
 msgid "The dashboard widget has been deleted."
 msgstr ""
 
-#: aleksis/core/views.py:1023
+#: aleksis/core/views.py:1060
 msgid "Your dashboard configuration has been saved successfully."
 msgstr ""
 
-#: aleksis/core/views.py:1025
+#: aleksis/core/views.py:1062
 msgid "The configuration of the default dashboard has been saved successfully."
 msgstr ""
 
-#: aleksis/core/views.py:1153
+#: aleksis/core/views.py:1138
+#, python-brace-format
+msgid "The invitation was successfully created. The invitation code is {code}"
+msgstr ""
+
+#: aleksis/core/views.py:1229
+msgid "We have successfully assigned the permissions."
+msgstr ""
+
+#: aleksis/core/views.py:1239
+msgid "The global user permission has been deleted."
+msgstr ""
+
+#: aleksis/core/views.py:1249
+msgid "The global group permission has been deleted."
+msgstr ""
+
+#: aleksis/core/views.py:1259
+msgid "The object user permission has been deleted."
+msgstr ""
+
+#: aleksis/core/views.py:1269
+msgid "The object group permission has been deleted."
+msgstr ""
+
+#: aleksis/core/views.py:1337
+msgid "The requested PDF file does not exist"
+msgstr ""
+
+#: aleksis/core/views.py:1346 aleksis/core/views.py:1350
+msgid "The requested task does not exist or is not accessible"
+msgstr ""
+
+#: aleksis/core/views.py:1388
 msgid "The third-party account could not be disconnected because it is the only login method available."
 msgstr ""
 
-#: aleksis/core/views.py:1160
+#: aleksis/core/views.py:1395
 msgid "The third-party account has been successfully disconnected."
 msgstr ""
+
+#: aleksis/core/views.py:1466
+msgid "Person was invited successfully and an email with further instructions has been send to them."
+msgstr ""
+
+#: aleksis/core/views.py:1477
+msgid "Person was already invited."
+msgstr ""
diff --git a/aleksis/core/locale/ar/LC_MESSAGES/djangojs.po b/aleksis/core/locale/ar/LC_MESSAGES/djangojs.po
index 58d7ae38a2326acb79508cd05c32cab673c58fe9..8a53fc2fc168227f37f344c17ae359b5d5764596 100644
--- a/aleksis/core/locale/ar/LC_MESSAGES/djangojs.po
+++ b/aleksis/core/locale/ar/LC_MESSAGES/djangojs.po
@@ -8,7 +8,7 @@ msgid ""
 msgstr ""
 "Project-Id-Version: PACKAGE VERSION\n"
 "Report-Msgid-Bugs-To: \n"
-"POT-Creation-Date: 2021-10-28 16:18+0200\n"
+"POT-Creation-Date: 2022-02-08 23:16+0000\n"
 "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
 "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
 "Language-Team: LANGUAGE <LL@li.org>\n"
@@ -18,18 +18,18 @@ msgstr ""
 "Content-Transfer-Encoding: 8bit\n"
 "Plural-Forms: nplurals=6; plural=n==0 ? 0 : n==1 ? 1 : n==2 ? 2 : n%100>=3 && n%100<=10 ? 3 : n%100>=11 && n%100<=99 ? 4 : 5;\n"
 
-#: aleksis/core/static/js/main.js:15
+#: aleksis/core/static/js/main.js:66
 msgid "Today"
 msgstr ""
 
-#: aleksis/core/static/js/main.js:16
+#: aleksis/core/static/js/main.js:67
 msgid "Cancel"
 msgstr ""
 
-#: aleksis/core/static/js/main.js:17
+#: aleksis/core/static/js/main.js:68
 msgid "OK"
 msgstr ""
 
-#: aleksis/core/static/js/main.js:127
+#: aleksis/core/static/js/main.js:191
 msgid "This page may contain outdated information since there is no internet connection."
 msgstr ""
diff --git a/aleksis/core/locale/de_DE/LC_MESSAGES/django.po b/aleksis/core/locale/de_DE/LC_MESSAGES/django.po
index f392000d5c400048743b02d15cfca5dbb4efd644..56ff3ab652cb345ae714d6b1eb8b54e7ae5f27a2 100644
--- a/aleksis/core/locale/de_DE/LC_MESSAGES/django.po
+++ b/aleksis/core/locale/de_DE/LC_MESSAGES/django.po
@@ -7,9 +7,9 @@ msgid ""
 msgstr ""
 "Project-Id-Version: AlekSIS (School Information System) 0.1\n"
 "Report-Msgid-Bugs-To: \n"
-"POT-Creation-Date: 2021-10-28 16:18+0200\n"
-"PO-Revision-Date: 2021-10-29 14:28+0000\n"
-"Last-Translator: Jonathan Weth <teckids@jonathanweth.de>\n"
+"POT-Creation-Date: 2022-02-08 23:16+0000\n"
+"PO-Revision-Date: 2022-02-08 23:24+0000\n"
+"Last-Translator: Tom Teichler <tom.teichler@teckids.org>\n"
 "Language-Team: German <https://translate.edugit.org/projects/aleksis/"
 "aleksis-core/de/>\n"
 "Language: de_DE\n"
@@ -19,31 +19,38 @@ msgstr ""
 "Plural-Forms: nplurals=2; plural=n != 1;\n"
 "X-Generator: Weblate 4.8\n"
 
-#: aleksis/core/apps.py:150
+#: aleksis/core/apps.py:166
 msgid "OpenID Connect scope"
 msgstr "OpenID-Connect-Scope"
 
-#: aleksis/core/apps.py:151
+#: aleksis/core/apps.py:167
 msgid "Given name, family name, link to profile and picture if existing."
-msgstr "Vorname, Nachname, Link zum Profil und Bild falls vorhanden"
+msgstr "Vorname, Nachname, Link zum Profil und Bild falls vorhanden."
 
-#: aleksis/core/apps.py:152
+#: aleksis/core/apps.py:168
 msgid "Full home postal address"
 msgstr "Vollständige Postanschrift"
 
-#: aleksis/core/apps.py:153
+#: aleksis/core/apps.py:169
 msgid "Email address"
 msgstr "E-Mail-Adresse"
 
-#: aleksis/core/apps.py:154
+#: aleksis/core/apps.py:170
 msgid "Home and mobile phone"
 msgstr "Festnetz- und Mobilfunknummer"
 
-#: aleksis/core/data_checks.py:55
+#: aleksis/core/apps.py:171 aleksis/core/forms.py:220 aleksis/core/menus.py:265
+#: aleksis/core/models.py:462 aleksis/core/templates/core/group/list.html:8
+#: aleksis/core/templates/core/group/list.html:9
+#: aleksis/core/templates/core/person/full.html:250
+msgid "Groups"
+msgstr "Gruppen"
+
+#: aleksis/core/data_checks.py:56
 msgid "Ignore problem"
 msgstr "Problem ignorieren"
 
-#: aleksis/core/data_checks.py:184
+#: aleksis/core/data_checks.py:185
 #, python-brace-format
 msgid "Solve option '{solve_option_obj.verbose_name}' "
 msgstr "Lösungsoption \"{solve_option_obj.verbose_name}\" "
@@ -60,8 +67,8 @@ msgstr "Sicherstellen, dass es keine kaputten Dashboard-Widgets gibt."
 msgid "The DashboardWidget was reported broken automatically."
 msgstr "Das Dashboard-Widget wurde automatisch als kaputt gemeldet."
 
-#: aleksis/core/filters.py:37 aleksis/core/templates/core/base.html:83
-#: aleksis/core/templates/core/base.html:84
+#: aleksis/core/filters.py:42 aleksis/core/templates/core/base.html:105
+#: aleksis/core/templates/core/base.html:106
 #: aleksis/core/templates/core/group/list.html:20
 #: aleksis/core/templates/core/person/list.html:24
 #: aleksis/core/templates/search/search.html:7
@@ -69,132 +76,181 @@ msgstr "Das Dashboard-Widget wurde automatisch als kaputt gemeldet."
 msgid "Search"
 msgstr "Suchen"
 
-#: aleksis/core/filters.py:53
+#: aleksis/core/filters.py:59
 msgid "Search by name"
 msgstr "Nach Namen suchen"
 
-#: aleksis/core/filters.py:65
+#: aleksis/core/filters.py:71
 msgid "Search by contact details"
 msgstr "Nach Kontaktdetails suchen"
 
-#: aleksis/core/forms.py:41 aleksis/core/forms.py:387
+#: aleksis/core/filters.py:92
+msgid "Permission"
+msgstr "Berechtigung"
+
+#: aleksis/core/filters.py:100
+msgid "Content type"
+msgstr "Inhaltstyp"
+
+#: aleksis/core/filters.py:113 aleksis/core/models.py:688
+msgid "User"
+msgstr "Benutzer"
+
+#: aleksis/core/filters.py:135 aleksis/core/models.py:461
+msgid "Group"
+msgstr "Gruppe"
+
+#: aleksis/core/forms.py:50 aleksis/core/forms.py:581
 msgid "Base data"
 msgstr "Basisdaten"
 
-#: aleksis/core/forms.py:47
+#: aleksis/core/forms.py:55
 msgid "Address"
 msgstr "Adresse"
 
-#: aleksis/core/forms.py:48
+#: aleksis/core/forms.py:56 aleksis/core/forms.py:590
 msgid "Contact data"
 msgstr "Kontaktdaten"
 
-#: aleksis/core/forms.py:50
+#: aleksis/core/forms.py:58
 msgid "Advanced personal data"
 msgstr "Zusätzliche persönliche Daten"
 
-#: aleksis/core/forms.py:93
+#: aleksis/core/forms.py:106
 msgid "New user"
 msgstr "Neuer Benutzer"
 
-#: aleksis/core/forms.py:93
+#: aleksis/core/forms.py:106
 msgid "Create a new account"
 msgstr "Neues Benutzerkonto erstellen"
 
-#: aleksis/core/forms.py:124
+#: aleksis/core/forms.py:132
 msgid "You cannot set a new username when also selecting an existing user."
 msgstr "Sie können keine neuen Benutzer erstellen, wenn Sie gleichzeitig einen existierenden Benutzer auswählen."
 
-#: aleksis/core/forms.py:128
+#: aleksis/core/forms.py:136
 msgid "This username is already in use."
 msgstr "Dieser Benutzername wird bereits genutzt."
 
-#: aleksis/core/forms.py:145 aleksis/core/models.py:117
+#: aleksis/core/forms.py:153 aleksis/core/models.py:130
 msgid "School term"
 msgstr "Schuljahr"
 
-#: aleksis/core/forms.py:146
+#: aleksis/core/forms.py:154
 msgid "Common data"
 msgstr "Allgemeine Daten"
 
-#: aleksis/core/forms.py:147 aleksis/core/forms.py:196
-#: aleksis/core/menus.py:238 aleksis/core/models.py:140
+#: aleksis/core/forms.py:155 aleksis/core/forms.py:207
+#: aleksis/core/menus.py:254 aleksis/core/models.py:153
 #: aleksis/core/templates/core/person/list.html:8
 #: aleksis/core/templates/core/person/list.html:9
 msgid "Persons"
 msgstr "Personen"
 
-#: aleksis/core/forms.py:148
+#: aleksis/core/forms.py:156 aleksis/core/forms.py:592
 msgid "Additional data"
 msgstr "Zusätzliche Datne"
 
-#: aleksis/core/forms.py:188 aleksis/core/forms.py:191
-#: aleksis/core/models.py:60
+#: aleksis/core/forms.py:157 aleksis/core/models.py:206
+#: aleksis/core/models.py:514
+msgid "Photo"
+msgstr "Foto"
+
+#: aleksis/core/forms.py:199 aleksis/core/forms.py:202
+#: aleksis/core/models.py:73
 msgid "Date"
 msgstr "Datum"
 
-#: aleksis/core/forms.py:189 aleksis/core/forms.py:192
-#: aleksis/core/models.py:68
+#: aleksis/core/forms.py:200 aleksis/core/forms.py:203
+#: aleksis/core/models.py:81
 msgid "Time"
 msgstr "Zeit"
 
-#: aleksis/core/forms.py:209 aleksis/core/menus.py:249
-#: aleksis/core/models.py:398 aleksis/core/templates/core/group/list.html:8
-#: aleksis/core/templates/core/group/list.html:9
-#: aleksis/core/templates/core/person/full.html:144
-msgid "Groups"
-msgstr "Gruppen"
-
-#: aleksis/core/forms.py:219
+#: aleksis/core/forms.py:233
 msgid "From when until when should the announcement be displayed?"
 msgstr "Von wann bis wann soll die Ankündigung angezeigt werden?"
 
-#: aleksis/core/forms.py:222
+#: aleksis/core/forms.py:236
 msgid "Who should see the announcement?"
 msgstr "Wer soll die Ankündigung sehen?"
 
-#: aleksis/core/forms.py:223
+#: aleksis/core/forms.py:237
 msgid "Write your announcement:"
 msgstr "Schreiben Sie ihre Ankündigung:"
 
-#: aleksis/core/forms.py:262
+#: aleksis/core/forms.py:276
 msgid "You are not allowed to create announcements which are only valid in the past."
 msgstr "Sie dürfen keine Ankündigungen erstellen, die nur für die Vergangenheit gültig sind."
 
-#: aleksis/core/forms.py:266
+#: aleksis/core/forms.py:280
 msgid "The from date and time must be earlier then the until date and time."
 msgstr "Das Startdatum und die Startzeit müssen vor dem Enddatum und der Endzeit sein."
 
-#: aleksis/core/forms.py:275
+#: aleksis/core/forms.py:289
 msgid "You need at least one recipient."
 msgstr "Sie benötigen mindestens einen Empfänger."
 
-#: aleksis/core/forms.py:389
+#: aleksis/core/forms.py:398
+msgid "Invitation code"
+msgstr "Einladungscode"
+
+#: aleksis/core/forms.py:399
+msgid "Please enter your invitation code."
+msgstr "Bitte geben Sie Ihren Einladungscode ein."
+
+#: aleksis/core/forms.py:418 aleksis/core/models.py:181
+msgid "First name"
+msgstr "Vorname"
+
+#: aleksis/core/forms.py:419 aleksis/core/models.py:182
+msgid "Last name"
+msgstr "Nachname"
+
+#: aleksis/core/forms.py:428
+msgid "A person is using this e-mail address"
+msgstr "Eine Person nutzt diese E-Mail-Adresse"
+
+#: aleksis/core/forms.py:456
+msgid "Who should get the permission?"
+msgstr "Wer soll die Berechtigung erhalten?"
+
+#: aleksis/core/forms.py:457
+msgid "On what?"
+msgstr "Auf was?"
+
+#: aleksis/core/forms.py:483
+msgid "Select objects which the permission should be granted for:"
+msgstr "Wählen Sie die Objekte aus, für welche die Berechtigung vergeben werden soll:"
+
+#: aleksis/core/forms.py:486
+msgid "Grant the permission for all objects"
+msgstr "Vergebe die Berechtigung für alle Objekte"
+
+#: aleksis/core/forms.py:494
+msgid "You must select at least one group or person which should get the permission."
+msgstr "Sie müssen mindestens eine Gruppe oder Person auswählen, welche die Berechtigung erhalten soll."
+
+#: aleksis/core/forms.py:499
+msgid "You must grant the permission to all objects and/or to some objects."
+msgstr "Sie müssen die Berechtigung auf alle Objekte und/oder für einige Objekte vergeben."
+
+#: aleksis/core/forms.py:586
+msgid "Adress data"
+msgstr "Adressdaten"
+
+#: aleksis/core/forms.py:598
 msgid "Account data"
 msgstr "Kontodaten"
 
-#: aleksis/core/forms.py:391
-msgid "Consents"
-msgstr "Zustimmungen"
-
-#: aleksis/core/forms.py:396
+#: aleksis/core/forms.py:605
 msgid "Password"
 msgstr "Passwort"
 
-#: aleksis/core/forms.py:402
+#: aleksis/core/forms.py:608
 msgid "Password (again)"
 msgstr "Passwort wiederholen"
 
-#: aleksis/core/forms.py:411
-#, python-brace-format
-msgid "I have read the <a href='{privacy_policy}'>Privacy policy</a> and agree with them."
-msgstr "Ich habe die <a href='{privacy_policy}'>Datenschutzerklärung</a> gelesen und stimme ihr zu."
-
-#: aleksis/core/forms.py:435
-msgid "You must type the same password each time."
-msgstr "Sie müssen zweimal das gleiche Passwort eingeben."
-
-#: aleksis/core/forms.py:580
+#: aleksis/core/forms.py:775
 msgid "No valid selection."
 msgstr "Keine gültige Auswahl."
 
@@ -202,67 +258,65 @@ msgstr "Keine gültige Auswahl."
 msgid "There are unresolved data problems."
 msgstr "Es gibt ungelöste Datenprobleme."
 
-#: aleksis/core/health_checks.py:38
-msgid "The backup folder doesn't exist."
-msgstr "Der Backup-Ordner existiert nicht."
+#: aleksis/core/health_checks.py:40
+msgid "Error accessing backup storage: {}"
+msgstr "Fehler beim Zugriff auf Backup-Storage: {}"
 
-#: aleksis/core/health_checks.py:47
-#, python-brace-format
-msgid "Last backup {time_gone_since_backup}!"
-msgstr "Letztes Backup: {time_gone_since_backup}!"
+#: aleksis/core/health_checks.py:50
+msgid "Last backup {}!"
+msgstr "Letztes Backup {}!"
 
-#: aleksis/core/health_checks.py:49
+#: aleksis/core/health_checks.py:52
 msgid "No backup found!"
 msgstr "Kein Backup gefunden!"
 
-#: aleksis/core/health_checks.py:76
+#: aleksis/core/health_checks.py:79
 msgid "No backup result found!"
 msgstr "Kein Backupergebnis gefunden!"
 
-#: aleksis/core/health_checks.py:78
-#, python-brace-format
-msgid "{task.status} - {task.result}"
-msgstr "{task.status} - {task.result}"
-
 #: aleksis/core/menus.py:9 aleksis/core/templates/two_factor/core/login.html:6
-#: aleksis/core/templates/two_factor/core/login.html:22
-#: aleksis/core/templates/two_factor/core/login.html:76
+#: aleksis/core/templates/two_factor/core/login.html:32
+#: aleksis/core/templates/two_factor/core/login.html:95
 msgid "Login"
 msgstr "Anmelden"
 
-#: aleksis/core/menus.py:15 aleksis/core/templates/account/signup.html:20
+#: aleksis/core/menus.py:15 aleksis/core/templates/account/signup.html:22
 #: aleksis/core/templates/socialaccount/signup.html:23
 msgid "Sign up"
 msgstr "Registrieren"
 
-#: aleksis/core/menus.py:24
+#: aleksis/core/menus.py:24 aleksis/core/templates/invitations/enter.html:7
+msgid "Accept invitation"
+msgstr "Einladung akzeptieren"
+
+#: aleksis/core/menus.py:33
 msgid "Dashboard"
 msgstr "Dashboard"
 
-#: aleksis/core/menus.py:32 aleksis/core/models.py:605
-#: aleksis/core/preferences.py:26
+#: aleksis/core/menus.py:41 aleksis/core/models.py:734
+#: aleksis/core/preferences.py:29
 #: aleksis/core/templates/core/notifications.html:4
 #: aleksis/core/templates/core/notifications.html:5
 msgid "Notifications"
 msgstr "Benachrichtigungen"
 
-#: aleksis/core/menus.py:41
+#: aleksis/core/menus.py:53
 msgid "Account"
 msgstr "Konto"
 
-#: aleksis/core/menus.py:48
+#: aleksis/core/menus.py:60
 msgid "Stop impersonation"
 msgstr "Verkleidung beenden"
 
-#: aleksis/core/menus.py:57 aleksis/core/templates/core/base.html:62
+#: aleksis/core/menus.py:69 aleksis/core/templates/core/base.html:80
 msgid "Logout"
 msgstr "Abmelden"
 
-#: aleksis/core/menus.py:63
+#: aleksis/core/menus.py:75
 msgid "2FA"
 msgstr "2FA"
 
-#: aleksis/core/menus.py:69
+#: aleksis/core/menus.py:83
 #: aleksis/core/templates/account/password_change.html:5
 #: aleksis/core/templates/account/password_change.html:6
 #: aleksis/core/templates/account/password_change.html:19
@@ -274,847 +328,875 @@ msgstr "2FA"
 msgid "Change password"
 msgstr "Passwort ändern"
 
-#: aleksis/core/menus.py:81
+#: aleksis/core/menus.py:95
 msgid "Me"
 msgstr "Ich"
 
-#: aleksis/core/menus.py:90
+#: aleksis/core/menus.py:104
 #: aleksis/core/templates/dynamic_preferences/form.html:5
 msgid "Preferences"
 msgstr "Einstellungen"
 
-#: aleksis/core/menus.py:99
+#: aleksis/core/menus.py:113
 msgid "Third-party accounts"
 msgstr "Drittanbieter-Konten"
 
-#: aleksis/core/menus.py:108
+#: aleksis/core/menus.py:122
 #: aleksis/core/templates/oauth2_provider/authorized-tokens.html:5
 #: aleksis/core/templates/oauth2_provider/authorized-tokens.html:6
 msgid "Authorized applications"
 msgstr "Autorisierte Anwendungen"
 
-#: aleksis/core/menus.py:119
+#: aleksis/core/menus.py:133
 msgid "Admin"
 msgstr "Admin"
 
-#: aleksis/core/menus.py:127 aleksis/core/models.py:704
+#: aleksis/core/menus.py:141 aleksis/core/models.py:834
 #: aleksis/core/templates/core/announcement/list.html:7
 #: aleksis/core/templates/core/announcement/list.html:8
 msgid "Announcements"
 msgstr "Ankündigungen"
 
-#: aleksis/core/menus.py:138 aleksis/core/models.py:118
+#: aleksis/core/menus.py:152 aleksis/core/models.py:131
 #: aleksis/core/templates/core/school_term/list.html:8
 #: aleksis/core/templates/core/school_term/list.html:9
 msgid "School terms"
 msgstr "Schuljahre"
 
-#: aleksis/core/menus.py:149
+#: aleksis/core/menus.py:163
 #: aleksis/core/templates/core/dashboard_widget/list.html:8
 #: aleksis/core/templates/core/dashboard_widget/list.html:9
 msgid "Dashboard widgets"
 msgstr "Dashboard-Widgets"
 
-#: aleksis/core/menus.py:160
+#: aleksis/core/menus.py:174
 #: aleksis/core/templates/core/management/data_management.html:6
 #: aleksis/core/templates/core/management/data_management.html:7
 msgid "Data management"
 msgstr "Datenverwaltung"
 
-#: aleksis/core/menus.py:171
+#: aleksis/core/menus.py:185
 #: aleksis/core/templates/core/pages/system_status.html:5
 #: aleksis/core/templates/core/pages/system_status.html:7
 msgid "System status"
 msgstr "Systemstatus"
 
-#: aleksis/core/menus.py:182
-msgid "Impersonation"
-msgstr "Verkleidung"
-
-#: aleksis/core/menus.py:193
+#: aleksis/core/menus.py:196
 msgid "Configuration"
 msgstr "Konfiguration"
 
-#: aleksis/core/menus.py:204 aleksis/core/templates/core/data_check/list.html:9
+#: aleksis/core/menus.py:207 aleksis/core/templates/core/data_check/list.html:9
 #: aleksis/core/templates/core/data_check/list.html:10
 msgid "Data checks"
 msgstr "Datenprüfungen"
 
-#: aleksis/core/menus.py:210
+#: aleksis/core/menus.py:213 aleksis/core/templates/core/perms/list.html:13
+#: aleksis/core/templates/core/perms/list.html:14
+msgid "Manage permissions"
+msgstr "Berechtigungen verwalten"
+
+#: aleksis/core/menus.py:224
 msgid "Backend Admin"
 msgstr "Backend-Administration"
 
-#: aleksis/core/menus.py:216
-#: aleksis/core/templates/oauth2_provider/application_detail.html:5
-#: aleksis/core/templates/oauth2_provider/application_list.html:5
+#: aleksis/core/menus.py:232
+#: aleksis/core/templates/oauth2_provider/application/list.html:5
+#: aleksis/core/templates/oauth2_provider/application/list.html:6
 msgid "OAuth2 Applications"
 msgstr "OAuth2-Anwendungen"
 
-#: aleksis/core/menus.py:229
+#: aleksis/core/menus.py:245
 msgid "People"
 msgstr "Leute"
 
-#: aleksis/core/menus.py:260 aleksis/core/models.py:958
+#: aleksis/core/menus.py:276 aleksis/core/models.py:1055
 #: aleksis/core/templates/core/group_type/list.html:8
 #: aleksis/core/templates/core/group_type/list.html:9
 msgid "Group types"
 msgstr "Gruppentypen"
 
-#: aleksis/core/menus.py:271
+#: aleksis/core/menus.py:287
 msgid "Groups and child groups"
 msgstr "Gruppen und Kindgruppen"
 
-#: aleksis/core/menus.py:282 aleksis/core/models.py:446
+#: aleksis/core/menus.py:298 aleksis/core/models.py:510
 #: aleksis/core/templates/core/additional_field/list.html:8
 #: aleksis/core/templates/core/additional_field/list.html:9
 msgid "Additional fields"
 msgstr "Zusätzliche Felder"
 
-#: aleksis/core/menus.py:297
+#: aleksis/core/menus.py:309
+msgid "Invite person"
+msgstr "Person einladen"
+
+#: aleksis/core/menus.py:322
 #: aleksis/core/templates/core/group/child_groups.html:7
 #: aleksis/core/templates/core/group/child_groups.html:9
 msgid "Assign child groups to groups"
 msgstr "Kindgruppen zu Gruppen zuordnen"
 
-#: aleksis/core/mixins.py:498
+#: aleksis/core/mixins.py:511
 msgid "Linked school term"
 msgstr "Zugeordnetes Schuljahr"
 
-#: aleksis/core/models.py:58
+#: aleksis/core/models.py:71
 msgid "Boolean (Yes/No)"
 msgstr "Boolean (Ja/Nein)"
 
-#: aleksis/core/models.py:59
+#: aleksis/core/models.py:72
 msgid "Text (one line)"
 msgstr "Text (eine Zeile)"
 
-#: aleksis/core/models.py:61
+#: aleksis/core/models.py:74
 msgid "Date and time"
 msgstr "Datum und Uhrzeit"
 
-#: aleksis/core/models.py:62
+#: aleksis/core/models.py:75
 msgid "Decimal number"
 msgstr "Dezimalzahl"
 
-#: aleksis/core/models.py:63 aleksis/core/models.py:186
+#: aleksis/core/models.py:76 aleksis/core/models.py:199
 msgid "E-mail address"
 msgstr "E-Mail-Adresse"
 
-#: aleksis/core/models.py:64
+#: aleksis/core/models.py:77
 msgid "Integer"
 msgstr "Ganze Zahl"
 
-#: aleksis/core/models.py:65
+#: aleksis/core/models.py:78
 msgid "IP address"
 msgstr "IP-Adresse"
 
-#: aleksis/core/models.py:66
+#: aleksis/core/models.py:79
 msgid "Boolean or empty (Yes/No/Neither)"
 msgstr "Boolean oder leer (Ja/Nein/weder)"
 
-#: aleksis/core/models.py:67
+#: aleksis/core/models.py:80
 msgid "Text (multi-line)"
 msgstr "Text (mehrzeilig)"
 
-#: aleksis/core/models.py:69
+#: aleksis/core/models.py:82
 msgid "URL / Link"
 msgstr "URL / Link"
 
-#: aleksis/core/models.py:81 aleksis/core/models.py:927
+#: aleksis/core/models.py:94 aleksis/core/models.py:1024
 msgid "Name"
 msgstr "Name"
 
-#: aleksis/core/models.py:83
+#: aleksis/core/models.py:96
 msgid "Start date"
 msgstr "Startdatum"
 
-#: aleksis/core/models.py:84
+#: aleksis/core/models.py:97
 msgid "End date"
 msgstr "Enddatum"
 
-#: aleksis/core/models.py:103
+#: aleksis/core/models.py:116
 msgid "The start date must be earlier than the end date."
 msgstr "Das Startdatum muss vor dem Enddatum liegen."
 
-#: aleksis/core/models.py:110
+#: aleksis/core/models.py:123
 msgid "There is already a school term for this time or a part of this time."
 msgstr "Es gibt bereits ein Schuljahr für diesen Zeitraum oder einen Teilzeitraum."
 
-#: aleksis/core/models.py:139 aleksis/core/models.py:876
+#: aleksis/core/models.py:152 aleksis/core/models.py:973
 msgid "Person"
 msgstr "Person"
 
-#: aleksis/core/models.py:142
+#: aleksis/core/models.py:155
 msgid "Can view address"
 msgstr "Kann Adresse sehen"
 
-#: aleksis/core/models.py:143
+#: aleksis/core/models.py:156
 msgid "Can view contact details"
 msgstr "Kann Kontaktdetails sehen"
 
-#: aleksis/core/models.py:144
+#: aleksis/core/models.py:157
 msgid "Can view photo"
 msgstr "Kann Foto sehen"
 
-#: aleksis/core/models.py:145
+#: aleksis/core/models.py:158
+msgid "Can view avatar image"
+msgstr "Kann Avatar-Bild sehen"
+
+#: aleksis/core/models.py:159
 msgid "Can view persons groups"
 msgstr "Kann Gruppen einer Person sehen"
 
-#: aleksis/core/models.py:146
+#: aleksis/core/models.py:160
 msgid "Can view personal details"
 msgstr "Kann persönliche Daten sehen"
 
-#: aleksis/core/models.py:156
+#: aleksis/core/models.py:170
 msgid "female"
 msgstr "weiblich"
 
-#: aleksis/core/models.py:156
+#: aleksis/core/models.py:170
 msgid "male"
 msgstr "männlich"
 
-#: aleksis/core/models.py:164
+#: aleksis/core/models.py:178 aleksis/core/models.py:1227
 msgid "Linked user"
 msgstr "Verknüpfter Benutzer"
 
-#: aleksis/core/models.py:166
-msgid "Is person active?"
-msgstr "Ist die Person aktiv?"
-
-#: aleksis/core/models.py:168
-msgid "First name"
-msgstr "Vorname"
-
-#: aleksis/core/models.py:169
-msgid "Last name"
-msgstr "Nachname"
-
-#: aleksis/core/models.py:171
+#: aleksis/core/models.py:184
 msgid "Additional name(s)"
 msgstr "Zusätzliche Namen"
 
-#: aleksis/core/models.py:175 aleksis/core/models.py:415
+#: aleksis/core/models.py:188 aleksis/core/models.py:479
 msgid "Short name"
 msgstr "Kurzname"
 
-#: aleksis/core/models.py:178
+#: aleksis/core/models.py:191
 msgid "Street"
 msgstr "Straße"
 
-#: aleksis/core/models.py:179
+#: aleksis/core/models.py:192
 msgid "Street number"
 msgstr "Hausnummer"
 
-#: aleksis/core/models.py:180
+#: aleksis/core/models.py:193
 msgid "Postal code"
 msgstr "Postleitzahl"
 
-#: aleksis/core/models.py:181
+#: aleksis/core/models.py:194
 msgid "Place"
 msgstr "Ort"
 
-#: aleksis/core/models.py:183
+#: aleksis/core/models.py:196 aleksis/core/templates/core/person/full.html:172
 msgid "Home phone"
 msgstr "Festnetz"
 
-#: aleksis/core/models.py:184
+#: aleksis/core/models.py:197 aleksis/core/templates/core/person/full.html:182
 msgid "Mobile phone"
 msgstr "Handy"
 
-#: aleksis/core/models.py:188
+#: aleksis/core/models.py:201
 msgid "Date of birth"
 msgstr "Geburtsdatum"
 
-#: aleksis/core/models.py:189
+#: aleksis/core/models.py:202
+msgid "Place of birth"
+msgstr "Geburtsort"
+
+#: aleksis/core/models.py:203
 msgid "Sex"
 msgstr "Geschlecht"
 
-#: aleksis/core/models.py:191
-msgid "Photo"
-msgstr "Foto"
+#: aleksis/core/models.py:210 aleksis/core/models.py:518
+msgid "This is an official photo, used for official documents and for internal use cases."
+msgstr "Dies ist ein offizielles Foto, genutzt für offizielle Dokumente und interne Zwecke."
+
+#: aleksis/core/models.py:215 aleksis/core/models.py:522
+msgid "Display picture / Avatar"
+msgstr "Bild/Avatar anzeigen"
 
-#: aleksis/core/models.py:195 aleksis/core/templates/core/person/full.html:137
+#: aleksis/core/models.py:218 aleksis/core/models.py:525
+msgid "This is a picture or an avatar for public display."
+msgstr "Dies ist ein Bild oder ein Avatar für die öffentliche Darstellung."
+
+#: aleksis/core/models.py:223 aleksis/core/templates/core/person/full.html:239
 msgid "Guardians / Parents"
 msgstr "Erziehungsberechtigte / Eltern"
 
-#: aleksis/core/models.py:202
+#: aleksis/core/models.py:230
 msgid "Primary group"
 msgstr "Primärgruppe"
 
-#: aleksis/core/models.py:205 aleksis/core/models.py:563
-#: aleksis/core/models.py:587 aleksis/core/models.py:672
-#: aleksis/core/models.py:951 aleksis/core/templates/core/person/full.html:120
+#: aleksis/core/models.py:233 aleksis/core/models.py:692
+#: aleksis/core/models.py:716 aleksis/core/models.py:801
+#: aleksis/core/models.py:1048
 msgid "Description"
 msgstr "Beschreibung"
 
-#: aleksis/core/models.py:370
+#: aleksis/core/models.py:434
 msgid "Title of field"
 msgstr "Feldtitel"
 
-#: aleksis/core/models.py:372
+#: aleksis/core/models.py:436
 msgid "Type of field"
 msgstr "Feldtyp"
 
-#: aleksis/core/models.py:379
+#: aleksis/core/models.py:443
 msgid "Addtitional field for groups"
 msgstr "Zusätzliche Felder für Gruppen"
 
-#: aleksis/core/models.py:380
+#: aleksis/core/models.py:444
 msgid "Addtitional fields for groups"
 msgstr "Zusätzliche Felder für Gruppen"
 
-#: aleksis/core/models.py:397
-msgid "Group"
-msgstr "Gruppe"
-
-#: aleksis/core/models.py:400
+#: aleksis/core/models.py:464
 msgid "Can assign child groups to groups"
 msgstr "Kann Kindgruppen zu Gruppen zuordnen"
 
-#: aleksis/core/models.py:401
+#: aleksis/core/models.py:465
 msgid "Can view statistics about group."
 msgstr "Kann Statistiken über Gruppen sehen."
 
-#: aleksis/core/models.py:413
+#: aleksis/core/models.py:477
 msgid "Long name"
 msgstr "Langname"
 
-#: aleksis/core/models.py:423 aleksis/core/templates/core/group/full.html:85
+#: aleksis/core/models.py:487 aleksis/core/templates/core/group/full.html:85
 msgid "Members"
 msgstr "Mitglieder"
 
-#: aleksis/core/models.py:426 aleksis/core/templates/core/group/full.html:82
+#: aleksis/core/models.py:490 aleksis/core/templates/core/group/full.html:82
 msgid "Owners"
 msgstr "Leiter/-innen"
 
-#: aleksis/core/models.py:433 aleksis/core/templates/core/group/full.html:55
+#: aleksis/core/models.py:497 aleksis/core/templates/core/group/full.html:55
 msgid "Parent groups"
 msgstr "Übergeordnete Gruppen"
 
-#: aleksis/core/models.py:441
+#: aleksis/core/models.py:505
 msgid "Type of group"
 msgstr "Gruppentyp"
 
-#: aleksis/core/models.py:559
-msgid "User"
-msgstr "Benutzer"
-
-#: aleksis/core/models.py:562 aleksis/core/models.py:586
-#: aleksis/core/models.py:671
+#: aleksis/core/models.py:691 aleksis/core/models.py:715
+#: aleksis/core/models.py:800
 #: aleksis/core/templates/core/announcement/list.html:18
 msgid "Title"
 msgstr "Titel"
 
-#: aleksis/core/models.py:565
+#: aleksis/core/models.py:694
 msgid "Application"
 msgstr "Anwendung"
 
-#: aleksis/core/models.py:571
+#: aleksis/core/models.py:700
 msgid "Activity"
 msgstr "Aktivität"
 
-#: aleksis/core/models.py:572
+#: aleksis/core/models.py:701
 msgid "Activities"
 msgstr "Aktivitäten"
 
-#: aleksis/core/models.py:578
+#: aleksis/core/models.py:707
 msgid "Sender"
 msgstr "Absender"
 
-#: aleksis/core/models.py:583
+#: aleksis/core/models.py:712
 msgid "Recipient"
 msgstr "Empfänger"
 
-#: aleksis/core/models.py:588 aleksis/core/models.py:928
+#: aleksis/core/models.py:717 aleksis/core/models.py:1025
 msgid "Link"
 msgstr "Link"
 
-#: aleksis/core/models.py:590
+#: aleksis/core/models.py:719
 msgid "Read"
 msgstr "Gelesen"
 
-#: aleksis/core/models.py:591
+#: aleksis/core/models.py:720
 msgid "Sent"
 msgstr "Versandt"
 
-#: aleksis/core/models.py:604
+#: aleksis/core/models.py:733
 msgid "Notification"
 msgstr "Benachrichtigung"
 
-#: aleksis/core/models.py:673
+#: aleksis/core/models.py:802
 msgid "Link to detailed view"
 msgstr "Link zur detaillierten Ansicht"
 
-#: aleksis/core/models.py:676
+#: aleksis/core/models.py:805
 msgid "Date and time from when to show"
 msgstr "Datum und Uhrzeit des Anzeigestarts"
 
-#: aleksis/core/models.py:679
+#: aleksis/core/models.py:808
 msgid "Date and time until when to show"
 msgstr "Anzeigezeitraum"
 
-#: aleksis/core/models.py:703
+#: aleksis/core/models.py:833
 msgid "Announcement"
 msgstr "Ankündigung"
 
-#: aleksis/core/models.py:741
+#: aleksis/core/models.py:871
 msgid "Announcement recipient"
 msgstr "Empfänger der Ankündigung"
 
-#: aleksis/core/models.py:742
+#: aleksis/core/models.py:872
 msgid "Announcement recipients"
 msgstr "Empfänger der Ankündigung"
 
-#: aleksis/core/models.py:797
+#: aleksis/core/models.py:894
 msgid "Widget Title"
 msgstr "Widget-Titel"
 
-#: aleksis/core/models.py:798
+#: aleksis/core/models.py:895
 msgid "Activate Widget"
 msgstr "Widget aktivieren"
 
-#: aleksis/core/models.py:799
+#: aleksis/core/models.py:896
 msgid "Widget is broken"
 msgstr "Widget ist kaputt"
 
-#: aleksis/core/models.py:802
+#: aleksis/core/models.py:899
 msgid "Size on mobile devices"
 msgstr "Größe auf Mobilgeräten"
 
-#: aleksis/core/models.py:803
+#: aleksis/core/models.py:900
 msgid "<= 600 px, 12 columns"
 msgstr "<= 600 px, 12 Spalten"
 
-#: aleksis/core/models.py:808
+#: aleksis/core/models.py:905
 msgid "Size on tablet devices"
 msgstr "Größe auf Tablets"
 
-#: aleksis/core/models.py:809
+#: aleksis/core/models.py:906
 msgid "> 600 px, 12 columns"
 msgstr "> 600px, 12 Spalten"
 
-#: aleksis/core/models.py:814
+#: aleksis/core/models.py:911
 msgid "Size on desktop devices"
 msgstr "Größe auf Desktopgeräten"
 
-#: aleksis/core/models.py:815
+#: aleksis/core/models.py:912
 msgid "> 992 px, 12 columns"
 msgstr "> 992 px, 12 Spalten"
 
-#: aleksis/core/models.py:820
+#: aleksis/core/models.py:917
 msgid "Size on large desktop devices"
 msgstr "Größe auf großen Desktopgeräten"
 
-#: aleksis/core/models.py:821
+#: aleksis/core/models.py:918
 msgid "> 1200 px>, 12 columns"
 msgstr "> 1200 px, 12 Spalten"
 
-#: aleksis/core/models.py:852
+#: aleksis/core/models.py:949
 msgid "Can edit default dashboard"
 msgstr "Kann Standarddashboard bearbeiten"
 
-#: aleksis/core/models.py:853
+#: aleksis/core/models.py:950
 msgid "Dashboard Widget"
 msgstr "Dashboard-Widget"
 
-#: aleksis/core/models.py:854
+#: aleksis/core/models.py:951
 msgid "Dashboard Widgets"
 msgstr "Dashboard-Widgets"
 
-#: aleksis/core/models.py:860
+#: aleksis/core/models.py:957
 msgid "URL"
 msgstr "URL"
 
-#: aleksis/core/models.py:861
+#: aleksis/core/models.py:958
 msgid "Icon URL"
 msgstr "Symbol-URL"
 
-#: aleksis/core/models.py:867
+#: aleksis/core/models.py:964
 msgid "External link widget"
 msgstr "Externer-Link-Widget"
 
-#: aleksis/core/models.py:868
+#: aleksis/core/models.py:965
 msgid "External link widgets"
 msgstr "Externer-Link-Widgets"
 
-#: aleksis/core/models.py:873
+#: aleksis/core/models.py:970
 msgid "Dashboard widget"
 msgstr "Dashboard-Widget"
 
-#: aleksis/core/models.py:878
+#: aleksis/core/models.py:975
 msgid "Order"
 msgstr "Reihenfolge"
 
-#: aleksis/core/models.py:879
+#: aleksis/core/models.py:976
 msgid "Part of the default dashboard"
 msgstr "Teil des Standarddashboards"
 
-#: aleksis/core/models.py:894
+#: aleksis/core/models.py:991
 msgid "Dashboard widget order"
 msgstr "Reihenfolge der Dashboard-Widgets"
 
-#: aleksis/core/models.py:895
+#: aleksis/core/models.py:992
 msgid "Dashboard widget orders"
 msgstr "Reihenfolgen der Dashboard-Widgets"
 
-#: aleksis/core/models.py:901
+#: aleksis/core/models.py:998
 msgid "Menu ID"
 msgstr "Menü-ID"
 
-#: aleksis/core/models.py:914
+#: aleksis/core/models.py:1011
 msgid "Custom menu"
 msgstr "Benutzerdefiniertes Menü"
 
-#: aleksis/core/models.py:915
+#: aleksis/core/models.py:1012
 msgid "Custom menus"
 msgstr "Benutzerdefinierte Menüs"
 
-#: aleksis/core/models.py:925
+#: aleksis/core/models.py:1022
 msgid "Menu"
 msgstr "Menü"
 
-#: aleksis/core/models.py:929
+#: aleksis/core/models.py:1026 aleksis/core/models.py:1274
+#: aleksis/core/templates/oauth2_provider/application/detail.html:26
 msgid "Icon"
 msgstr "Symbol"
 
-#: aleksis/core/models.py:935
+#: aleksis/core/models.py:1032
 msgid "Custom menu item"
 msgstr "Benutzerdefiniertes Menüelement"
 
-#: aleksis/core/models.py:936
+#: aleksis/core/models.py:1033
 msgid "Custom menu items"
 msgstr "Benutzerdefinierte Menüelemente"
 
-#: aleksis/core/models.py:950
+#: aleksis/core/models.py:1047
 msgid "Title of type"
 msgstr "Titel des Typs"
 
-#: aleksis/core/models.py:957 aleksis/core/templates/core/group/full.html:47
+#: aleksis/core/models.py:1054 aleksis/core/templates/core/group/full.html:47
 msgid "Group type"
 msgstr "Gruppentyp"
 
-#: aleksis/core/models.py:971
+#: aleksis/core/models.py:1068
 msgid "Can view system status"
 msgstr "Kann Systemstatus sehen"
 
-#: aleksis/core/models.py:972
+#: aleksis/core/models.py:1069
 msgid "Can manage data"
 msgstr "Kann Daten verwalten"
 
-#: aleksis/core/models.py:973
+#: aleksis/core/models.py:1070
 msgid "Can impersonate"
 msgstr "Kann sich verkleiden"
 
-#: aleksis/core/models.py:974
+#: aleksis/core/models.py:1071
 msgid "Can use search"
 msgstr "Kann Suche benutzen"
 
-#: aleksis/core/models.py:975
+#: aleksis/core/models.py:1072
 msgid "Can change site preferences"
 msgstr "Kann Konfiguration ändern"
 
-#: aleksis/core/models.py:976
+#: aleksis/core/models.py:1073
 msgid "Can change person preferences"
 msgstr "Kann Einstellungen einer Person verändern"
 
-#: aleksis/core/models.py:977
+#: aleksis/core/models.py:1074
 msgid "Can change group preferences"
 msgstr "Kann Einstellungen einer Gruppe verändern"
 
-#: aleksis/core/models.py:978
-msgid "Can add oauth applications"
-msgstr "Kann OAuth-Anwendungen hinzufügen"
-
-#: aleksis/core/models.py:979
-msgid "Can list oauth applications"
-msgstr "Can OAuth-Anwendungen auflisten"
-
-#: aleksis/core/models.py:980
-msgid "Can view oauth applications"
-msgstr "Kann OAuth-Anwendungen sehen"
-
-#: aleksis/core/models.py:981
-msgid "Can update oauth applications"
-msgstr "Kann OAuth-Anwendungen aktualisieren"
-
-#: aleksis/core/models.py:982
-msgid "Can delete oauth applications"
-msgstr "Kann OAuth-Anwendungen löschen"
-
-#: aleksis/core/models.py:983
+#: aleksis/core/models.py:1075
 msgid "Can test PDF generation"
 msgstr "Kann die PDF-Generierung testen"
 
-#: aleksis/core/models.py:1019
+#: aleksis/core/models.py:1076
+msgid "Can invite persons"
+msgstr "Kann Personen einladen"
+
+#: aleksis/core/models.py:1112
 msgid "Related data check task"
 msgstr "Zugehörige Datenprüfungsaufgabe"
 
-#: aleksis/core/models.py:1027
+#: aleksis/core/models.py:1120
 msgid "Issue solved"
 msgstr "Problem gelöst"
 
-#: aleksis/core/models.py:1028
+#: aleksis/core/models.py:1121
 msgid "Notification sent"
 msgstr "Benachrichtigung gesendet"
 
-#: aleksis/core/models.py:1041
+#: aleksis/core/models.py:1134
 msgid "Data check result"
 msgstr "Datenprüfungsergebnis"
 
-#: aleksis/core/models.py:1042
+#: aleksis/core/models.py:1135
 msgid "Data check results"
 msgstr "Datenprüfungsergebnisse"
 
-#: aleksis/core/models.py:1044
+#: aleksis/core/models.py:1137
 msgid "Can run data checks"
 msgstr "Kann Datenprüfungen ausführen"
 
-#: aleksis/core/models.py:1045
+#: aleksis/core/models.py:1138
 msgid "Can solve data check problems"
 msgstr "Kann Datenprüfungsprobleme lösen"
 
-#: aleksis/core/models.py:1060
+#: aleksis/core/models.py:1145
+msgid "E-Mail address"
+msgstr "E-Mail-Adresse"
+
+#: aleksis/core/models.py:1177
 msgid "Owner"
 msgstr "Leiter"
 
-#: aleksis/core/models.py:1064
+#: aleksis/core/models.py:1181
 msgid "File expires at"
 msgstr "Datei abgelaufen am"
 
-#: aleksis/core/models.py:1066
+#: aleksis/core/models.py:1183
 msgid "Generated HTML file"
 msgstr "Generierte HTML-Datei"
 
-#: aleksis/core/models.py:1068
+#: aleksis/core/models.py:1185
 msgid "Generated PDF file"
 msgstr "Generierte PDF-Datei"
 
-#: aleksis/core/models.py:1075
+#: aleksis/core/models.py:1192
 msgid "PDF file"
 msgstr "PDF-Datei"
 
-#: aleksis/core/models.py:1076
+#: aleksis/core/models.py:1193
 msgid "PDF files"
 msgstr "PDF-Dateien"
 
-#: aleksis/core/models.py:1081
+#: aleksis/core/models.py:1198
 msgid "Task result"
 msgstr "Task-Ergebnis"
 
-#: aleksis/core/models.py:1084
+#: aleksis/core/models.py:1201
 msgid "Task user"
 msgstr "Task-Benutzer"
 
-#: aleksis/core/models.py:1096
+#: aleksis/core/models.py:1213
 msgid "Task user assignment"
 msgstr "Task-Benutzer-Zuordnung"
 
-#: aleksis/core/models.py:1097
+#: aleksis/core/models.py:1214
 msgid "Task user assignments"
 msgstr "Task-Benutzer-Zuordnungen"
 
-#: aleksis/core/preferences.py:22
+#: aleksis/core/models.py:1230
+msgid "Additional attributes"
+msgstr "Zusätzliche Attribute"
+
+#: aleksis/core/models.py:1268
+msgid "Allowed scopes that clients can request"
+msgstr "Erlaubte Scopes, die ein Client anfordern kann"
+
+#: aleksis/core/models.py:1278
+msgid "This image will be shown as icon in the authorization flow. It should be squared."
+msgstr "Dieses Bild wird im Autorisierungs-Vorgang als Symbol angezeigt werden. Es sollte rechteckig sein."
+
+#: aleksis/core/preferences.py:25
 msgid "General"
 msgstr "Allgemein"
 
-#: aleksis/core/preferences.py:23
+#: aleksis/core/preferences.py:26
 msgid "School"
 msgstr "Schule"
 
-#: aleksis/core/preferences.py:24
+#: aleksis/core/preferences.py:27
 msgid "Theme"
 msgstr "Theme"
 
-#: aleksis/core/preferences.py:25
+#: aleksis/core/preferences.py:28
 msgid "Mail"
 msgstr "E-Mail"
 
-#: aleksis/core/preferences.py:27
+#: aleksis/core/preferences.py:30
 msgid "Footer"
 msgstr "Fußbereich"
 
-#: aleksis/core/preferences.py:28
+#: aleksis/core/preferences.py:31
 msgid "Accounts"
 msgstr "Konten"
 
-#: aleksis/core/preferences.py:29
+#: aleksis/core/preferences.py:32
 msgid "Authentication"
 msgstr "Authentifizierung"
 
-#: aleksis/core/preferences.py:30
+#: aleksis/core/preferences.py:33
 msgid "Internationalisation"
 msgstr "Internationalisierung"
 
-#: aleksis/core/preferences.py:41
+#: aleksis/core/preferences.py:43
 msgid "Site title"
 msgstr "Seitentitel"
 
-#: aleksis/core/preferences.py:52
+#: aleksis/core/preferences.py:55
 msgid "Site description"
 msgstr "Seitenbeschreibung"
 
-#: aleksis/core/preferences.py:63
+#: aleksis/core/preferences.py:65
 msgid "Primary colour"
 msgstr "Primärfarbe"
 
-#: aleksis/core/preferences.py:75
+#: aleksis/core/preferences.py:77
 msgid "Secondary colour"
 msgstr "Akzentfarbe"
 
-#: aleksis/core/preferences.py:86
+#: aleksis/core/preferences.py:89
 msgid "Logo"
 msgstr "Logo"
 
-#: aleksis/core/preferences.py:96
+#: aleksis/core/preferences.py:100
 msgid "Favicon"
 msgstr "Favicon"
 
-#: aleksis/core/preferences.py:106
+#: aleksis/core/preferences.py:111
 msgid "PWA-Icon"
 msgstr "PWA-Icon"
 
-#: aleksis/core/preferences.py:117
+#: aleksis/core/preferences.py:121
+msgid "PWA-Icon is maskable"
+msgstr "PWA-Symbol ist maskable"
+
+#: aleksis/core/preferences.py:133
 msgid "Mail out name"
 msgstr "Ausgangsmailname"
 
-#: aleksis/core/preferences.py:128
+#: aleksis/core/preferences.py:144
 msgid "Mail out address"
 msgstr "E-Mail-Ausgangsadresse"
 
-#: aleksis/core/preferences.py:140
+#: aleksis/core/preferences.py:157
 msgid "Link to privacy policy"
 msgstr "Link zur Datenschutzerklärung"
 
-#: aleksis/core/preferences.py:152
+#: aleksis/core/preferences.py:169
 msgid "Link to imprint"
 msgstr "Link zum Impressum"
 
-#: aleksis/core/preferences.py:164
+#: aleksis/core/preferences.py:180
 msgid "Name format for addressing"
 msgstr "Namensformat für Anreden"
 
-#: aleksis/core/preferences.py:180
+#: aleksis/core/preferences.py:197
 msgid "Channels to use for notifications"
 msgstr "Aktivierte Benachrichtungskanäle"
 
-#: aleksis/core/preferences.py:192
+#: aleksis/core/preferences.py:209
 msgid "Regular expression to match primary group, e.g. '^Class .*'"
 msgstr "Regulärer Ausdruck um Primärgruppen zu finden, z. B.  '^Class .*'"
 
-#: aleksis/core/preferences.py:203
+#: aleksis/core/preferences.py:220
 msgid "Field on person to match primary group against"
 msgstr "Feld um Primärgruppen zu finden"
 
-#: aleksis/core/preferences.py:215
+#: aleksis/core/preferences.py:232
 msgid "Automatically create new persons for new users"
 msgstr "Erstelle automatisch neue Personen für neue Benutzer"
 
-#: aleksis/core/preferences.py:224
+#: aleksis/core/preferences.py:241
 msgid "Automatically link existing persons to new users by their e-mail address"
 msgstr "Verknüpfe existierende Personen automatisch mit neuen Personen anhand ihrer E-Mail-Adresse"
 
-#: aleksis/core/preferences.py:235
+#: aleksis/core/preferences.py:252
 msgid "Display name of the school"
 msgstr "Sichtbarer Name der Schule"
 
-#: aleksis/core/preferences.py:246
+#: aleksis/core/preferences.py:263
 msgid "Official name of the school, e.g. as given by supervisory authority"
 msgstr "Offizieller Name der Schule, wie er z.B. von der Behörde vorgegeben ist"
 
-#: aleksis/core/preferences.py:254
+#: aleksis/core/preferences.py:271
 msgid "Allow users to change their passwords"
 msgstr "Erlaube Benutzern, ihr Passwort zu ändern"
 
-#: aleksis/core/preferences.py:262
+#: aleksis/core/preferences.py:279
 msgid "Enable signup"
 msgstr "Registrierung aktivieren"
 
-#: aleksis/core/preferences.py:273
+#: aleksis/core/preferences.py:287
+msgid "Enable invitations"
+msgstr "Einladungen aktivieren"
+
+#: aleksis/core/preferences.py:295
+msgid "Length of invite code. (Default 3: abcde-acbde-abcde)"
+msgstr "Länge des Einladungscodes. (Standard: 3: abcde-acbde-abcde)"
+
+#: aleksis/core/preferences.py:303
+msgid "Size of packets. (Default 5: abcde)"
+msgstr "Größe der Pakete. (Standard 5: abcde)"
+
+#: aleksis/core/preferences.py:314
+msgid "Allowed Grant Flows for OAuth applications"
+msgstr "Erlaubte Grant Flows für OAuth-Anwendungen"
+
+#: aleksis/core/preferences.py:328
 msgid "Available languages"
 msgstr "Verfügbare Sprachen"
 
-#: aleksis/core/preferences.py:285
+#: aleksis/core/preferences.py:341
 msgid "Send emails if data checks detect problems"
 msgstr "E-Mails versenden, wenn Datenprüfungen Probleme finden"
 
-#: aleksis/core/preferences.py:296
+#: aleksis/core/preferences.py:352
 msgid "Email recipients for data checks problem emails"
 msgstr "E-Mailempfänger für Datenprüfungsproblem-E-Mails"
 
-#: aleksis/core/preferences.py:307
+#: aleksis/core/preferences.py:363
 msgid "Email recipient groups for data checks problem emails"
 msgstr "E-Mail-Empfängergruppen für Datenprüfungsproblem-E-Mails"
 
-#: aleksis/core/preferences.py:316
+#: aleksis/core/preferences.py:372
 msgid "Show dashboard to users without login"
 msgstr "Zeige Dashboard für Benutzer ohne Login"
 
-#: aleksis/core/preferences.py:325
+#: aleksis/core/preferences.py:381
 msgid "Allow users to edit their dashboard"
 msgstr "Erlaube Benutzern, ihr Dashboard zu bearbeiten"
 
-#: aleksis/core/preferences.py:336
+#: aleksis/core/preferences.py:392
 msgid "Fields on person model which are editable by themselves."
 msgstr "Felder des Personen-Models welche von ihnen selbst editierbar sind."
 
-#: aleksis/core/preferences.py:350
+#: aleksis/core/preferences.py:407
 msgid "Editable fields on person model which should trigger a notification on change"
 msgstr "Editierbare Felder des Personen-Models welche eine Benachrichtigung für Änderungen auslösen soll"
 
-#: aleksis/core/preferences.py:363
+#: aleksis/core/preferences.py:421
 msgid "Contact for notification if a person changes their data"
 msgstr "Kontakt für Benachrichtigung, wenn eine Person ihre Daten ändert"
 
-#: aleksis/core/preferences.py:373
+#: aleksis/core/preferences.py:432
 msgid "PDF file expiration duration"
 msgstr "PDF-Datei-Ablaufdauer"
 
-#: aleksis/core/preferences.py:374
+#: aleksis/core/preferences.py:433
 msgid "in minutes"
 msgstr "in Minuten"
 
-#: aleksis/core/preferences.py:384
+#: aleksis/core/preferences.py:443
 msgid "Automatically update the dashboard and its widgets"
 msgstr "Automatisch das Dashboard und seine Widgets aktualisieren"
 
-#: aleksis/core/preferences.py:394
+#: aleksis/core/preferences.py:453
 msgid "Automatically update the dashboard and its widgets sitewide"
 msgstr "Automatisch das Dashboard und seine Widgets aktualisieren (auf der ganzen Seite)"
 
-#: aleksis/core/settings.py:452
+#: aleksis/core/preferences.py:463
+msgid "Country for phone number parsing"
+msgstr "Land für das Einlesen von Telefonnummern"
+
+#: aleksis/core/settings.py:529
 msgid "English"
 msgstr "Englisch"
 
-#: aleksis/core/settings.py:453
+#: aleksis/core/settings.py:530
 msgid "German"
 msgstr "Deutsch"
 
-#: aleksis/core/tables.py:19
+#: aleksis/core/tables.py:24
 #: aleksis/core/templates/core/announcement/list.html:36
 #: aleksis/core/templates/core/group/full.html:24
-#: aleksis/core/templates/core/person/full.html:23
-#: aleksis/core/templates/oauth2_provider/application_detail.html:17
+#: aleksis/core/templates/core/person/full.html:26
+#: aleksis/core/templates/core/person/full.html:98
+#: aleksis/core/templates/oauth2_provider/application/detail.html:17
 msgid "Edit"
 msgstr "Bearbeiten"
 
-#: aleksis/core/tables.py:21 aleksis/core/tables.py:89
+#: aleksis/core/tables.py:26 aleksis/core/tables.py:94
+#: aleksis/core/tables.py:138
 #: aleksis/core/templates/core/announcement/list.html:22
 msgid "Actions"
 msgstr "Aktionen"
 
-#: aleksis/core/tables.py:56 aleksis/core/tables.py:57
-#: aleksis/core/tables.py:71 aleksis/core/tables.py:87
+#: aleksis/core/tables.py:61 aleksis/core/tables.py:62
+#: aleksis/core/tables.py:76 aleksis/core/tables.py:92
+#: aleksis/core/tables.py:136
 #: aleksis/core/templates/core/announcement/list.html:42
 #: aleksis/core/templates/core/group/full.html:31
 #: aleksis/core/templates/core/pages/delete.html:22
-#: aleksis/core/templates/core/person/full.html:30
-#: aleksis/core/templates/oauth2_provider/application_confirm_delete.html:20
-#: aleksis/core/templates/oauth2_provider/application_detail.html:21
+#: aleksis/core/templates/core/person/full.html:33
+#: aleksis/core/templates/core/person/full.html:105
+#: aleksis/core/templates/oauth2_provider/application/detail.html:21
 msgid "Delete"
 msgstr "Löschen"
 
-#: aleksis/core/templates/403.html:14 aleksis/core/templates/404.html:10
-#: aleksis/core/templates/500.html:10
-#: aleksis/core/templates/oauth2_provider/authorize.html:47
+#: aleksis/core/templates/403.html:14 aleksis/core/templates/500.html:10
+#: aleksis/core/templates/oauth2_provider/authorize.html:54
 #: aleksis/core/templates/oauth2_provider/authorized-oob.html:24
 msgid "Error"
 msgstr "Fehler"
@@ -1127,7 +1209,7 @@ msgstr ""
 "Es ist Ihnen nicht erlaubt, auf die angefragte Seite oder das angefragte\n"
 "             Objekt zuzugreifen."
 
-#: aleksis/core/templates/403.html:19 aleksis/core/templates/404.html:17
+#: aleksis/core/templates/403.html:19 aleksis/core/templates/404.html:16
 msgid ""
 "\n"
 "            If you think this is an error in AlekSIS, please contact your site\n"
@@ -1139,15 +1221,7 @@ msgstr ""
 "     Systemadministratoren:\n"
 "          "
 
-#: aleksis/core/templates/404.html:10
-msgid ""
-"The requested page or object was not\n"
-"          found."
-msgstr ""
-"Die angefragte Seite oder das angefragte Objekt wurde nicht\n"
-"            gefunden."
-
-#: aleksis/core/templates/404.html:13
+#: aleksis/core/templates/404.html:12
 msgid ""
 "\n"
 "            If you were redirected by a link on an external page,\n"
@@ -1202,11 +1276,11 @@ msgstr ""
 msgid "Account inactive"
 msgstr "Konto inaktiv"
 
-#: aleksis/core/templates/account/account_inactive.html:13
+#: aleksis/core/templates/account/account_inactive.html:14
 msgid "Account inactive."
 msgstr "Konto inaktiv."
 
-#: aleksis/core/templates/account/account_inactive.html:15
+#: aleksis/core/templates/account/account_inactive.html:17
 msgid ""
 "\n"
 "            This account is currently inactive. If you think this is an\n"
@@ -1223,14 +1297,13 @@ msgid "Hello!"
 msgstr "Hallo!"
 
 #: aleksis/core/templates/account/email/base_message.txt:9
-#: aleksis/core/templates/templated_email/notification.email:22
-#: aleksis/core/templates/templated_email/notification.email:46
 msgid "Your AlekSIS team"
 msgstr "Ihr AlekSIS-Team"
 
 #: aleksis/core/templates/account/email_confirm.html:5
 #: aleksis/core/templates/account/email_confirm.html:6
 #: aleksis/core/templates/account/email_confirm.html:17
+#: aleksis/core/templates/socialaccount/login.html:17
 msgid "Confirm"
 msgstr "Bestätigen"
 
@@ -1279,7 +1352,7 @@ msgstr ""
 #: aleksis/core/templates/account/password_reset_done.html:5
 #: aleksis/core/templates/account/verification_email_required.html:5
 #: aleksis/core/templates/account/verification_email_required.html:6
-#: aleksis/core/templates/two_factor/core/login.html:81
+#: aleksis/core/templates/two_factor/core/login.html:100
 msgid "Reset password"
 msgstr "Passwort zurücksetzen"
 
@@ -1382,11 +1455,11 @@ msgstr "Haben Sie bereits ein Konto? Dann <a href=\"%(login_url)s\">melden Sie s
 msgid "Signup closed"
 msgstr "Registrierung geschlossen"
 
-#: aleksis/core/templates/account/signup_closed.html:13
+#: aleksis/core/templates/account/signup_closed.html:14
 msgid "Signup closed."
 msgstr "Registrierung geschlossen."
 
-#: aleksis/core/templates/account/signup_closed.html:15
+#: aleksis/core/templates/account/signup_closed.html:17
 msgid ""
 "\n"
 "            This sign up is currently closed. If you think this is an\n"
@@ -1437,11 +1510,6 @@ msgstr ""
 "Bitte kontaktieren Sie uns, wenn Sie diese nicht binnen weniger Minuten erhalten.\n"
 "          "
 
-#: aleksis/core/templates/account/verification_sent.html:30
-#, python-format
-msgid "<strong>Note:</strong> you can still <a href=\"%(email_url)s\">change your e-mail address</a>"
-msgstr "<strong>Hinweis:</strong> Sie können immer noch <a href=\"%(email_url)s\"> Ihre E-Mail-Adresse ändern</a>"
-
 #: aleksis/core/templates/core/additional_field/edit.html:6
 #: aleksis/core/templates/core/additional_field/edit.html:7
 msgid "Edit additional field"
@@ -1485,25 +1553,25 @@ msgstr "Empfänger"
 msgid "There are no announcements."
 msgstr "Es gibt aktuell keine Ankündigungen."
 
-#: aleksis/core/templates/core/base.html:60
+#: aleksis/core/templates/core/base.html:78
 msgid "Logged in as"
 msgstr "Angemeldet als"
 
-#: aleksis/core/templates/core/base.html:154
-msgid "About AlekSIS — The Free School Information System"
-msgstr "Über AlekSIS — The Free School Information System"
+#: aleksis/core/templates/core/base.html:179
+msgid "About AlekSIS® — The Free School Information System"
+msgstr "Über AlekSIS® — The Free School Information System"
 
-#: aleksis/core/templates/core/base.html:162
-msgid "Impress"
+#: aleksis/core/templates/core/base.html:187
+msgid "Imprint"
 msgstr "Impressum"
 
-#: aleksis/core/templates/core/base.html:170
+#: aleksis/core/templates/core/base.html:195
 msgid "Privacy Policy"
 msgstr "Datenschutzerklärung"
 
-#: aleksis/core/templates/core/base_print.html:72
-msgid "Powered by AlekSIS"
-msgstr "Betrieben mit AlekSIS"
+#: aleksis/core/templates/core/base_print.html:74
+msgid "Powered by AlekSIS®"
+msgstr "Betrieben mit AlekSIS®"
 
 #: aleksis/core/templates/core/dashboard_widget/create.html:8
 #: aleksis/core/templates/core/dashboard_widget/create.html:12
@@ -1593,15 +1661,15 @@ msgstr "Details anzeigen"
 msgid "Options to solve the problem"
 msgstr "Optionen, das Problem zu lösen"
 
-#: aleksis/core/templates/core/data_check/list.html:62
+#: aleksis/core/templates/core/data_check/list.html:63
 msgid "Show object"
 msgstr "Objekt anzeigen"
 
-#: aleksis/core/templates/core/data_check/list.html:84
+#: aleksis/core/templates/core/data_check/list.html:86
 msgid "Registered checks"
 msgstr "Registrierte Prüfungen"
 
-#: aleksis/core/templates/core/data_check/list.html:88
+#: aleksis/core/templates/core/data_check/list.html:90
 msgid ""
 "\n"
 "            The system will check for the following problems:\n"
@@ -1719,7 +1787,7 @@ msgstr ""
 
 #: aleksis/core/templates/core/group/child_groups.html:93
 #: aleksis/core/templates/core/group/child_groups.html:128
-#: aleksis/core/templates/oauth2_provider/application_detail.html:9
+#: aleksis/core/templates/oauth2_provider/application/detail.html:9
 #: aleksis/core/templates/two_factor/_wizard_actions.html:15
 #: aleksis/core/templates/two_factor/_wizard_actions.html:20
 msgid "Back"
@@ -1729,7 +1797,7 @@ msgstr "Zurück"
 #: aleksis/core/templates/core/group/child_groups.html:134
 #: aleksis/core/templates/two_factor/_wizard_actions.html:26
 msgid "Next"
-msgstr "Weiter"
+msgstr "Nächste"
 
 #: aleksis/core/templates/core/group/child_groups.html:106
 #: aleksis/core/templates/core/group/child_groups.html:141
@@ -1748,7 +1816,8 @@ msgid "Edit group"
 msgstr "Gruppe editieren"
 
 #: aleksis/core/templates/core/group/full.html:38
-#: aleksis/core/templates/core/person/full.html:37
+#: aleksis/core/templates/core/person/full.html:40
+#: aleksis/core/templates/core/person/full.html:112
 msgid "Change preferences"
 msgstr "Einstellungen ändern"
 
@@ -1841,41 +1910,54 @@ msgid "No notifications available yet."
 msgstr "Aktuell keine Benachrichtigungen verfügbar."
 
 #: aleksis/core/templates/core/pages/about.html:6
+msgid "About AlekSIS®"
+msgstr "Über AlekSIS®"
+
+#: aleksis/core/templates/core/pages/about.html:7
+msgid "AlekSIS® – The Free School Information System"
+msgstr "AlekSIS® – The Free School Information System"
+
 #: aleksis/core/templates/core/pages/about.html:15
 msgid "About AlekSIS"
 msgstr "Über AlekSIS"
 
-#: aleksis/core/templates/core/pages/about.html:7
-msgid "AlekSIS – The Free School Information System"
-msgstr "AlekSIS – The Free School Information System"
-
 #: aleksis/core/templates/core/pages/about.html:17
 msgid ""
 "\n"
-"              This platform is powered by AlekSIS, a web-based school information system (SIS) which can be used\n"
+"              This platform is powered by AlekSIS®, a web-based school information system (SIS) which can be used\n"
 "              to manage and/or publish organisational artifacts of educational institutions. AlekSIS is free software and\n"
 "              can be used by anyone.\n"
 "            "
 msgstr ""
 "\n"
-"              Diese Plattform wird mit AlekSIS, einem webbasierten Schulinformationssystem (SIS), \n"
+"              Diese Plattform wird mit AlekSIS®, einem webbasierten Schulinformationssystem (SIS), \n"
 "welches für die Verwaltung und/oder Veröffentlichung von Bildungseinrichtungen verwendet werden kann.\n"
 "AlekSIS ist freie Software und kann von jedem benutzt werden.\n"
 "            "
 
-#: aleksis/core/templates/core/pages/about.html:25
+#: aleksis/core/templates/core/pages/about.html:24
+msgid ""
+"\n"
+"              AlekSIS® is a registered trademark of the AlekSIS open source project, represented by Teckids e.V.\n"
+"            "
+msgstr ""
+"\n"
+"              AlekSIS® ist eine eingetragene Wortmarke des Open-Source-Projektes AlekSIS, vertreten durch den Teckids e.V.\n"
+"            "
+
+#: aleksis/core/templates/core/pages/about.html:30
 msgid "Website of AlekSIS"
 msgstr "Website von AlekSIS"
 
-#: aleksis/core/templates/core/pages/about.html:26
+#: aleksis/core/templates/core/pages/about.html:31
 msgid "Source code"
 msgstr "Quellcode"
 
-#: aleksis/core/templates/core/pages/about.html:35
+#: aleksis/core/templates/core/pages/about.html:40
 msgid "Licence information"
 msgstr "Lizenzinformationen"
 
-#: aleksis/core/templates/core/pages/about.html:37
+#: aleksis/core/templates/core/pages/about.html:42
 msgid ""
 "\n"
 "              The core and the official apps of AlekSIS are licenced under the EUPL, version 1.2 or later. For licence\n"
@@ -1889,23 +1971,23 @@ msgstr ""
 "sind wie folgt markiert:\n"
 "            "
 
-#: aleksis/core/templates/core/pages/about.html:45
+#: aleksis/core/templates/core/pages/about.html:50
 msgid "Free/Open Source Licence"
 msgstr "Freie/Open Source Lizenz"
 
-#: aleksis/core/templates/core/pages/about.html:46
+#: aleksis/core/templates/core/pages/about.html:51
 msgid "Other Licence"
 msgstr "Andere Lizenz"
 
-#: aleksis/core/templates/core/pages/about.html:50
+#: aleksis/core/templates/core/pages/about.html:55
 msgid "Full licence text"
 msgstr "Kompletter Lizenztext"
 
-#: aleksis/core/templates/core/pages/about.html:51
+#: aleksis/core/templates/core/pages/about.html:56
 msgid "More information about the EUPL"
 msgstr "Weitere Informationen über die EUPL"
 
-#: aleksis/core/templates/core/pages/about.html:90
+#: aleksis/core/templates/core/pages/about.html:95
 #, python-format
 msgid ""
 "\n"
@@ -2027,6 +2109,8 @@ msgid "Celery task results"
 msgstr "Celery Task-Ergebnisse"
 
 #: aleksis/core/templates/core/pages/system_status.html:112
+#: aleksis/core/templates/templated_email/celery_failure.email:9
+#: aleksis/core/templates/templated_email/celery_failure.email:28
 msgid "Task"
 msgstr "Task"
 
@@ -2129,6 +2213,51 @@ msgstr ""
 "        die Verwaltenden von AlekSIS an Ihrer Schule.\n"
 "          "
 
+#: aleksis/core/templates/core/perms/assign.html:12
+#: aleksis/core/templates/core/perms/assign.html:13
+msgid "Assign permission"
+msgstr "Berechtigung zuweisen"
+
+#: aleksis/core/templates/core/perms/assign.html:17
+msgid "Selected permission"
+msgstr "Ausgewählte Berechtigung"
+
+#: aleksis/core/templates/core/perms/assign.html:26
+msgid "Assign"
+msgstr "Zuweisen"
+
+#: aleksis/core/templates/core/perms/list.html:21
+msgid "Assign a new permission"
+msgstr "Eine neue Berechtigung zuweisen"
+
+#: aleksis/core/templates/core/perms/list.html:25
+msgid "Select"
+msgstr "Auswählen"
+
+#: aleksis/core/templates/core/perms/list.html:34
+msgid "Global (user)"
+msgstr "Global (Benutzer)"
+
+#: aleksis/core/templates/core/perms/list.html:38
+msgid "Global (group)"
+msgstr "Global (Gruppe)"
+
+#: aleksis/core/templates/core/perms/list.html:42
+msgid "Object (user)"
+msgstr "Objekt (Benutzer)"
+
+#: aleksis/core/templates/core/perms/list.html:46
+msgid "Object (group)"
+msgstr "Objekt (Gruppe)"
+
+#: aleksis/core/templates/core/perms/list.html:52
+msgid "Filter permissions"
+msgstr "Berechtigungen filtern"
+
+#: aleksis/core/templates/core/perms/list.html:58
+msgid "Update"
+msgstr "Aktualisieren"
+
 #: aleksis/core/templates/core/person/create.html:12
 #: aleksis/core/templates/core/person/create.html:13
 #: aleksis/core/templates/core/person/list.html:17
@@ -2140,17 +2269,25 @@ msgstr "Person erstellen"
 msgid "Edit person"
 msgstr "Person editieren"
 
-#: aleksis/core/templates/core/person/full.html:44
-#: aleksis/core/templates/impersonate/list_users.html:7
-#: aleksis/core/templates/impersonate/list_users.html:8
+#: aleksis/core/templates/core/person/full.html:47
+#: aleksis/core/templates/core/person/full.html:119
 msgid "Impersonate"
 msgstr "Verkleiden"
 
-#: aleksis/core/templates/core/person/full.html:50
+#: aleksis/core/templates/core/person/full.html:54
+#: aleksis/core/templates/core/person/full.html:126
+msgid "Invite user"
+msgstr "Benutzer einladen"
+
+#: aleksis/core/templates/core/person/full.html:133
 msgid "Contact details"
 msgstr "Kontaktdetails"
 
-#: aleksis/core/templates/core/person/full.html:130
+#: aleksis/core/templates/core/person/full.html:224
+msgid "This person didn't upload a personal photo."
+msgstr "Diese Person hat kein persönliches Foto hochgeladen."
+
+#: aleksis/core/templates/core/person/full.html:232
 msgid "Children"
 msgstr "Kinder"
 
@@ -2190,77 +2327,144 @@ msgstr "Einstellungen für %(instance)s"
 msgid "Save preferences"
 msgstr "Einstellungen speichern"
 
-#: aleksis/core/templates/oauth2_provider/application_confirm_delete.html:5
-#: aleksis/core/templates/oauth2_provider/application_confirm_delete.html:6
-msgid "Delete application"
-msgstr "Anwendung löschen"
+#: aleksis/core/templates/invitations/disabled.html:5
+msgid "The invite feature is disabled"
+msgstr "Die Einladungsfunktion ist deaktiviert"
+
+#: aleksis/core/templates/invitations/disabled.html:13
+msgid "The invite feature is disabled."
+msgstr "Die Einladungsfunktion ist deaktiviert."
+
+#: aleksis/core/templates/invitations/disabled.html:15
+msgid "To enable it, switch on the corresponding checkbox in the authentication section of the "
+msgstr ""
+"Um sie zu aktivieren, nutzen Sie die Checkbox im Abschnitt \"Authentifikation"
+"\" der "
+
+#: aleksis/core/templates/invitations/disabled.html:16
+msgid "site preferences page"
+msgstr "Seiteneinstellungen"
+
+#: aleksis/core/templates/invitations/enter.html:21
+msgid "Accept your invitation"
+msgstr "Ihre Einladung akzeptieren"
+
+#: aleksis/core/templates/invitations/enter.html:25
+msgid ""
+"\n"
+"                Please enter your invitation code to register\n"
+"                your new user account:\n"
+"              "
+msgstr ""
+"\n"
+"                Bitte geben Sie Ihren Einladungscode ein, \n"
+"um Ihr neues Benutzerkonto zu registrieren:\n"
+"              "
+
+#: aleksis/core/templates/invitations/enter.html:37
+msgid "Accept invite"
+msgstr "Einladung akzeptieren"
+
+#: aleksis/core/templates/invitations/forms/_invite.html:9
+#: aleksis/core/templates/invitations/forms/_invite.html:10
+#: aleksis/core/templates/invitations/forms/_invite.html:21
+msgid "Invite"
+msgstr "Einladen"
+
+#: aleksis/core/templates/invitations/forms/_invite.html:17
+msgid "Invite by email address"
+msgstr "Mit E-Mail-Adresse einladen"
+
+#: aleksis/core/templates/invitations/forms/_invite.html:26
+msgid "Generate invitation code"
+msgstr "Einladungscode generieren"
 
-#: aleksis/core/templates/oauth2_provider/application_confirm_delete.html:12
+#: aleksis/core/templates/invitations/forms/_invite.html:29
+msgid "Generate code"
+msgstr "Code generieren"
+
+#: aleksis/core/templates/invitations/forms/_invite.html:33
+msgid "Invitations"
+msgstr "Einladungen"
+
+#: aleksis/core/templates/invitations/messages/invite_accepted.txt:3
 #, python-format
-msgid "Are you sure to delete the application %(application_name)s?"
-msgstr "Sind Sie sicher, dass Sie die Anwendung %(application_name)s löschen möchten?"
+msgid "The invitation for %(email)s has been accepted."
+msgstr "Die Einladung für %(email)s wurde akzeptiert."
+
+#: aleksis/core/templates/oauth2_provider/application/create.html:5
+#: aleksis/core/templates/oauth2_provider/application/create.html:6
+msgid "Register OAuth2 Application"
+msgstr "OAuth2-Anwendung registrieren"
 
-#: aleksis/core/templates/oauth2_provider/application_confirm_delete.html:24
-#: aleksis/core/templates/oauth2_provider/application_form.html:18
+#: aleksis/core/templates/oauth2_provider/application/create.html:14
+#: aleksis/core/templates/oauth2_provider/application/edit.html:14
 #: aleksis/core/templates/oauth2_provider/authorized-token-delete.html:24
 #: aleksis/core/templates/two_factor/_wizard_actions.html:6
 msgid "Cancel"
 msgstr "Abbrechen"
 
-#: aleksis/core/templates/oauth2_provider/application_detail.html:27
+#: aleksis/core/templates/oauth2_provider/application/detail.html:5
+msgid "OAuth2 Application"
+msgstr "OAuth2-Anwendung"
+
+#: aleksis/core/templates/oauth2_provider/application/detail.html:39
 msgid "Client id"
 msgstr "Client-ID"
 
-#: aleksis/core/templates/oauth2_provider/application_detail.html:35
+#: aleksis/core/templates/oauth2_provider/application/detail.html:47
 msgid "Client secret"
 msgstr "Client-Secret"
 
-#: aleksis/core/templates/oauth2_provider/application_detail.html:43
+#: aleksis/core/templates/oauth2_provider/application/detail.html:55
 msgid "Client type"
 msgstr "Client-Typ"
 
-#: aleksis/core/templates/oauth2_provider/application_detail.html:51
-msgid "Authorization Grant Type"
-msgstr "Authorization Grant-Typ"
+#: aleksis/core/templates/oauth2_provider/application/detail.html:63
+msgid "Allowed scopes"
+msgstr "Erlaubte Scopes"
 
-#: aleksis/core/templates/oauth2_provider/application_detail.html:59
+#: aleksis/core/templates/oauth2_provider/application/detail.html:71
 msgid "Redirect URIs"
 msgstr "Weiterleitungs-URLs"
 
-#: aleksis/core/templates/oauth2_provider/application_form.html:5
-msgid "Create OAuth2 Application"
-msgstr "OAuth2-Anwendung erstellen"
+#: aleksis/core/templates/oauth2_provider/application/detail.html:79
+msgid "Skip Authorisation"
+msgstr "Authentifizierung überspringen"
 
-#: aleksis/core/templates/oauth2_provider/application_form.html:10
-msgid "Edit application"
-msgstr "Anwendung bearbeiten"
+#: aleksis/core/templates/oauth2_provider/application/edit.html:5
+#: aleksis/core/templates/oauth2_provider/application/edit.html:6
+msgid "Edit OAuth2 Application"
+msgstr "OAuth2-Anwendung bearbeiten"
 
-#: aleksis/core/templates/oauth2_provider/application_list.html:8
-msgid "OAuth2 applications"
-msgstr "OAuth2-Anwendungen"
+#: aleksis/core/templates/oauth2_provider/application/list.html:11
+msgid "Register new application"
+msgstr "Neue Anwendung registrieren"
 
-#: aleksis/core/templates/oauth2_provider/application_list.html:12
-msgid "Register new applications"
-msgstr "Neue Anwendungen registrieren"
-
-#: aleksis/core/templates/oauth2_provider/application_list.html:23
+#: aleksis/core/templates/oauth2_provider/application/list.html:25
 msgid "No applications defined."
 msgstr "Keine Anwendungen definiert."
 
 #: aleksis/core/templates/oauth2_provider/authorize.html:5
-#: aleksis/core/templates/oauth2_provider/authorize.html:16
+#: aleksis/core/templates/socialaccount/login.html:5
+#: aleksis/core/templates/socialaccount/login.html:6
 msgid "Authorize"
 msgstr "Autorisieren"
 
-#: aleksis/core/templates/oauth2_provider/authorize.html:18
+#: aleksis/core/templates/oauth2_provider/authorize.html:23
+#, python-format
+msgid "Authorize %(name)s"
+msgstr "%(name)s autorisieren"
+
+#: aleksis/core/templates/oauth2_provider/authorize.html:25
 msgid "The application requests access to the following scopes:"
 msgstr "Die Anwendung fordert Zugriff auf die folgenden Bereiche an:"
 
-#: aleksis/core/templates/oauth2_provider/authorize.html:33
+#: aleksis/core/templates/oauth2_provider/authorize.html:40
 msgid "Allow"
 msgstr "Erlauben"
 
-#: aleksis/core/templates/oauth2_provider/authorize.html:36
+#: aleksis/core/templates/oauth2_provider/authorize.html:43
 msgid "Disallow"
 msgstr "Verbieten"
 
@@ -2374,6 +2578,20 @@ msgstr "Sie haben aktuell keine Drittanbieter-Konten mit Ihrem Konto verbunden."
 msgid "Add a Third-party Account"
 msgstr "Ein Drittanbieter-Konto hinzufügen"
 
+#: aleksis/core/templates/socialaccount/login.html:12
+#, python-format
+msgid "You are about to connect a new third party account from %(provider)s."
+msgstr "Sie sind dabei, ein neues Drittanbieterkonto von %(provider)s zu verbinden."
+
+#: aleksis/core/templates/socialaccount/login.html:23
+#, python-format
+msgid "You are about to sign in using a third party account from %(provider)s."
+msgstr "Sie sind dabei, sich mit einem Drittanbieterkonto von %(provider)s anzumelden."
+
+#: aleksis/core/templates/socialaccount/login.html:28
+msgid "Continue"
+msgstr "Fortsetzen"
+
 #: aleksis/core/templates/socialaccount/login_cancelled.html:5
 #: aleksis/core/templates/socialaccount/login_cancelled.html:6
 #: aleksis/core/templates/socialaccount/login_cancelled.html:13
@@ -2432,30 +2650,65 @@ msgstr ""
 "          Keine Drittanbieter verfügbar.\n"
 "        "
 
-#: aleksis/core/templates/templated_email/data_checks.email:4
-msgid "The system detected some new problems with your data."
-msgstr "Das System hat einige neue Probleme mit Ihren Daten entdeckt."
+#: aleksis/core/templates/templated_email/base.email:5
+#: aleksis/core/templates/templated_email/base.email:16
+msgid "Hello"
+msgstr "Hallo"
 
-#: aleksis/core/templates/templated_email/data_checks.email:8
-#: aleksis/core/templates/templated_email/data_checks.email:24
-#: aleksis/core/templates/templated_email/person_changed.email:8
-#: aleksis/core/templates/templated_email/person_changed.email:20
-msgid "Hello,"
-msgstr "Hallo,"
+#: aleksis/core/templates/templated_email/celery_failure.email:4
+#, python-format
+msgid "Celery task %(task_name)s failed!"
+msgstr "Celery-Task %(task_name)s fehlgeschlagen!"
 
-#: aleksis/core/templates/templated_email/data_checks.email:10
+#: aleksis/core/templates/templated_email/celery_failure.email:7
+#, python-format
+msgid "the celery task %(task_name)s failed with following information:"
+msgstr "der Celery-Task  %(task_name)s ist mit folgenden Informationen fehlgeschlagen:"
+
+#: aleksis/core/templates/templated_email/celery_failure.email:10
+#: aleksis/core/templates/templated_email/celery_failure.email:29
+msgid "Task ID"
+msgstr "Task-ID"
+
+#: aleksis/core/templates/templated_email/celery_failure.email:11
+#: aleksis/core/templates/templated_email/celery_failure.email:30
+msgid "Raised exception"
+msgstr "Geworfene Exception"
+
+#: aleksis/core/templates/templated_email/celery_failure.email:12
+#: aleksis/core/templates/templated_email/celery_failure.email:31
+msgid "Positional arguments"
+msgstr "Argumente"
+
+#: aleksis/core/templates/templated_email/celery_failure.email:15
+#: aleksis/core/templates/templated_email/celery_failure.email:38
+msgid "Keyword arguments"
+msgstr "Keyword-Argumente"
+
+#: aleksis/core/templates/templated_email/celery_failure.email:22
+#, python-format
 msgid ""
 "\n"
-"  the system detected some new problems with your data.\n"
-"  Please take some time to inspect them and solve the issues or mark them as ignored.\n"
-" "
+"      the celery task %(task_name)s failed with following information:\n"
+"    "
 msgstr ""
 "\n"
-"  das System hat einige neue Probleme mit Ihren Daten entdeckt.\n"
-"Bitte nehmen Sie sich etwas Zeit, diese zu überprüfen und sie zu lösen oder als ignoriert zu markieren.\n"
-" "
+"      der Celery-Task  %(task_name)s ist mit folgenden Informationen fehlgeschlagen:\n"
+"    "
 
-#: aleksis/core/templates/templated_email/data_checks.email:26
+#: aleksis/core/templates/templated_email/data_checks.email:3
+msgid "The system detected some new problems with your data."
+msgstr "Das System hat einige neue Probleme mit Ihren Daten entdeckt."
+
+#: aleksis/core/templates/templated_email/data_checks.email:6
+msgid ""
+"the system detected some new problems with your data.\n"
+"Please take some time to inspect them and solve the issues or mark them as ignored."
+msgstr ""
+"das System hat einige neue Probleme mit Ihren Daten entdeckt.\n"
+"Bitte nehmen Sie sich etwas Zeit, diese zu überprüfen und sie zu lösen oder als ignoriert zu markieren."
+
+#: aleksis/core/templates/templated_email/data_checks.email:15
 msgid ""
 "\n"
 "   the system detected some new problems with your data.\n"
@@ -2467,46 +2720,40 @@ msgstr ""
 "Bitte nehmen Sie sich etwas Zeit, diese zu überprüfen und sie zu lösen oder als ignoriert zu markieren.\n"
 "  "
 
-#: aleksis/core/templates/templated_email/data_checks.email:34
+#: aleksis/core/templates/templated_email/data_checks.email:23
 msgid "Problem description"
 msgstr "Problembeschreibung"
 
-#: aleksis/core/templates/templated_email/data_checks.email:35
+#: aleksis/core/templates/templated_email/data_checks.email:24
 msgid "Count of objects with new problems"
 msgstr "Anzahl der Objekte mit neuen Problemen"
 
-#: aleksis/core/templates/templated_email/notification.email:3
+#: aleksis/core/templates/templated_email/notification.email:4
 msgid "New notification for"
 msgstr "Neue Benachrichtigung für"
 
 #: aleksis/core/templates/templated_email/notification.email:6
-#: aleksis/core/templates/templated_email/notification.email:27
+#: aleksis/core/templates/templated_email/notification.email:20
 #, python-format
-msgid "Dear %(notification_user)s,"
-msgstr "Liebe(r) %(notification_user)s,"
+msgid "Hello %(notification_user)s,"
+msgstr "Hallo %(notification_user)s,"
 
-#: aleksis/core/templates/templated_email/notification.email:8
-#: aleksis/core/templates/templated_email/notification.email:29
+#: aleksis/core/templates/templated_email/notification.email:9
+#: aleksis/core/templates/templated_email/notification.email:23
 msgid "we got a new notification for you:"
 msgstr "wir haben eine neue Benachrichtigung für Sie:"
 
 #: aleksis/core/templates/templated_email/notification.email:15
-#: aleksis/core/templates/templated_email/notification.email:35
+#: aleksis/core/templates/templated_email/notification.email:29
 msgid "More information"
 msgstr "Mehr Informationen"
 
-#: aleksis/core/templates/templated_email/notification.email:18
+#: aleksis/core/templates/templated_email/notification.email:17
 #, python-format
-msgid ""
-"\n"
-"        Sent by %(trans_sender)s at %(trans_created_at)s\n"
-"    "
-msgstr ""
-"\n"
-"        Von %(trans_sender)s am %(trans_created_at)s\n"
-"    "
+msgid "Sent by %(trans_sender)s at %(trans_created_at)s"
+msgstr "Von %(trans_sender)s am %(trans_created_at)s"
 
-#: aleksis/core/templates/templated_email/notification.email:40
+#: aleksis/core/templates/templated_email/notification.email:34
 #, python-format
 msgid ""
 "\n"
@@ -2522,18 +2769,12 @@ msgstr ""
 msgid "%(person)s changed their data!"
 msgstr "%(person)s hat Daten verändert!"
 
-#: aleksis/core/templates/templated_email/person_changed.email:10
+#: aleksis/core/templates/templated_email/person_changed.email:7
 #, python-format
-msgid ""
-"\n"
-"   the person %(person)s recently changed the following fields:\n"
-" "
-msgstr ""
-"\n"
-"   die Person %(person)s hat kürzlich die folgenden Felder geändert:\n"
-" "
+msgid "the person %(person)s recently changed the following fields:"
+msgstr "die Person %(person)s hat kürzlich die folgenden Felder geändert:"
 
-#: aleksis/core/templates/templated_email/person_changed.email:22
+#: aleksis/core/templates/templated_email/person_changed.email:15
 #, python-format
 msgid ""
 "\n"
@@ -2596,11 +2837,16 @@ msgstr "Zurück zur Kontosicherheit"
 msgid "Generate Tokens"
 msgstr "Tokens generieren"
 
-#: aleksis/core/templates/two_factor/core/login.html:20
+#: aleksis/core/templates/two_factor/core/login.html:27
+#, python-format
+msgid "Login for %(name)s"
+msgstr "Anmelden für %(name)s"
+
+#: aleksis/core/templates/two_factor/core/login.html:30
 msgid "Login with username and password"
 msgstr "Anmeldung mit Benutzername und Passwort"
 
-#: aleksis/core/templates/two_factor/core/login.html:28
+#: aleksis/core/templates/two_factor/core/login.html:38
 msgid ""
 "You have no permission to view this page. Please login with an other\n"
 "                    account."
@@ -2608,11 +2854,15 @@ msgstr ""
 "Sie haben keine Berichtigung, um diese Seite aufzurufen. \n"
 "Bitte loggen Sie sich mit einem anderen Account ein."
 
-#: aleksis/core/templates/two_factor/core/login.html:36
+#: aleksis/core/templates/two_factor/core/login.html:47
+msgid "Please login with your account to use the external application."
+msgstr "Bitte melden Sie sich mit Ihrem Konto an, um die externe Anwendung zu benutzen."
+
+#: aleksis/core/templates/two_factor/core/login.html:54
 msgid "Please login to see this page."
 msgstr "Bitte melden Sie sich an, um diese Seite zu sehen."
 
-#: aleksis/core/templates/two_factor/core/login.html:46
+#: aleksis/core/templates/two_factor/core/login.html:65
 msgid ""
 "\n"
 "                        We are calling your phone right now, please enter the\n"
@@ -2624,7 +2874,7 @@ msgstr ""
 "            bitte geben Sie die Zahlen ein, die Sie hören.\n"
 "                      "
 
-#: aleksis/core/templates/two_factor/core/login.html:51
+#: aleksis/core/templates/two_factor/core/login.html:70
 msgid ""
 "\n"
 "                        We sent you a text message, please enter the tokens we\n"
@@ -2636,7 +2886,7 @@ msgstr ""
 "            bitte geben Sie die Token ein, die wir geschickt haben.\n"
 "                      "
 
-#: aleksis/core/templates/two_factor/core/login.html:56
+#: aleksis/core/templates/two_factor/core/login.html:75
 msgid ""
 "\n"
 "                        Please enter the tokens generated by your token\n"
@@ -2648,7 +2898,7 @@ msgstr ""
 "              generierten Token ein.\n"
 "                      "
 
-#: aleksis/core/templates/two_factor/core/login.html:62
+#: aleksis/core/templates/two_factor/core/login.html:81
 msgid ""
 "\n"
 "                      Use this form for entering backup tokens for logging in.\n"
@@ -2657,30 +2907,28 @@ msgid ""
 "                    "
 msgstr ""
 "\n"
-"                      Nutzen Sie dieses Formular um Ihre Backup-Tokens zum "
-"Anmelden einzugeben.\n"
-"                Diese Tokens wurden für Sie generiert, um diese gut "
-"aufzubewahren. Bitte\n"
+"                      Nutzen Sie dieses Formular um Ihre Backup-Tokens zum Anmelden einzugeben.\n"
+"                Diese Tokens wurden für Sie generiert, um diese gut aufzubewahren. Bitte\n"
 "                geben Sie einen dieser Tokens ein, um sich einzuloggen.\n"
 "                    "
 
-#: aleksis/core/templates/two_factor/core/login.html:90
+#: aleksis/core/templates/two_factor/core/login.html:109
 msgid "Device currently not available?"
 msgstr "Gerät aktuell nicht verfügbar?"
 
-#: aleksis/core/templates/two_factor/core/login.html:92
+#: aleksis/core/templates/two_factor/core/login.html:111
 msgid "Or, alternatively, use one of your backup phones:"
 msgstr "Oder, alternativ, nutzen Sie eins Ihrer Backup-Telefone:"
 
-#: aleksis/core/templates/two_factor/core/login.html:102
+#: aleksis/core/templates/two_factor/core/login.html:121
 msgid "As a last resort, you can use a backup token:"
 msgstr "Als letzte Möglichkeit können Sie einen Backup-Token nutzen:"
 
-#: aleksis/core/templates/two_factor/core/login.html:105
+#: aleksis/core/templates/two_factor/core/login.html:124
 msgid "Use Backup Token"
 msgstr "Backup-Token nutzen"
 
-#: aleksis/core/templates/two_factor/core/login.html:116
+#: aleksis/core/templates/two_factor/core/login.html:135
 msgid "Use alternative login options"
 msgstr "Alternative Anmeldemöglichkeiten nutzen"
 
@@ -2810,8 +3058,7 @@ msgid ""
 "          "
 msgstr ""
 "\n"
-"            Wir haben Ihnen eine SMS geschickt, bitte geben Sie die Tokens "
-"ein, die wir geschickt haben.\n"
+"            Wir haben Ihnen eine SMS geschickt, bitte geben Sie die Tokens ein, die wir geschickt haben.\n"
 "          "
 
 #: aleksis/core/templates/two_factor/core/setup.html:63
@@ -2994,139 +3241,312 @@ msgstr ""
 "          Accountsicherheit.\n"
 "      "
 
-#: aleksis/core/util/notifications.py:65
+#: aleksis/core/util/notifications.py:63
 msgid "E-Mail"
 msgstr "E-Mail"
 
-#: aleksis/core/util/notifications.py:66
+#: aleksis/core/util/notifications.py:64
 msgid "SMS"
 msgstr "SMS"
 
-#: aleksis/core/util/pdf.py:105
+#: aleksis/core/util/pdf.py:118
 msgid "Progress: Generate PDF file"
 msgstr "Fortschritt: PDF-Datei generieren"
 
-#: aleksis/core/util/pdf.py:106
+#: aleksis/core/util/pdf.py:119
 msgid "Generating PDF file …"
 msgstr "PDF-Datei wird generiert …"
 
-#: aleksis/core/util/pdf.py:107
+#: aleksis/core/util/pdf.py:120
 msgid "The PDF file has been generated successfully."
 msgstr "Die PDF-Datei wurde erfolgreich generiert."
 
-#: aleksis/core/util/pdf.py:108
+#: aleksis/core/util/pdf.py:121
 msgid "There was a problem while generating the PDF file."
 msgstr "Es ist ein Fehler beim Generieren der PDF-Datei aufgetreten."
 
-#: aleksis/core/util/pdf.py:111
+#: aleksis/core/util/pdf.py:124
 msgid "Download PDF"
 msgstr "PDF herunterladen"
 
-#: aleksis/core/views.py:251
+#: aleksis/core/views.py:285
 msgid "The school term has been created."
 msgstr "Das Schuljahr wurde erstellt."
 
-#: aleksis/core/views.py:263
+#: aleksis/core/views.py:297
 msgid "The school term has been saved."
 msgstr "Das Schuljahr wurde gespeichert."
 
-#: aleksis/core/views.py:387
+#: aleksis/core/views.py:417
 msgid "The child groups were successfully saved."
 msgstr "Die Untergruppen wurden gespeichert."
 
-#: aleksis/core/views.py:406 aleksis/core/views.py:416
+#: aleksis/core/views.py:436 aleksis/core/views.py:446
 msgid "The person has been saved."
 msgstr "Die Person wurde gespeichert."
 
-#: aleksis/core/views.py:466
+#: aleksis/core/views.py:496
 msgid "The group has been saved."
 msgstr "Die Gruppe wurde gespeichert."
 
-#: aleksis/core/views.py:563
+#: aleksis/core/views.py:593
 msgid "The announcement has been saved."
 msgstr "Die Ankündigung wurde gespeichert."
 
-#: aleksis/core/views.py:579
+#: aleksis/core/views.py:609
 msgid "The announcement has been deleted."
 msgstr "Ankündigung wurde gelöscht."
 
-#: aleksis/core/views.py:663
+#: aleksis/core/views.py:677
+msgid "The requested preference registry does not exist"
+msgstr "Das angeforderte Einstellungsregister existiert nicht"
+
+#: aleksis/core/views.py:696
 msgid "The preferences have been saved successfully."
 msgstr "Die Einstellungen wurde gespeichert."
 
-#: aleksis/core/views.py:687
+#: aleksis/core/views.py:720
 msgid "The person has been deleted."
 msgstr "Die Person wurde gelöscht."
 
-#: aleksis/core/views.py:701
+#: aleksis/core/views.py:734
 msgid "The group has been deleted."
 msgstr "Die Gruppe wurde gelöscht."
 
-#: aleksis/core/views.py:733
+#: aleksis/core/views.py:766
 msgid "The additional_field has been saved."
 msgstr "Das zusätzliche Feld wurde gespeichert."
 
-#: aleksis/core/views.py:767
+#: aleksis/core/views.py:800
 msgid "The additional field has been deleted."
 msgstr "Das zusätzliche Feld wurde gelöscht."
 
-#: aleksis/core/views.py:792
+#: aleksis/core/views.py:825
 msgid "The group type has been saved."
 msgstr "Der Gruppentyp wurde gespeichert."
 
-#: aleksis/core/views.py:822
+#: aleksis/core/views.py:855
 msgid "The group type has been deleted."
 msgstr "Der Gruppentyp wurde gelöscht."
 
-#: aleksis/core/views.py:855
+#: aleksis/core/views.py:888
 msgid "Progress: Run data checks"
 msgstr "Fortschritt: Datenprüfungen ausführen"
 
-#: aleksis/core/views.py:856
+#: aleksis/core/views.py:889
 msgid "Run data checks …"
 msgstr "Datenprüfungen laufen …"
 
-#: aleksis/core/views.py:857
+#: aleksis/core/views.py:890
 msgid "The data checks were run successfully."
 msgstr "Die Datenprüfungen wurden erfolgreich ausgeführt."
 
-#: aleksis/core/views.py:858
+#: aleksis/core/views.py:891
 msgid "There was a problem while running data checks."
 msgstr "Es gab ein Problem beim Ausführen der Datenprüfungen."
 
-#: aleksis/core/views.py:874
+#: aleksis/core/views.py:907
 #, python-brace-format
 msgid "The solve option '{solve_option_obj.verbose_name}' "
 msgstr "Die Lösungsoption \"{solve_option_obj.verbose_name}\" "
 
-#: aleksis/core/views.py:916
+#: aleksis/core/views.py:917
+msgid "The requested solve option does not exist"
+msgstr "Die angeforderte Lösungsoption existiert nicht"
+
+#: aleksis/core/views.py:949
 msgid "The dashboard widget has been saved."
 msgstr "Das Dashboard-Widget wurde gespeichert."
 
-#: aleksis/core/views.py:946
+#: aleksis/core/views.py:979
 msgid "The dashboard widget has been created."
 msgstr "Das Dashboard-Widget wurde erstellt."
 
-#: aleksis/core/views.py:956
+#: aleksis/core/views.py:989
 msgid "The dashboard widget has been deleted."
 msgstr "Das Dashboard-Widget wurde gelöscht."
 
-#: aleksis/core/views.py:1023
+#: aleksis/core/views.py:1060
 msgid "Your dashboard configuration has been saved successfully."
 msgstr "Ihre Dashboardkonfiguration wurde erfolgreich gespeichert."
 
-#: aleksis/core/views.py:1025
+#: aleksis/core/views.py:1062
 msgid "The configuration of the default dashboard has been saved successfully."
 msgstr "Die Konfiguration des Standard-Dashboardes wurde erfolgreich gespeichert."
 
-#: aleksis/core/views.py:1153
+#: aleksis/core/views.py:1138
+#, python-brace-format
+msgid "The invitation was successfully created. The invitation code is {code}"
+msgstr "Die Einladung wurde erfolgreich erstellt. Der Einladungscode ist {code}"
+
+#: aleksis/core/views.py:1229
+msgid "We have successfully assigned the permissions."
+msgstr "Wir haben die Berechtigungen erfolgreich zugewiesen."
+
+#: aleksis/core/views.py:1239
+msgid "The global user permission has been deleted."
+msgstr "Die globale Benutzerberechtigung wurde gelöscht."
+
+#: aleksis/core/views.py:1249
+msgid "The global group permission has been deleted."
+msgstr "Die globale Gruppenberechtigung wurde gelöscht."
+
+#: aleksis/core/views.py:1259
+msgid "The object user permission has been deleted."
+msgstr "Die Objekt-Benutzerberechtigung wurde gelöscht."
+
+#: aleksis/core/views.py:1269
+msgid "The object group permission has been deleted."
+msgstr "Die Objekt-Gruppenberechtigung wurde gelöscht."
+
+#: aleksis/core/views.py:1337
+msgid "The requested PDF file does not exist"
+msgstr "Die angeforderte PDF-Datei existiert nicht"
+
+#: aleksis/core/views.py:1346 aleksis/core/views.py:1350
+msgid "The requested task does not exist or is not accessible"
+msgstr "Der angeforderte Task existiert nicht oder ist nicht abrufbar"
+
+#: aleksis/core/views.py:1388
 msgid "The third-party account could not be disconnected because it is the only login method available."
 msgstr "Das Drittanbieter-Konto konnte nicht deaktiviert werden, weil es die einzige verfügbare Anmeldeoption ist."
 
-#: aleksis/core/views.py:1160
+#: aleksis/core/views.py:1395
 msgid "The third-party account has been successfully disconnected."
 msgstr "Das Drittanbieter-Konto wurde erfolgreich getrennt."
 
+#: aleksis/core/views.py:1466
+msgid "Person was invited successfully and an email with further instructions has been send to them."
+msgstr ""
+"Die Person wurde erfolgreich eingeladen und eine E-Mail mit weiteren "
+"Anweisungen wurde an sie verschickt."
+
+#: aleksis/core/views.py:1477
+msgid "Person was already invited."
+msgstr "Person wurde bereits eingeladen."
+
+#~ msgid "Person was invited successfully."
+#~ msgstr "Person wurde erfolgreich eingeladen."
+
+#, python-brace-format
+#~ msgid "Last backup {time_gone_since_backup}!"
+#~ msgstr "Letztes Backup: {time_gone_since_backup}!"
+
+#, python-brace-format
+#~ msgid "{task.status} - {task.result}"
+#~ msgstr "{task.status} - {task.result}"
+
+#~ msgid "home number"
+#~ msgstr "Festnetznummer"
+
+#~ msgid "mobile number"
+#~ msgstr "Handynummer"
+
+#~ msgid "Hello,"
+#~ msgstr "Hallo,"
+
+#, python-format
+#~ msgid ""
+#~ "\n"
+#~ " * Task name: %(task_name)s\n"
+#~ " * Task: %(task)s\n"
+#~ " * Id of the task: %(task_id)s\n"
+#~ " * Exception instance raised: %(exception)s\n"
+#~ " * Positional arguments the task was called with: %(args)s\n"
+#~ " * Keyword arguments the task was called with: %(kwargs)s\n"
+#~ " * Stack trace object: %(traceback)s\n"
+#~ msgstr ""
+#~ "\n"
+#~ " * Task-Name: %(task_name)s\n"
+#~ " * Task: %(task)s\n"
+#~ " * ID des Tasks: %(task_id)s\n"
+#~ " * Instanz der aufgetretenen Exception: %(exception)s\n"
+#~ " * Argumente, mit denen der Task aufgerufen wurde: %(args)s\n"
+#~ " * Keyword-Argumente, mit denen der Task aufgerufen wurde: %(kwargs)s\n"
+#~ " * Stacktrace: %(traceback)s\n"
+
+#, python-format
+#~ msgid ""
+#~ "\n"
+#~ "   <li>Task name: %(task_name)s</li>\n"
+#~ "   <li>Task: %(task)s</li>\n"
+#~ "   <li>Id of the task: %(task_id)s</li>\n"
+#~ "   <li>Exception instance raised: %(exception)s</li>\n"
+#~ "   <li>Positional arguments the task was called with: %(args)s</li>\n"
+#~ "   <li>Keyword arguments the task was called with: %(kwargs)s</li>\n"
+#~ "   <li>Stack trace object: %(traceback)s</li>\n"
+#~ " </ul>\n"
+#~ " "
+#~ msgstr ""
+#~ "\n"
+#~ "   <li>Task-Name: %(task_name)s</li>\n"
+#~ "   <li>Task: %(task)s</li>\n"
+#~ "   <li>ID des Tasks: %(task_id)s</li>\n"
+#~ "   <li>Aufgetretene Exception: %(exception)s</li>\n"
+#~ "   <li>Argumente, mit denen der Task aufgerufen wurde: %(args)s</li>\n"
+#~ "   <li>Keyword-Argumente, mit denen der Task aufgerufen wurde: %(kwargs)s</li>\n"
+#~ "   <li>Stacktrace: %(traceback)s</li>\n"
+#~ " </ul>\n"
+#~ " "
+
+#~ msgid "Is person active?"
+#~ msgstr "Ist die Person aktiv?"
+
+#~ msgid ""
+#~ "The requested page or object was not\n"
+#~ "          found."
+#~ msgstr ""
+#~ "Die angefragte Seite oder das angefragte Objekt wurde nicht\n"
+#~ "            gefunden."
+
+#~ msgid "Consents"
+#~ msgstr "Zustimmungen"
+
+#, python-brace-format
+#~ msgid "I have read the <a href='{privacy_policy}'>Privacy policy</a> and agree with them."
+#~ msgstr "Ich habe die <a href='{privacy_policy}'>Datenschutzerklärung</a> gelesen und stimme ihr zu."
+
+#~ msgid "You must type the same password each time."
+#~ msgstr "Sie müssen zweimal das gleiche Passwort eingeben."
+
+#~ msgid "Impersonation"
+#~ msgstr "Verkleidung"
+
+#, python-format
+#~ msgid "<strong>Note:</strong> you can still <a href=\"%(email_url)s\">change your e-mail address</a>"
+#~ msgstr "<strong>Hinweis:</strong> Sie können immer noch <a href=\"%(email_url)s\"> Ihre E-Mail-Adresse ändern</a>"
+
+#~ msgid "Impress"
+#~ msgstr "Impressum"
+
+#~ msgid "Can add oauth applications"
+#~ msgstr "Kann OAuth-Anwendungen hinzufügen"
+
+#~ msgid "Can view oauth applications"
+#~ msgstr "Kann OAuth-Anwendungen sehen"
+
+#~ msgid "Can update oauth applications"
+#~ msgstr "Kann OAuth-Anwendungen aktualisieren"
+
+#~ msgid "Can delete oauth applications"
+#~ msgstr "Kann OAuth-Anwendungen löschen"
+
+#~ msgid "Delete application"
+#~ msgstr "Anwendung löschen"
+
+#, python-format
+#~ msgid "Are you sure to delete the application %(application_name)s?"
+#~ msgstr "Sind Sie sicher, dass Sie die Anwendung %(application_name)s löschen möchten?"
+
+#~ msgid "Authorization Grant Type"
+#~ msgstr "Authorization Grant-Typ"
+
+#~ msgid "Edit application"
+#~ msgstr "Anwendung bearbeiten"
+
+#~ msgid "OAuth2 applications"
+#~ msgstr "OAuth2-Anwendungen"
+
 #~ msgid "Persons and accounts"
 #~ msgstr "Personen und Konten"
 
@@ -3154,21 +3574,12 @@ msgstr "Das Drittanbieter-Konto wurde erfolgreich getrennt."
 #~ "    eingegebenen Benutzernamen und kopiert alle anderen Daten der Person.\n"
 #~ "      "
 
-#~ msgid "Update"
-#~ msgstr "Aktualisieren"
-
 #~ msgid "Existing account"
 #~ msgstr "Existierendes Konto"
 
 #~ msgid "New account"
 #~ msgstr "Neues Konto"
 
-#~ msgid "Impersonate user"
-#~ msgstr "Als Benutzer verkleiden"
-
-#~ msgid "Authorized tokens"
-#~ msgstr "Autorisierte Tokens"
-
 #~ msgid "No authorized tokens defined."
 #~ msgstr "Keine autorisierten Token definiert."
 
@@ -3430,9 +3841,6 @@ msgstr "Das Drittanbieter-Konto wurde erfolgreich getrennt."
 #~ msgid "Assorted"
 #~ msgstr "Unsortiert"
 
-#~ msgid "Administration"
-#~ msgstr "Verwaltung"
-
 #~ msgid "List of all groups"
 #~ msgstr "Liste aller Gruppen"
 
diff --git a/aleksis/core/locale/de_DE/LC_MESSAGES/djangojs.po b/aleksis/core/locale/de_DE/LC_MESSAGES/djangojs.po
index 6bbb636d795adbbe19acdf95ed873b27814d9284..376788f8a7ea941b89dff0620fdbc78a40c106a5 100644
--- a/aleksis/core/locale/de_DE/LC_MESSAGES/djangojs.po
+++ b/aleksis/core/locale/de_DE/LC_MESSAGES/djangojs.po
@@ -7,11 +7,10 @@ msgid ""
 msgstr ""
 "Project-Id-Version: PACKAGE VERSION\n"
 "Report-Msgid-Bugs-To: \n"
-"POT-Creation-Date: 2021-08-28 17:53+0200\n"
+"POT-Creation-Date: 2022-02-08 23:16+0000\n"
 "PO-Revision-Date: 2021-10-28 14:37+0000\n"
 "Last-Translator: Jonathan Weth <teckids@jonathanweth.de>\n"
-"Language-Team: German <https://translate.edugit.org/projects/aleksis/"
-"aleksis-core-js/de/>\n"
+"Language-Team: German <https://translate.edugit.org/projects/aleksis/aleksis-core-js/de/>\n"
 "Language: de_DE\n"
 "MIME-Version: 1.0\n"
 "Content-Type: text/plain; charset=UTF-8\n"
@@ -19,20 +18,18 @@ msgstr ""
 "Plural-Forms: nplurals=2; plural=n != 1;\n"
 "X-Generator: Weblate 4.8\n"
 
-#: aleksis/core/static/js/main.js:15
+#: aleksis/core/static/js/main.js:66
 msgid "Today"
 msgstr "Heute"
 
-#: aleksis/core/static/js/main.js:16
+#: aleksis/core/static/js/main.js:67
 msgid "Cancel"
 msgstr "Abbrechen"
 
-#: aleksis/core/static/js/main.js:17
+#: aleksis/core/static/js/main.js:68
 msgid "OK"
 msgstr "OK"
 
-#: aleksis/core/static/js/main.js:127
+#: aleksis/core/static/js/main.js:191
 msgid "This page may contain outdated information since there is no internet connection."
-msgstr ""
-"Diese Seite enthält vielleicht veraltete Informationen, da es keine "
-"Internetverbindung gibt."
+msgstr "Diese Seite enthält vielleicht veraltete Informationen, da es keine Internetverbindung gibt."
diff --git a/aleksis/core/locale/fr/LC_MESSAGES/django.po b/aleksis/core/locale/fr/LC_MESSAGES/django.po
index 3c118dd9a39119c4615e68b5f729d60de3726bff..ac9b5f99c721e75f4dd0e5bf94368eaee2c7867f 100644
--- a/aleksis/core/locale/fr/LC_MESSAGES/django.po
+++ b/aleksis/core/locale/fr/LC_MESSAGES/django.po
@@ -7,7 +7,7 @@ msgid ""
 msgstr ""
 "Project-Id-Version: AlekSIS (School Information System) 0.1\n"
 "Report-Msgid-Bugs-To: \n"
-"POT-Creation-Date: 2021-10-28 16:18+0200\n"
+"POT-Creation-Date: 2022-02-08 23:16+0000\n"
 "PO-Revision-Date: 2021-06-16 12:00+0000\n"
 "Last-Translator: Jonathan Weth <teckids@jonathanweth.de>\n"
 "Language-Team: French <https://translate.edugit.org/projects/aleksis/aleksis/fr/>\n"
@@ -18,33 +18,42 @@ msgstr ""
 "Plural-Forms: nplurals=2; plural=n > 1;\n"
 "X-Generator: Weblate 4.4\n"
 
-#: aleksis/core/apps.py:150
+#: aleksis/core/apps.py:166
 msgid "OpenID Connect scope"
 msgstr ""
 
-#: aleksis/core/apps.py:151
+#: aleksis/core/apps.py:167
 msgid "Given name, family name, link to profile and picture if existing."
 msgstr ""
 
-#: aleksis/core/apps.py:152
+#: aleksis/core/apps.py:168
 msgid "Full home postal address"
 msgstr ""
 
-#: aleksis/core/apps.py:153
+#: aleksis/core/apps.py:169
 #, fuzzy
 #| msgid "Contact details"
 msgid "Email address"
 msgstr "Détails de contact"
 
-#: aleksis/core/apps.py:154
+#: aleksis/core/apps.py:170
 msgid "Home and mobile phone"
 msgstr ""
 
-#: aleksis/core/data_checks.py:55
+#: aleksis/core/apps.py:171 aleksis/core/forms.py:220 aleksis/core/menus.py:265
+#: aleksis/core/models.py:462 aleksis/core/templates/core/group/list.html:8
+#: aleksis/core/templates/core/group/list.html:9
+#: aleksis/core/templates/core/person/full.html:250
+#, fuzzy
+#| msgid "Group"
+msgid "Groups"
+msgstr "Groupe"
+
+#: aleksis/core/data_checks.py:56
 msgid "Ignore problem"
 msgstr ""
 
-#: aleksis/core/data_checks.py:184
+#: aleksis/core/data_checks.py:185
 #, python-brace-format
 msgid "Solve option '{solve_option_obj.verbose_name}' "
 msgstr ""
@@ -61,8 +70,8 @@ msgstr ""
 msgid "The DashboardWidget was reported broken automatically."
 msgstr ""
 
-#: aleksis/core/filters.py:37 aleksis/core/templates/core/base.html:83
-#: aleksis/core/templates/core/base.html:84
+#: aleksis/core/filters.py:42 aleksis/core/templates/core/base.html:105
+#: aleksis/core/templates/core/base.html:106
 #: aleksis/core/templates/core/group/list.html:20
 #: aleksis/core/templates/core/person/list.html:24
 #: aleksis/core/templates/search/search.html:7
@@ -70,64 +79,80 @@ msgstr ""
 msgid "Search"
 msgstr ""
 
-#: aleksis/core/filters.py:53
+#: aleksis/core/filters.py:59
 msgid "Search by name"
 msgstr ""
 
-#: aleksis/core/filters.py:65
+#: aleksis/core/filters.py:71
 #, fuzzy
 #| msgid "Contact details"
 msgid "Search by contact details"
 msgstr "Détails de contact"
 
-#: aleksis/core/forms.py:41 aleksis/core/forms.py:387
+#: aleksis/core/filters.py:92
+msgid "Permission"
+msgstr ""
+
+#: aleksis/core/filters.py:100
+msgid "Content type"
+msgstr ""
+
+#: aleksis/core/filters.py:113 aleksis/core/models.py:688
+msgid "User"
+msgstr ""
+
+#: aleksis/core/filters.py:135 aleksis/core/models.py:461
+msgid "Group"
+msgstr "groupe"
+
+#: aleksis/core/forms.py:50 aleksis/core/forms.py:581
 msgid "Base data"
 msgstr ""
 
-#: aleksis/core/forms.py:47
+#: aleksis/core/forms.py:55
 msgid "Address"
 msgstr ""
 
-#: aleksis/core/forms.py:48
+#: aleksis/core/forms.py:56 aleksis/core/forms.py:590
 #, fuzzy
 #| msgid "Contact details"
 msgid "Contact data"
 msgstr "Détails de contact"
 
-#: aleksis/core/forms.py:50
+#: aleksis/core/forms.py:58
 #, fuzzy
 #| msgid "Contact details"
 msgid "Advanced personal data"
 msgstr "Détails de contact"
 
-#: aleksis/core/forms.py:93
+#: aleksis/core/forms.py:106
 msgid "New user"
 msgstr ""
 
-#: aleksis/core/forms.py:93
+#: aleksis/core/forms.py:106
 msgid "Create a new account"
 msgstr ""
 
-#: aleksis/core/forms.py:124
+#: aleksis/core/forms.py:132
 msgid "You cannot set a new username when also selecting an existing user."
 msgstr ""
 
-#: aleksis/core/forms.py:128
+#: aleksis/core/forms.py:136
 msgid "This username is already in use."
 msgstr "Cet nom est deja en utilisation."
 
-#: aleksis/core/forms.py:145 aleksis/core/models.py:117
+#: aleksis/core/forms.py:153 aleksis/core/models.py:130
 msgid "School term"
 msgstr ""
 
-#: aleksis/core/forms.py:146
+#: aleksis/core/forms.py:154
 #, fuzzy
 #| msgid "Contact details"
 msgid "Common data"
 msgstr "Détails de contact"
 
-#: aleksis/core/forms.py:147 aleksis/core/forms.py:196
-#: aleksis/core/menus.py:238 aleksis/core/models.py:140
+#: aleksis/core/forms.py:155 aleksis/core/forms.py:207
+#: aleksis/core/menus.py:254 aleksis/core/models.py:153
 #: aleksis/core/templates/core/person/list.html:8
 #: aleksis/core/templates/core/person/list.html:9
 #, fuzzy
@@ -135,83 +160,114 @@ msgstr "Détails de contact"
 msgid "Persons"
 msgstr "Personne"
 
-#: aleksis/core/forms.py:148
+#: aleksis/core/forms.py:156 aleksis/core/forms.py:592
 #, fuzzy
 #| msgid "Contact details"
 msgid "Additional data"
 msgstr "Détails de contact"
 
-#: aleksis/core/forms.py:188 aleksis/core/forms.py:191
-#: aleksis/core/models.py:60
+#: aleksis/core/forms.py:157 aleksis/core/models.py:206
+#: aleksis/core/models.py:514
+msgid "Photo"
+msgstr ""
+
+#: aleksis/core/forms.py:199 aleksis/core/forms.py:202
+#: aleksis/core/models.py:73
 msgid "Date"
 msgstr "Date"
 
-#: aleksis/core/forms.py:189 aleksis/core/forms.py:192
-#: aleksis/core/models.py:68
+#: aleksis/core/forms.py:200 aleksis/core/forms.py:203
+#: aleksis/core/models.py:81
 msgid "Time"
 msgstr ""
 
-#: aleksis/core/forms.py:209 aleksis/core/menus.py:249
-#: aleksis/core/models.py:398 aleksis/core/templates/core/group/list.html:8
-#: aleksis/core/templates/core/group/list.html:9
-#: aleksis/core/templates/core/person/full.html:144
-#, fuzzy
-#| msgid "Group"
-msgid "Groups"
-msgstr "Groupe"
-
-#: aleksis/core/forms.py:219
+#: aleksis/core/forms.py:233
 msgid "From when until when should the announcement be displayed?"
 msgstr ""
 
-#: aleksis/core/forms.py:222
+#: aleksis/core/forms.py:236
 msgid "Who should see the announcement?"
 msgstr ""
 
-#: aleksis/core/forms.py:223
+#: aleksis/core/forms.py:237
 msgid "Write your announcement:"
 msgstr ""
 
-#: aleksis/core/forms.py:262
+#: aleksis/core/forms.py:276
 msgid "You are not allowed to create announcements which are only valid in the past."
 msgstr ""
 
-#: aleksis/core/forms.py:266
+#: aleksis/core/forms.py:280
 msgid "The from date and time must be earlier then the until date and time."
 msgstr ""
 
-#: aleksis/core/forms.py:275
+#: aleksis/core/forms.py:289
 msgid "You need at least one recipient."
 msgstr ""
 
-#: aleksis/core/forms.py:389
-#, fuzzy
-#| msgid "Contact details"
-msgid "Account data"
-msgstr "Détails de contact"
+#: aleksis/core/forms.py:398
+msgid "Invitation code"
+msgstr ""
 
-#: aleksis/core/forms.py:391
-msgid "Consents"
+#: aleksis/core/forms.py:399
+msgid "Please enter your invitation code."
 msgstr ""
 
-#: aleksis/core/forms.py:396
-msgid "Password"
+#: aleksis/core/forms.py:418 aleksis/core/models.py:181
+msgid "First name"
+msgstr "Prénom"
+
+#: aleksis/core/forms.py:419 aleksis/core/models.py:182
+msgid "Last name"
+msgstr "Nom de famille"
+
+#: aleksis/core/forms.py:428
+msgid "A person is using this e-mail address"
 msgstr ""
 
-#: aleksis/core/forms.py:402
-msgid "Password (again)"
+#: aleksis/core/forms.py:456
+msgid "Who should get the permission?"
 msgstr ""
 
-#: aleksis/core/forms.py:411
-#, python-brace-format
-msgid "I have read the <a href='{privacy_policy}'>Privacy policy</a> and agree with them."
+#: aleksis/core/forms.py:457
+msgid "On what?"
+msgstr ""
+
+#: aleksis/core/forms.py:483
+msgid "Select objects which the permission should be granted for:"
+msgstr ""
+
+#: aleksis/core/forms.py:486
+msgid "Grant the permission for all objects"
+msgstr ""
+
+#: aleksis/core/forms.py:494
+msgid "You must select at least one group or person which should get the permission."
+msgstr ""
+
+#: aleksis/core/forms.py:499
+msgid "You must grant the permission to all objects and/or to some objects."
+msgstr ""
+
+#: aleksis/core/forms.py:586
+msgid "Adress data"
+msgstr ""
+
+#: aleksis/core/forms.py:598
+#, fuzzy
+#| msgid "Contact details"
+msgid "Account data"
+msgstr "Détails de contact"
+
+#: aleksis/core/forms.py:605
+msgid "Password"
 msgstr ""
 
-#: aleksis/core/forms.py:435
-msgid "You must type the same password each time."
+#: aleksis/core/forms.py:608
+msgid "Password (again)"
 msgstr ""
 
-#: aleksis/core/forms.py:580
+#: aleksis/core/forms.py:775
 msgid "No valid selection."
 msgstr ""
 
@@ -219,67 +275,65 @@ msgstr ""
 msgid "There are unresolved data problems."
 msgstr ""
 
-#: aleksis/core/health_checks.py:38
-msgid "The backup folder doesn't exist."
+#: aleksis/core/health_checks.py:40
+msgid "Error accessing backup storage: {}"
 msgstr ""
 
-#: aleksis/core/health_checks.py:47
-#, python-brace-format
-msgid "Last backup {time_gone_since_backup}!"
+#: aleksis/core/health_checks.py:50
+msgid "Last backup {}!"
 msgstr ""
 
-#: aleksis/core/health_checks.py:49
+#: aleksis/core/health_checks.py:52
 msgid "No backup found!"
 msgstr ""
 
-#: aleksis/core/health_checks.py:76
+#: aleksis/core/health_checks.py:79
 msgid "No backup result found!"
 msgstr ""
 
-#: aleksis/core/health_checks.py:78
-#, python-brace-format
-msgid "{task.status} - {task.result}"
-msgstr ""
-
 #: aleksis/core/menus.py:9 aleksis/core/templates/two_factor/core/login.html:6
-#: aleksis/core/templates/two_factor/core/login.html:22
-#: aleksis/core/templates/two_factor/core/login.html:76
+#: aleksis/core/templates/two_factor/core/login.html:32
+#: aleksis/core/templates/two_factor/core/login.html:95
 msgid "Login"
 msgstr ""
 
-#: aleksis/core/menus.py:15 aleksis/core/templates/account/signup.html:20
+#: aleksis/core/menus.py:15 aleksis/core/templates/account/signup.html:22
 #: aleksis/core/templates/socialaccount/signup.html:23
 msgid "Sign up"
 msgstr ""
 
-#: aleksis/core/menus.py:24
+#: aleksis/core/menus.py:24 aleksis/core/templates/invitations/enter.html:7
+msgid "Accept invitation"
+msgstr ""
+
+#: aleksis/core/menus.py:33
 msgid "Dashboard"
 msgstr ""
 
-#: aleksis/core/menus.py:32 aleksis/core/models.py:605
-#: aleksis/core/preferences.py:26
+#: aleksis/core/menus.py:41 aleksis/core/models.py:734
+#: aleksis/core/preferences.py:29
 #: aleksis/core/templates/core/notifications.html:4
 #: aleksis/core/templates/core/notifications.html:5
 msgid "Notifications"
 msgstr ""
 
-#: aleksis/core/menus.py:41
+#: aleksis/core/menus.py:53
 msgid "Account"
 msgstr ""
 
-#: aleksis/core/menus.py:48
+#: aleksis/core/menus.py:60
 msgid "Stop impersonation"
 msgstr ""
 
-#: aleksis/core/menus.py:57 aleksis/core/templates/core/base.html:62
+#: aleksis/core/menus.py:69 aleksis/core/templates/core/base.html:80
 msgid "Logout"
 msgstr ""
 
-#: aleksis/core/menus.py:63
+#: aleksis/core/menus.py:75
 msgid "2FA"
 msgstr ""
 
-#: aleksis/core/menus.py:69
+#: aleksis/core/menus.py:83
 #: aleksis/core/templates/account/password_change.html:5
 #: aleksis/core/templates/account/password_change.html:6
 #: aleksis/core/templates/account/password_change.html:19
@@ -291,87 +345,88 @@ msgstr ""
 msgid "Change password"
 msgstr ""
 
-#: aleksis/core/menus.py:81
+#: aleksis/core/menus.py:95
 msgid "Me"
 msgstr ""
 
-#: aleksis/core/menus.py:90
+#: aleksis/core/menus.py:104
 #: aleksis/core/templates/dynamic_preferences/form.html:5
 msgid "Preferences"
 msgstr ""
 
-#: aleksis/core/menus.py:99
+#: aleksis/core/menus.py:113
 msgid "Third-party accounts"
 msgstr ""
 
-#: aleksis/core/menus.py:108
+#: aleksis/core/menus.py:122
 #: aleksis/core/templates/oauth2_provider/authorized-tokens.html:5
 #: aleksis/core/templates/oauth2_provider/authorized-tokens.html:6
 msgid "Authorized applications"
 msgstr ""
 
-#: aleksis/core/menus.py:119
+#: aleksis/core/menus.py:133
 msgid "Admin"
 msgstr ""
 
-#: aleksis/core/menus.py:127 aleksis/core/models.py:704
+#: aleksis/core/menus.py:141 aleksis/core/models.py:834
 #: aleksis/core/templates/core/announcement/list.html:7
 #: aleksis/core/templates/core/announcement/list.html:8
 msgid "Announcements"
 msgstr ""
 
-#: aleksis/core/menus.py:138 aleksis/core/models.py:118
+#: aleksis/core/menus.py:152 aleksis/core/models.py:131
 #: aleksis/core/templates/core/school_term/list.html:8
 #: aleksis/core/templates/core/school_term/list.html:9
 msgid "School terms"
 msgstr ""
 
-#: aleksis/core/menus.py:149
+#: aleksis/core/menus.py:163
 #: aleksis/core/templates/core/dashboard_widget/list.html:8
 #: aleksis/core/templates/core/dashboard_widget/list.html:9
 msgid "Dashboard widgets"
 msgstr ""
 
-#: aleksis/core/menus.py:160
+#: aleksis/core/menus.py:174
 #: aleksis/core/templates/core/management/data_management.html:6
 #: aleksis/core/templates/core/management/data_management.html:7
 msgid "Data management"
 msgstr ""
 
-#: aleksis/core/menus.py:171
+#: aleksis/core/menus.py:185
 #: aleksis/core/templates/core/pages/system_status.html:5
 #: aleksis/core/templates/core/pages/system_status.html:7
 msgid "System status"
 msgstr ""
 
-#: aleksis/core/menus.py:182
-msgid "Impersonation"
-msgstr ""
-
-#: aleksis/core/menus.py:193
+#: aleksis/core/menus.py:196
 msgid "Configuration"
 msgstr ""
 
-#: aleksis/core/menus.py:204 aleksis/core/templates/core/data_check/list.html:9
+#: aleksis/core/menus.py:207 aleksis/core/templates/core/data_check/list.html:9
 #: aleksis/core/templates/core/data_check/list.html:10
 msgid "Data checks"
 msgstr ""
 
-#: aleksis/core/menus.py:210
+#: aleksis/core/menus.py:213 aleksis/core/templates/core/perms/list.html:13
+#: aleksis/core/templates/core/perms/list.html:14
+msgid "Manage permissions"
+msgstr ""
+
+#: aleksis/core/menus.py:224
 msgid "Backend Admin"
 msgstr ""
 
-#: aleksis/core/menus.py:216
-#: aleksis/core/templates/oauth2_provider/application_detail.html:5
-#: aleksis/core/templates/oauth2_provider/application_list.html:5
+#: aleksis/core/menus.py:232
+#: aleksis/core/templates/oauth2_provider/application/list.html:5
+#: aleksis/core/templates/oauth2_provider/application/list.html:6
 msgid "OAuth2 Applications"
 msgstr ""
 
-#: aleksis/core/menus.py:229
+#: aleksis/core/menus.py:245
 msgid "People"
 msgstr ""
 
-#: aleksis/core/menus.py:260 aleksis/core/models.py:958
+#: aleksis/core/menus.py:276 aleksis/core/models.py:1055
 #: aleksis/core/templates/core/group_type/list.html:8
 #: aleksis/core/templates/core/group_type/list.html:9
 #, fuzzy
@@ -379,795 +434,834 @@ msgstr ""
 msgid "Group types"
 msgstr "Groupe"
 
-#: aleksis/core/menus.py:271
+#: aleksis/core/menus.py:287
 msgid "Groups and child groups"
 msgstr ""
 
-#: aleksis/core/menus.py:282 aleksis/core/models.py:446
+#: aleksis/core/menus.py:298 aleksis/core/models.py:510
 #: aleksis/core/templates/core/additional_field/list.html:8
 #: aleksis/core/templates/core/additional_field/list.html:9
 msgid "Additional fields"
 msgstr ""
 
-#: aleksis/core/menus.py:297
+#: aleksis/core/menus.py:309
+#, fuzzy
+#| msgid "Contact details"
+msgid "Invite person"
+msgstr "Détails de contact"
+
+#: aleksis/core/menus.py:322
 #: aleksis/core/templates/core/group/child_groups.html:7
 #: aleksis/core/templates/core/group/child_groups.html:9
 msgid "Assign child groups to groups"
 msgstr ""
 
-#: aleksis/core/mixins.py:498
+#: aleksis/core/mixins.py:511
 msgid "Linked school term"
 msgstr ""
 
-#: aleksis/core/models.py:58
+#: aleksis/core/models.py:71
 msgid "Boolean (Yes/No)"
 msgstr ""
 
-#: aleksis/core/models.py:59
+#: aleksis/core/models.py:72
 msgid "Text (one line)"
 msgstr ""
 
-#: aleksis/core/models.py:61
+#: aleksis/core/models.py:74
 msgid "Date and time"
 msgstr ""
 
-#: aleksis/core/models.py:62
+#: aleksis/core/models.py:75
 msgid "Decimal number"
 msgstr ""
 
-#: aleksis/core/models.py:63 aleksis/core/models.py:186
+#: aleksis/core/models.py:76 aleksis/core/models.py:199
 msgid "E-mail address"
 msgstr ""
 
-#: aleksis/core/models.py:64
+#: aleksis/core/models.py:77
 msgid "Integer"
 msgstr ""
 
-#: aleksis/core/models.py:65
+#: aleksis/core/models.py:78
 msgid "IP address"
 msgstr ""
 
-#: aleksis/core/models.py:66
+#: aleksis/core/models.py:79
 msgid "Boolean or empty (Yes/No/Neither)"
 msgstr ""
 
-#: aleksis/core/models.py:67
+#: aleksis/core/models.py:80
 msgid "Text (multi-line)"
 msgstr ""
 
-#: aleksis/core/models.py:69
+#: aleksis/core/models.py:82
 msgid "URL / Link"
 msgstr ""
 
-#: aleksis/core/models.py:81 aleksis/core/models.py:927
+#: aleksis/core/models.py:94 aleksis/core/models.py:1024
 msgid "Name"
 msgstr ""
 
-#: aleksis/core/models.py:83
+#: aleksis/core/models.py:96
 #, fuzzy
 #| msgid "Contact details"
 msgid "Start date"
 msgstr "Détails de contact"
 
-#: aleksis/core/models.py:84
+#: aleksis/core/models.py:97
 msgid "End date"
 msgstr ""
 
-#: aleksis/core/models.py:103
+#: aleksis/core/models.py:116
 msgid "The start date must be earlier than the end date."
 msgstr ""
 
-#: aleksis/core/models.py:110
+#: aleksis/core/models.py:123
 msgid "There is already a school term for this time or a part of this time."
 msgstr ""
 
-#: aleksis/core/models.py:139 aleksis/core/models.py:876
+#: aleksis/core/models.py:152 aleksis/core/models.py:973
 msgid "Person"
 msgstr "Personne"
 
-#: aleksis/core/models.py:142
+#: aleksis/core/models.py:155
 #, fuzzy
 #| msgid "Contact details"
 msgid "Can view address"
 msgstr "Détails de contact"
 
-#: aleksis/core/models.py:143
+#: aleksis/core/models.py:156
 #, fuzzy
 #| msgid "Contact details"
 msgid "Can view contact details"
 msgstr "Détails de contact"
 
-#: aleksis/core/models.py:144
+#: aleksis/core/models.py:157
 #, fuzzy
 #| msgid "Contact details"
 msgid "Can view photo"
 msgstr "Détails de contact"
 
-#: aleksis/core/models.py:145
+#: aleksis/core/models.py:158
+#, fuzzy
+#| msgid "Contact details"
+msgid "Can view avatar image"
+msgstr "Détails de contact"
+
+#: aleksis/core/models.py:159
 #, fuzzy
 #| msgid "Contact details"
 msgid "Can view persons groups"
 msgstr "Détails de contact"
 
-#: aleksis/core/models.py:146
+#: aleksis/core/models.py:160
 #, fuzzy
 #| msgid "Contact details"
 msgid "Can view personal details"
 msgstr "Détails de contact"
 
-#: aleksis/core/models.py:156
+#: aleksis/core/models.py:170
 msgid "female"
 msgstr ""
 
-#: aleksis/core/models.py:156
+#: aleksis/core/models.py:170
 msgid "male"
 msgstr ""
 
-#: aleksis/core/models.py:164
+#: aleksis/core/models.py:178 aleksis/core/models.py:1227
 msgid "Linked user"
 msgstr ""
 
-#: aleksis/core/models.py:166
-msgid "Is person active?"
-msgstr ""
-
-#: aleksis/core/models.py:168
-msgid "First name"
-msgstr "Prénom"
-
-#: aleksis/core/models.py:169
-msgid "Last name"
-msgstr "Nom de famille"
-
-#: aleksis/core/models.py:171
+#: aleksis/core/models.py:184
 msgid "Additional name(s)"
 msgstr ""
 
-#: aleksis/core/models.py:175 aleksis/core/models.py:415
+#: aleksis/core/models.py:188 aleksis/core/models.py:479
 #, fuzzy
 #| msgid "First name"
 msgid "Short name"
 msgstr "Prénom"
 
-#: aleksis/core/models.py:178
+#: aleksis/core/models.py:191
 msgid "Street"
 msgstr ""
 
-#: aleksis/core/models.py:179
+#: aleksis/core/models.py:192
 msgid "Street number"
 msgstr ""
 
-#: aleksis/core/models.py:180
+#: aleksis/core/models.py:193
 msgid "Postal code"
 msgstr ""
 
-#: aleksis/core/models.py:181
+#: aleksis/core/models.py:194
 msgid "Place"
 msgstr ""
 
-#: aleksis/core/models.py:183
+#: aleksis/core/models.py:196 aleksis/core/templates/core/person/full.html:172
 msgid "Home phone"
 msgstr ""
 
-#: aleksis/core/models.py:184
+#: aleksis/core/models.py:197 aleksis/core/templates/core/person/full.html:182
 msgid "Mobile phone"
 msgstr ""
 
-#: aleksis/core/models.py:188
+#: aleksis/core/models.py:201
 msgid "Date of birth"
 msgstr "Date d'anniversaire"
 
-#: aleksis/core/models.py:189
+#: aleksis/core/models.py:202
+#, fuzzy
+#| msgid "Date of birth"
+msgid "Place of birth"
+msgstr "Date d'anniversaire"
+
+#: aleksis/core/models.py:203
 msgid "Sex"
 msgstr "Sexe"
 
-#: aleksis/core/models.py:191
-msgid "Photo"
+#: aleksis/core/models.py:210 aleksis/core/models.py:518
+msgid "This is an official photo, used for official documents and for internal use cases."
+msgstr ""
+
+#: aleksis/core/models.py:215 aleksis/core/models.py:522
+msgid "Display picture / Avatar"
+msgstr ""
+
+#: aleksis/core/models.py:218 aleksis/core/models.py:525
+msgid "This is a picture or an avatar for public display."
 msgstr ""
 
-#: aleksis/core/models.py:195 aleksis/core/templates/core/person/full.html:137
+#: aleksis/core/models.py:223 aleksis/core/templates/core/person/full.html:239
 msgid "Guardians / Parents"
 msgstr ""
 
-#: aleksis/core/models.py:202
+#: aleksis/core/models.py:230
 msgid "Primary group"
 msgstr ""
 
-#: aleksis/core/models.py:205 aleksis/core/models.py:563
-#: aleksis/core/models.py:587 aleksis/core/models.py:672
-#: aleksis/core/models.py:951 aleksis/core/templates/core/person/full.html:120
+#: aleksis/core/models.py:233 aleksis/core/models.py:692
+#: aleksis/core/models.py:716 aleksis/core/models.py:801
+#: aleksis/core/models.py:1048
 msgid "Description"
 msgstr "Description"
 
-#: aleksis/core/models.py:370
+#: aleksis/core/models.py:434
 msgid "Title of field"
 msgstr ""
 
-#: aleksis/core/models.py:372
+#: aleksis/core/models.py:436
 msgid "Type of field"
 msgstr ""
 
-#: aleksis/core/models.py:379
+#: aleksis/core/models.py:443
 msgid "Addtitional field for groups"
 msgstr ""
 
-#: aleksis/core/models.py:380
+#: aleksis/core/models.py:444
 msgid "Addtitional fields for groups"
 msgstr ""
 
-#: aleksis/core/models.py:397
-msgid "Group"
-msgstr "groupe"
-
-#: aleksis/core/models.py:400
+#: aleksis/core/models.py:464
 msgid "Can assign child groups to groups"
 msgstr ""
 
-#: aleksis/core/models.py:401
+#: aleksis/core/models.py:465
 #, fuzzy
 #| msgid "Contact details"
 msgid "Can view statistics about group."
 msgstr "Détails de contact"
 
-#: aleksis/core/models.py:413
+#: aleksis/core/models.py:477
 #, fuzzy
 #| msgid "Last name"
 msgid "Long name"
 msgstr "Nom de famille"
 
-#: aleksis/core/models.py:423 aleksis/core/templates/core/group/full.html:85
+#: aleksis/core/models.py:487 aleksis/core/templates/core/group/full.html:85
 msgid "Members"
 msgstr ""
 
-#: aleksis/core/models.py:426 aleksis/core/templates/core/group/full.html:82
+#: aleksis/core/models.py:490 aleksis/core/templates/core/group/full.html:82
 msgid "Owners"
 msgstr "Propriétaires"
 
-#: aleksis/core/models.py:433 aleksis/core/templates/core/group/full.html:55
+#: aleksis/core/models.py:497 aleksis/core/templates/core/group/full.html:55
 msgid "Parent groups"
 msgstr ""
 
-#: aleksis/core/models.py:441
+#: aleksis/core/models.py:505
 msgid "Type of group"
 msgstr ""
 
-#: aleksis/core/models.py:559
-msgid "User"
-msgstr ""
-
-#: aleksis/core/models.py:562 aleksis/core/models.py:586
-#: aleksis/core/models.py:671
+#: aleksis/core/models.py:691 aleksis/core/models.py:715
+#: aleksis/core/models.py:800
 #: aleksis/core/templates/core/announcement/list.html:18
 msgid "Title"
 msgstr ""
 
-#: aleksis/core/models.py:565
+#: aleksis/core/models.py:694
 msgid "Application"
 msgstr ""
 
-#: aleksis/core/models.py:571
+#: aleksis/core/models.py:700
 msgid "Activity"
 msgstr ""
 
-#: aleksis/core/models.py:572
+#: aleksis/core/models.py:701
 msgid "Activities"
 msgstr ""
 
-#: aleksis/core/models.py:578
+#: aleksis/core/models.py:707
 msgid "Sender"
 msgstr ""
 
-#: aleksis/core/models.py:583
+#: aleksis/core/models.py:712
 msgid "Recipient"
 msgstr ""
 
-#: aleksis/core/models.py:588 aleksis/core/models.py:928
+#: aleksis/core/models.py:717 aleksis/core/models.py:1025
 msgid "Link"
 msgstr ""
 
-#: aleksis/core/models.py:590
+#: aleksis/core/models.py:719
 msgid "Read"
 msgstr ""
 
-#: aleksis/core/models.py:591
+#: aleksis/core/models.py:720
 msgid "Sent"
 msgstr ""
 
-#: aleksis/core/models.py:604
+#: aleksis/core/models.py:733
 msgid "Notification"
 msgstr ""
 
-#: aleksis/core/models.py:673
+#: aleksis/core/models.py:802
 msgid "Link to detailed view"
 msgstr ""
 
-#: aleksis/core/models.py:676
+#: aleksis/core/models.py:805
 msgid "Date and time from when to show"
 msgstr ""
 
-#: aleksis/core/models.py:679
+#: aleksis/core/models.py:808
 msgid "Date and time until when to show"
 msgstr ""
 
-#: aleksis/core/models.py:703
+#: aleksis/core/models.py:833
 msgid "Announcement"
 msgstr ""
 
-#: aleksis/core/models.py:741
+#: aleksis/core/models.py:871
 msgid "Announcement recipient"
 msgstr ""
 
-#: aleksis/core/models.py:742
+#: aleksis/core/models.py:872
 msgid "Announcement recipients"
 msgstr ""
 
-#: aleksis/core/models.py:797
+#: aleksis/core/models.py:894
 msgid "Widget Title"
 msgstr ""
 
-#: aleksis/core/models.py:798
+#: aleksis/core/models.py:895
 msgid "Activate Widget"
 msgstr ""
 
-#: aleksis/core/models.py:799
+#: aleksis/core/models.py:896
 msgid "Widget is broken"
 msgstr ""
 
-#: aleksis/core/models.py:802
+#: aleksis/core/models.py:899
 msgid "Size on mobile devices"
 msgstr ""
 
-#: aleksis/core/models.py:803
+#: aleksis/core/models.py:900
 msgid "<= 600 px, 12 columns"
 msgstr ""
 
-#: aleksis/core/models.py:808
+#: aleksis/core/models.py:905
 msgid "Size on tablet devices"
 msgstr ""
 
-#: aleksis/core/models.py:809
+#: aleksis/core/models.py:906
 msgid "> 600 px, 12 columns"
 msgstr ""
 
-#: aleksis/core/models.py:814
+#: aleksis/core/models.py:911
 msgid "Size on desktop devices"
 msgstr ""
 
-#: aleksis/core/models.py:815
+#: aleksis/core/models.py:912
 msgid "> 992 px, 12 columns"
 msgstr ""
 
-#: aleksis/core/models.py:820
+#: aleksis/core/models.py:917
 msgid "Size on large desktop devices"
 msgstr ""
 
-#: aleksis/core/models.py:821
+#: aleksis/core/models.py:918
 msgid "> 1200 px>, 12 columns"
 msgstr ""
 
-#: aleksis/core/models.py:852
+#: aleksis/core/models.py:949
 msgid "Can edit default dashboard"
 msgstr ""
 
-#: aleksis/core/models.py:853
+#: aleksis/core/models.py:950
 msgid "Dashboard Widget"
 msgstr ""
 
-#: aleksis/core/models.py:854
+#: aleksis/core/models.py:951
 msgid "Dashboard Widgets"
 msgstr ""
 
-#: aleksis/core/models.py:860
+#: aleksis/core/models.py:957
 msgid "URL"
 msgstr ""
 
-#: aleksis/core/models.py:861
+#: aleksis/core/models.py:958
 msgid "Icon URL"
 msgstr ""
 
-#: aleksis/core/models.py:867
+#: aleksis/core/models.py:964
 msgid "External link widget"
 msgstr ""
 
-#: aleksis/core/models.py:868
+#: aleksis/core/models.py:965
 msgid "External link widgets"
 msgstr ""
 
-#: aleksis/core/models.py:873
+#: aleksis/core/models.py:970
 msgid "Dashboard widget"
 msgstr ""
 
-#: aleksis/core/models.py:878
+#: aleksis/core/models.py:975
 msgid "Order"
 msgstr ""
 
-#: aleksis/core/models.py:879
+#: aleksis/core/models.py:976
 msgid "Part of the default dashboard"
 msgstr ""
 
-#: aleksis/core/models.py:894
+#: aleksis/core/models.py:991
 msgid "Dashboard widget order"
 msgstr ""
 
-#: aleksis/core/models.py:895
+#: aleksis/core/models.py:992
 msgid "Dashboard widget orders"
 msgstr ""
 
-#: aleksis/core/models.py:901
+#: aleksis/core/models.py:998
 msgid "Menu ID"
 msgstr ""
 
-#: aleksis/core/models.py:914
+#: aleksis/core/models.py:1011
 msgid "Custom menu"
 msgstr ""
 
-#: aleksis/core/models.py:915
+#: aleksis/core/models.py:1012
 msgid "Custom menus"
 msgstr ""
 
-#: aleksis/core/models.py:925
+#: aleksis/core/models.py:1022
 msgid "Menu"
 msgstr ""
 
-#: aleksis/core/models.py:929
+#: aleksis/core/models.py:1026 aleksis/core/models.py:1274
+#: aleksis/core/templates/oauth2_provider/application/detail.html:26
 msgid "Icon"
 msgstr ""
 
-#: aleksis/core/models.py:935
+#: aleksis/core/models.py:1032
 msgid "Custom menu item"
 msgstr ""
 
-#: aleksis/core/models.py:936
+#: aleksis/core/models.py:1033
 msgid "Custom menu items"
 msgstr ""
 
-#: aleksis/core/models.py:950
+#: aleksis/core/models.py:1047
 msgid "Title of type"
 msgstr ""
 
-#: aleksis/core/models.py:957 aleksis/core/templates/core/group/full.html:47
+#: aleksis/core/models.py:1054 aleksis/core/templates/core/group/full.html:47
 #, fuzzy
 #| msgid "Group"
 msgid "Group type"
 msgstr "Groupe"
 
-#: aleksis/core/models.py:971
+#: aleksis/core/models.py:1068
 #, fuzzy
 #| msgid "Contact details"
 msgid "Can view system status"
 msgstr "Détails de contact"
 
-#: aleksis/core/models.py:972
+#: aleksis/core/models.py:1069
 msgid "Can manage data"
 msgstr ""
 
-#: aleksis/core/models.py:973
+#: aleksis/core/models.py:1070
 #, fuzzy
 #| msgid "Contact details"
 msgid "Can impersonate"
 msgstr "Détails de contact"
 
-#: aleksis/core/models.py:974
+#: aleksis/core/models.py:1071
 msgid "Can use search"
 msgstr ""
 
-#: aleksis/core/models.py:975
+#: aleksis/core/models.py:1072
 msgid "Can change site preferences"
 msgstr ""
 
-#: aleksis/core/models.py:976
+#: aleksis/core/models.py:1073
 msgid "Can change person preferences"
 msgstr ""
 
-#: aleksis/core/models.py:977
+#: aleksis/core/models.py:1074
 msgid "Can change group preferences"
 msgstr ""
 
-#: aleksis/core/models.py:978
-msgid "Can add oauth applications"
-msgstr ""
-
-#: aleksis/core/models.py:979
-msgid "Can list oauth applications"
+#: aleksis/core/models.py:1075
+msgid "Can test PDF generation"
 msgstr ""
 
-#: aleksis/core/models.py:980
+#: aleksis/core/models.py:1076
 #, fuzzy
 #| msgid "Contact details"
-msgid "Can view oauth applications"
+msgid "Can invite persons"
 msgstr "Détails de contact"
 
-#: aleksis/core/models.py:981
-msgid "Can update oauth applications"
-msgstr ""
-
-#: aleksis/core/models.py:982
-msgid "Can delete oauth applications"
-msgstr ""
-
-#: aleksis/core/models.py:983
-msgid "Can test PDF generation"
-msgstr ""
-
-#: aleksis/core/models.py:1019
+#: aleksis/core/models.py:1112
 msgid "Related data check task"
 msgstr ""
 
-#: aleksis/core/models.py:1027
+#: aleksis/core/models.py:1120
 msgid "Issue solved"
 msgstr ""
 
-#: aleksis/core/models.py:1028
+#: aleksis/core/models.py:1121
 msgid "Notification sent"
 msgstr ""
 
-#: aleksis/core/models.py:1041
+#: aleksis/core/models.py:1134
 msgid "Data check result"
 msgstr ""
 
-#: aleksis/core/models.py:1042
+#: aleksis/core/models.py:1135
 msgid "Data check results"
 msgstr ""
 
-#: aleksis/core/models.py:1044
+#: aleksis/core/models.py:1137
 msgid "Can run data checks"
 msgstr ""
 
-#: aleksis/core/models.py:1045
+#: aleksis/core/models.py:1138
 msgid "Can solve data check problems"
 msgstr ""
 
-#: aleksis/core/models.py:1060
+#: aleksis/core/models.py:1145
+#, fuzzy
+#| msgid "Contact details"
+msgid "E-Mail address"
+msgstr "Détails de contact"
+
+#: aleksis/core/models.py:1177
 #, fuzzy
 #| msgid "Owners"
 msgid "Owner"
 msgstr "Propriétaires"
 
-#: aleksis/core/models.py:1064
+#: aleksis/core/models.py:1181
 msgid "File expires at"
 msgstr ""
 
-#: aleksis/core/models.py:1066
+#: aleksis/core/models.py:1183
 msgid "Generated HTML file"
 msgstr ""
 
-#: aleksis/core/models.py:1068
+#: aleksis/core/models.py:1185
 msgid "Generated PDF file"
 msgstr ""
 
-#: aleksis/core/models.py:1075
+#: aleksis/core/models.py:1192
 msgid "PDF file"
 msgstr ""
 
-#: aleksis/core/models.py:1076
+#: aleksis/core/models.py:1193
 msgid "PDF files"
 msgstr ""
 
-#: aleksis/core/models.py:1081
+#: aleksis/core/models.py:1198
 msgid "Task result"
 msgstr ""
 
-#: aleksis/core/models.py:1084
+#: aleksis/core/models.py:1201
 msgid "Task user"
 msgstr ""
 
-#: aleksis/core/models.py:1096
+#: aleksis/core/models.py:1213
 msgid "Task user assignment"
 msgstr ""
 
-#: aleksis/core/models.py:1097
+#: aleksis/core/models.py:1214
 msgid "Task user assignments"
 msgstr ""
 
-#: aleksis/core/preferences.py:22
+#: aleksis/core/models.py:1230
+#, fuzzy
+#| msgid "Contact details"
+msgid "Additional attributes"
+msgstr "Détails de contact"
+
+#: aleksis/core/models.py:1268
+msgid "Allowed scopes that clients can request"
+msgstr ""
+
+#: aleksis/core/models.py:1278
+msgid "This image will be shown as icon in the authorization flow. It should be squared."
+msgstr ""
+
+#: aleksis/core/preferences.py:25
 msgid "General"
 msgstr ""
 
-#: aleksis/core/preferences.py:23
+#: aleksis/core/preferences.py:26
 msgid "School"
 msgstr ""
 
-#: aleksis/core/preferences.py:24
+#: aleksis/core/preferences.py:27
 msgid "Theme"
 msgstr ""
 
-#: aleksis/core/preferences.py:25
+#: aleksis/core/preferences.py:28
 msgid "Mail"
 msgstr ""
 
-#: aleksis/core/preferences.py:27
+#: aleksis/core/preferences.py:30
 msgid "Footer"
 msgstr ""
 
-#: aleksis/core/preferences.py:28
+#: aleksis/core/preferences.py:31
 #, fuzzy
 #| msgid "Contact details"
 msgid "Accounts"
 msgstr "Détails de contact"
 
-#: aleksis/core/preferences.py:29
+#: aleksis/core/preferences.py:32
 msgid "Authentication"
 msgstr ""
 
-#: aleksis/core/preferences.py:30
+#: aleksis/core/preferences.py:33
 msgid "Internationalisation"
 msgstr ""
 
-#: aleksis/core/preferences.py:41
+#: aleksis/core/preferences.py:43
 msgid "Site title"
 msgstr ""
 
-#: aleksis/core/preferences.py:52
+#: aleksis/core/preferences.py:55
 #, fuzzy
 #| msgid "Description"
 msgid "Site description"
 msgstr "Description"
 
-#: aleksis/core/preferences.py:63
+#: aleksis/core/preferences.py:65
 msgid "Primary colour"
 msgstr ""
 
-#: aleksis/core/preferences.py:75
+#: aleksis/core/preferences.py:77
 msgid "Secondary colour"
 msgstr ""
 
-#: aleksis/core/preferences.py:86
+#: aleksis/core/preferences.py:89
 msgid "Logo"
 msgstr ""
 
-#: aleksis/core/preferences.py:96
+#: aleksis/core/preferences.py:100
 msgid "Favicon"
 msgstr ""
 
-#: aleksis/core/preferences.py:106
+#: aleksis/core/preferences.py:111
 msgid "PWA-Icon"
 msgstr ""
 
-#: aleksis/core/preferences.py:117
+#: aleksis/core/preferences.py:121
+msgid "PWA-Icon is maskable"
+msgstr ""
+
+#: aleksis/core/preferences.py:133
 #, fuzzy
 #| msgid "Last name"
 msgid "Mail out name"
 msgstr "Nom de famille"
 
-#: aleksis/core/preferences.py:128
+#: aleksis/core/preferences.py:144
 msgid "Mail out address"
 msgstr ""
 
-#: aleksis/core/preferences.py:140
+#: aleksis/core/preferences.py:157
 msgid "Link to privacy policy"
 msgstr ""
 
-#: aleksis/core/preferences.py:152
+#: aleksis/core/preferences.py:169
 msgid "Link to imprint"
 msgstr ""
 
-#: aleksis/core/preferences.py:164
+#: aleksis/core/preferences.py:180
 msgid "Name format for addressing"
 msgstr ""
 
-#: aleksis/core/preferences.py:180
+#: aleksis/core/preferences.py:197
 msgid "Channels to use for notifications"
 msgstr ""
 
-#: aleksis/core/preferences.py:192
+#: aleksis/core/preferences.py:209
 msgid "Regular expression to match primary group, e.g. '^Class .*'"
 msgstr ""
 
-#: aleksis/core/preferences.py:203
+#: aleksis/core/preferences.py:220
 msgid "Field on person to match primary group against"
 msgstr ""
 
-#: aleksis/core/preferences.py:215
+#: aleksis/core/preferences.py:232
 msgid "Automatically create new persons for new users"
 msgstr ""
 
-#: aleksis/core/preferences.py:224
+#: aleksis/core/preferences.py:241
 msgid "Automatically link existing persons to new users by their e-mail address"
 msgstr ""
 
-#: aleksis/core/preferences.py:235
+#: aleksis/core/preferences.py:252
 msgid "Display name of the school"
 msgstr ""
 
-#: aleksis/core/preferences.py:246
+#: aleksis/core/preferences.py:263
 msgid "Official name of the school, e.g. as given by supervisory authority"
 msgstr ""
 
-#: aleksis/core/preferences.py:254
+#: aleksis/core/preferences.py:271
 msgid "Allow users to change their passwords"
 msgstr ""
 
-#: aleksis/core/preferences.py:262
+#: aleksis/core/preferences.py:279
 msgid "Enable signup"
 msgstr ""
 
-#: aleksis/core/preferences.py:273
+#: aleksis/core/preferences.py:287
+msgid "Enable invitations"
+msgstr ""
+
+#: aleksis/core/preferences.py:295
+msgid "Length of invite code. (Default 3: abcde-acbde-abcde)"
+msgstr ""
+
+#: aleksis/core/preferences.py:303
+msgid "Size of packets. (Default 5: abcde)"
+msgstr ""
+
+#: aleksis/core/preferences.py:314
+#, fuzzy
+#| msgid "Contact details"
+msgid "Allowed Grant Flows for OAuth applications"
+msgstr "Détails de contact"
+
+#: aleksis/core/preferences.py:328
 msgid "Available languages"
 msgstr ""
 
-#: aleksis/core/preferences.py:285
+#: aleksis/core/preferences.py:341
 msgid "Send emails if data checks detect problems"
 msgstr ""
 
-#: aleksis/core/preferences.py:296
+#: aleksis/core/preferences.py:352
 msgid "Email recipients for data checks problem emails"
 msgstr ""
 
-#: aleksis/core/preferences.py:307
+#: aleksis/core/preferences.py:363
 msgid "Email recipient groups for data checks problem emails"
 msgstr ""
 
-#: aleksis/core/preferences.py:316
+#: aleksis/core/preferences.py:372
 msgid "Show dashboard to users without login"
 msgstr ""
 
-#: aleksis/core/preferences.py:325
+#: aleksis/core/preferences.py:381
 msgid "Allow users to edit their dashboard"
 msgstr ""
 
-#: aleksis/core/preferences.py:336
+#: aleksis/core/preferences.py:392
 msgid "Fields on person model which are editable by themselves."
 msgstr ""
 
-#: aleksis/core/preferences.py:350
+#: aleksis/core/preferences.py:407
 msgid "Editable fields on person model which should trigger a notification on change"
 msgstr ""
 
-#: aleksis/core/preferences.py:363
+#: aleksis/core/preferences.py:421
 msgid "Contact for notification if a person changes their data"
 msgstr ""
 
-#: aleksis/core/preferences.py:373
+#: aleksis/core/preferences.py:432
 msgid "PDF file expiration duration"
 msgstr ""
 
-#: aleksis/core/preferences.py:374
+#: aleksis/core/preferences.py:433
 msgid "in minutes"
 msgstr ""
 
-#: aleksis/core/preferences.py:384
+#: aleksis/core/preferences.py:443
 msgid "Automatically update the dashboard and its widgets"
 msgstr ""
 
-#: aleksis/core/preferences.py:394
+#: aleksis/core/preferences.py:453
 msgid "Automatically update the dashboard and its widgets sitewide"
 msgstr ""
 
-#: aleksis/core/settings.py:452
+#: aleksis/core/preferences.py:463
+msgid "Country for phone number parsing"
+msgstr ""
+
+#: aleksis/core/settings.py:529
 msgid "English"
 msgstr ""
 
-#: aleksis/core/settings.py:453
+#: aleksis/core/settings.py:530
 msgid "German"
 msgstr ""
 
-#: aleksis/core/tables.py:19
+#: aleksis/core/tables.py:24
 #: aleksis/core/templates/core/announcement/list.html:36
 #: aleksis/core/templates/core/group/full.html:24
-#: aleksis/core/templates/core/person/full.html:23
-#: aleksis/core/templates/oauth2_provider/application_detail.html:17
+#: aleksis/core/templates/core/person/full.html:26
+#: aleksis/core/templates/core/person/full.html:98
+#: aleksis/core/templates/oauth2_provider/application/detail.html:17
 msgid "Edit"
 msgstr ""
 
-#: aleksis/core/tables.py:21 aleksis/core/tables.py:89
+#: aleksis/core/tables.py:26 aleksis/core/tables.py:94
+#: aleksis/core/tables.py:138
 #: aleksis/core/templates/core/announcement/list.html:22
 msgid "Actions"
 msgstr ""
 
-#: aleksis/core/tables.py:56 aleksis/core/tables.py:57
-#: aleksis/core/tables.py:71 aleksis/core/tables.py:87
+#: aleksis/core/tables.py:61 aleksis/core/tables.py:62
+#: aleksis/core/tables.py:76 aleksis/core/tables.py:92
+#: aleksis/core/tables.py:136
 #: aleksis/core/templates/core/announcement/list.html:42
 #: aleksis/core/templates/core/group/full.html:31
 #: aleksis/core/templates/core/pages/delete.html:22
-#: aleksis/core/templates/core/person/full.html:30
-#: aleksis/core/templates/oauth2_provider/application_confirm_delete.html:20
-#: aleksis/core/templates/oauth2_provider/application_detail.html:21
+#: aleksis/core/templates/core/person/full.html:33
+#: aleksis/core/templates/core/person/full.html:105
+#: aleksis/core/templates/oauth2_provider/application/detail.html:21
 msgid "Delete"
 msgstr ""
 
-#: aleksis/core/templates/403.html:14 aleksis/core/templates/404.html:10
-#: aleksis/core/templates/500.html:10
-#: aleksis/core/templates/oauth2_provider/authorize.html:47
+#: aleksis/core/templates/403.html:14 aleksis/core/templates/500.html:10
+#: aleksis/core/templates/oauth2_provider/authorize.html:54
 #: aleksis/core/templates/oauth2_provider/authorized-oob.html:24
 msgid "Error"
 msgstr ""
@@ -1178,7 +1272,7 @@ msgid ""
 "          object."
 msgstr ""
 
-#: aleksis/core/templates/403.html:19 aleksis/core/templates/404.html:17
+#: aleksis/core/templates/403.html:19 aleksis/core/templates/404.html:16
 msgid ""
 "\n"
 "            If you think this is an error in AlekSIS, please contact your site\n"
@@ -1186,13 +1280,7 @@ msgid ""
 "          "
 msgstr ""
 
-#: aleksis/core/templates/404.html:10
-msgid ""
-"The requested page or object was not\n"
-"          found."
-msgstr ""
-
-#: aleksis/core/templates/404.html:13
+#: aleksis/core/templates/404.html:12
 msgid ""
 "\n"
 "            If you were redirected by a link on an external page,\n"
@@ -1232,11 +1320,11 @@ msgstr ""
 msgid "Account inactive"
 msgstr ""
 
-#: aleksis/core/templates/account/account_inactive.html:13
+#: aleksis/core/templates/account/account_inactive.html:14
 msgid "Account inactive."
 msgstr ""
 
-#: aleksis/core/templates/account/account_inactive.html:15
+#: aleksis/core/templates/account/account_inactive.html:17
 msgid ""
 "\n"
 "            This account is currently inactive. If you think this is an\n"
@@ -1249,14 +1337,13 @@ msgid "Hello!"
 msgstr ""
 
 #: aleksis/core/templates/account/email/base_message.txt:9
-#: aleksis/core/templates/templated_email/notification.email:22
-#: aleksis/core/templates/templated_email/notification.email:46
 msgid "Your AlekSIS team"
 msgstr ""
 
 #: aleksis/core/templates/account/email_confirm.html:5
 #: aleksis/core/templates/account/email_confirm.html:6
 #: aleksis/core/templates/account/email_confirm.html:17
+#: aleksis/core/templates/socialaccount/login.html:17
 msgid "Confirm"
 msgstr ""
 
@@ -1301,7 +1388,7 @@ msgstr ""
 #: aleksis/core/templates/account/password_reset_done.html:5
 #: aleksis/core/templates/account/verification_email_required.html:5
 #: aleksis/core/templates/account/verification_email_required.html:6
-#: aleksis/core/templates/two_factor/core/login.html:81
+#: aleksis/core/templates/two_factor/core/login.html:100
 msgid "Reset password"
 msgstr ""
 
@@ -1388,11 +1475,11 @@ msgstr ""
 msgid "Signup closed"
 msgstr ""
 
-#: aleksis/core/templates/account/signup_closed.html:13
+#: aleksis/core/templates/account/signup_closed.html:14
 msgid "Signup closed."
 msgstr ""
 
-#: aleksis/core/templates/account/signup_closed.html:15
+#: aleksis/core/templates/account/signup_closed.html:17
 msgid ""
 "\n"
 "            This sign up is currently closed. If you think this is an\n"
@@ -1430,11 +1517,6 @@ msgid ""
 "          "
 msgstr ""
 
-#: aleksis/core/templates/account/verification_sent.html:30
-#, python-format
-msgid "<strong>Note:</strong> you can still <a href=\"%(email_url)s\">change your e-mail address</a>"
-msgstr ""
-
 #: aleksis/core/templates/core/additional_field/edit.html:6
 #: aleksis/core/templates/core/additional_field/edit.html:7
 msgid "Edit additional field"
@@ -1478,24 +1560,24 @@ msgstr ""
 msgid "There are no announcements."
 msgstr ""
 
-#: aleksis/core/templates/core/base.html:60
+#: aleksis/core/templates/core/base.html:78
 msgid "Logged in as"
 msgstr ""
 
-#: aleksis/core/templates/core/base.html:154
-msgid "About AlekSIS — The Free School Information System"
+#: aleksis/core/templates/core/base.html:179
+msgid "About AlekSIS® — The Free School Information System"
 msgstr ""
 
-#: aleksis/core/templates/core/base.html:162
-msgid "Impress"
+#: aleksis/core/templates/core/base.html:187
+msgid "Imprint"
 msgstr ""
 
-#: aleksis/core/templates/core/base.html:170
+#: aleksis/core/templates/core/base.html:195
 msgid "Privacy Policy"
 msgstr ""
 
-#: aleksis/core/templates/core/base_print.html:72
-msgid "Powered by AlekSIS"
+#: aleksis/core/templates/core/base_print.html:74
+msgid "Powered by AlekSIS®"
 msgstr ""
 
 #: aleksis/core/templates/core/dashboard_widget/create.html:8
@@ -1586,15 +1668,15 @@ msgstr "Détails de contact"
 msgid "Options to solve the problem"
 msgstr ""
 
-#: aleksis/core/templates/core/data_check/list.html:62
+#: aleksis/core/templates/core/data_check/list.html:63
 msgid "Show object"
 msgstr ""
 
-#: aleksis/core/templates/core/data_check/list.html:84
+#: aleksis/core/templates/core/data_check/list.html:86
 msgid "Registered checks"
 msgstr ""
 
-#: aleksis/core/templates/core/data_check/list.html:88
+#: aleksis/core/templates/core/data_check/list.html:90
 msgid ""
 "\n"
 "            The system will check for the following problems:\n"
@@ -1687,7 +1769,7 @@ msgstr ""
 
 #: aleksis/core/templates/core/group/child_groups.html:93
 #: aleksis/core/templates/core/group/child_groups.html:128
-#: aleksis/core/templates/oauth2_provider/application_detail.html:9
+#: aleksis/core/templates/oauth2_provider/application/detail.html:9
 #: aleksis/core/templates/two_factor/_wizard_actions.html:15
 #: aleksis/core/templates/two_factor/_wizard_actions.html:20
 msgid "Back"
@@ -1716,7 +1798,8 @@ msgid "Edit group"
 msgstr ""
 
 #: aleksis/core/templates/core/group/full.html:38
-#: aleksis/core/templates/core/person/full.html:37
+#: aleksis/core/templates/core/person/full.html:40
+#: aleksis/core/templates/core/person/full.html:112
 msgid "Change preferences"
 msgstr ""
 
@@ -1809,36 +1892,46 @@ msgid "No notifications available yet."
 msgstr ""
 
 #: aleksis/core/templates/core/pages/about.html:6
-#: aleksis/core/templates/core/pages/about.html:15
-msgid "About AlekSIS"
+msgid "About AlekSIS®"
 msgstr ""
 
 #: aleksis/core/templates/core/pages/about.html:7
-msgid "AlekSIS – The Free School Information System"
+msgid "AlekSIS® – The Free School Information System"
+msgstr ""
+
+#: aleksis/core/templates/core/pages/about.html:15
+msgid "About AlekSIS"
 msgstr ""
 
 #: aleksis/core/templates/core/pages/about.html:17
 msgid ""
 "\n"
-"              This platform is powered by AlekSIS, a web-based school information system (SIS) which can be used\n"
+"              This platform is powered by AlekSIS®, a web-based school information system (SIS) which can be used\n"
 "              to manage and/or publish organisational artifacts of educational institutions. AlekSIS is free software and\n"
 "              can be used by anyone.\n"
 "            "
 msgstr ""
 
-#: aleksis/core/templates/core/pages/about.html:25
+#: aleksis/core/templates/core/pages/about.html:24
+msgid ""
+"\n"
+"              AlekSIS® is a registered trademark of the AlekSIS open source project, represented by Teckids e.V.\n"
+"            "
+msgstr ""
+
+#: aleksis/core/templates/core/pages/about.html:30
 msgid "Website of AlekSIS"
 msgstr ""
 
-#: aleksis/core/templates/core/pages/about.html:26
+#: aleksis/core/templates/core/pages/about.html:31
 msgid "Source code"
 msgstr ""
 
-#: aleksis/core/templates/core/pages/about.html:35
+#: aleksis/core/templates/core/pages/about.html:40
 msgid "Licence information"
 msgstr ""
 
-#: aleksis/core/templates/core/pages/about.html:37
+#: aleksis/core/templates/core/pages/about.html:42
 msgid ""
 "\n"
 "              The core and the official apps of AlekSIS are licenced under the EUPL, version 1.2 or later. For licence\n"
@@ -1847,23 +1940,23 @@ msgid ""
 "            "
 msgstr ""
 
-#: aleksis/core/templates/core/pages/about.html:45
+#: aleksis/core/templates/core/pages/about.html:50
 msgid "Free/Open Source Licence"
 msgstr ""
 
-#: aleksis/core/templates/core/pages/about.html:46
+#: aleksis/core/templates/core/pages/about.html:51
 msgid "Other Licence"
 msgstr ""
 
-#: aleksis/core/templates/core/pages/about.html:50
+#: aleksis/core/templates/core/pages/about.html:55
 msgid "Full licence text"
 msgstr ""
 
-#: aleksis/core/templates/core/pages/about.html:51
+#: aleksis/core/templates/core/pages/about.html:56
 msgid "More information about the EUPL"
 msgstr ""
 
-#: aleksis/core/templates/core/pages/about.html:90
+#: aleksis/core/templates/core/pages/about.html:95
 #, python-format
 msgid ""
 "\n"
@@ -1967,6 +2060,8 @@ msgid "Celery task results"
 msgstr ""
 
 #: aleksis/core/templates/core/pages/system_status.html:112
+#: aleksis/core/templates/templated_email/celery_failure.email:9
+#: aleksis/core/templates/templated_email/celery_failure.email:28
 msgid "Task"
 msgstr ""
 
@@ -2050,6 +2145,53 @@ msgid ""
 "          "
 msgstr ""
 
+#: aleksis/core/templates/core/perms/assign.html:12
+#: aleksis/core/templates/core/perms/assign.html:13
+msgid "Assign permission"
+msgstr ""
+
+#: aleksis/core/templates/core/perms/assign.html:17
+#, fuzzy
+#| msgid "Contact details"
+msgid "Selected permission"
+msgstr "Détails de contact"
+
+#: aleksis/core/templates/core/perms/assign.html:26
+msgid "Assign"
+msgstr ""
+
+#: aleksis/core/templates/core/perms/list.html:21
+msgid "Assign a new permission"
+msgstr ""
+
+#: aleksis/core/templates/core/perms/list.html:25
+msgid "Select"
+msgstr ""
+
+#: aleksis/core/templates/core/perms/list.html:34
+msgid "Global (user)"
+msgstr ""
+
+#: aleksis/core/templates/core/perms/list.html:38
+msgid "Global (group)"
+msgstr ""
+
+#: aleksis/core/templates/core/perms/list.html:42
+msgid "Object (user)"
+msgstr ""
+
+#: aleksis/core/templates/core/perms/list.html:46
+msgid "Object (group)"
+msgstr ""
+
+#: aleksis/core/templates/core/perms/list.html:52
+msgid "Filter permissions"
+msgstr ""
+
+#: aleksis/core/templates/core/perms/list.html:58
+msgid "Update"
+msgstr ""
+
 #: aleksis/core/templates/core/person/create.html:12
 #: aleksis/core/templates/core/person/create.html:13
 #: aleksis/core/templates/core/person/list.html:17
@@ -2063,19 +2205,27 @@ msgstr "Détails de contact"
 msgid "Edit person"
 msgstr ""
 
-#: aleksis/core/templates/core/person/full.html:44
-#: aleksis/core/templates/impersonate/list_users.html:7
-#: aleksis/core/templates/impersonate/list_users.html:8
+#: aleksis/core/templates/core/person/full.html:47
+#: aleksis/core/templates/core/person/full.html:119
 #, fuzzy
 #| msgid "Contact details"
 msgid "Impersonate"
 msgstr "Détails de contact"
 
-#: aleksis/core/templates/core/person/full.html:50
+#: aleksis/core/templates/core/person/full.html:54
+#: aleksis/core/templates/core/person/full.html:126
+msgid "Invite user"
+msgstr ""
+
+#: aleksis/core/templates/core/person/full.html:133
 msgid "Contact details"
 msgstr "Détails de contact"
 
-#: aleksis/core/templates/core/person/full.html:130
+#: aleksis/core/templates/core/person/full.html:224
+msgid "This person didn't upload a personal photo."
+msgstr ""
+
+#: aleksis/core/templates/core/person/full.html:232
 msgid "Children"
 msgstr ""
 
@@ -2115,77 +2265,147 @@ msgstr ""
 msgid "Save preferences"
 msgstr ""
 
-#: aleksis/core/templates/oauth2_provider/application_confirm_delete.html:5
-#: aleksis/core/templates/oauth2_provider/application_confirm_delete.html:6
-msgid "Delete application"
+#: aleksis/core/templates/invitations/disabled.html:5
+msgid "The invite feature is disabled"
+msgstr ""
+
+#: aleksis/core/templates/invitations/disabled.html:13
+msgid "The invite feature is disabled."
+msgstr ""
+
+#: aleksis/core/templates/invitations/disabled.html:15
+msgid "To enable it, switch on the corresponding checkbox in the authentication section of the "
+msgstr ""
+
+#: aleksis/core/templates/invitations/disabled.html:16
+msgid "site preferences page"
+msgstr ""
+
+#: aleksis/core/templates/invitations/enter.html:21
+msgid "Accept your invitation"
+msgstr ""
+
+#: aleksis/core/templates/invitations/enter.html:25
+msgid ""
+"\n"
+"                Please enter your invitation code to register\n"
+"                your new user account:\n"
+"              "
+msgstr ""
+
+#: aleksis/core/templates/invitations/enter.html:37
+msgid "Accept invite"
+msgstr ""
+
+#: aleksis/core/templates/invitations/forms/_invite.html:9
+#: aleksis/core/templates/invitations/forms/_invite.html:10
+#: aleksis/core/templates/invitations/forms/_invite.html:21
+msgid "Invite"
+msgstr ""
+
+#: aleksis/core/templates/invitations/forms/_invite.html:17
+#, fuzzy
+#| msgid "Contact details"
+msgid "Invite by email address"
+msgstr "Détails de contact"
+
+#: aleksis/core/templates/invitations/forms/_invite.html:26
+msgid "Generate invitation code"
+msgstr ""
+
+#: aleksis/core/templates/invitations/forms/_invite.html:29
+msgid "Generate code"
+msgstr ""
+
+#: aleksis/core/templates/invitations/forms/_invite.html:33
+msgid "Invitations"
 msgstr ""
 
-#: aleksis/core/templates/oauth2_provider/application_confirm_delete.html:12
+#: aleksis/core/templates/invitations/messages/invite_accepted.txt:3
 #, python-format
-msgid "Are you sure to delete the application %(application_name)s?"
+msgid "The invitation for %(email)s has been accepted."
 msgstr ""
 
-#: aleksis/core/templates/oauth2_provider/application_confirm_delete.html:24
-#: aleksis/core/templates/oauth2_provider/application_form.html:18
+#: aleksis/core/templates/oauth2_provider/application/create.html:5
+#: aleksis/core/templates/oauth2_provider/application/create.html:6
+#, fuzzy
+#| msgid "Contact details"
+msgid "Register OAuth2 Application"
+msgstr "Détails de contact"
+
+#: aleksis/core/templates/oauth2_provider/application/create.html:14
+#: aleksis/core/templates/oauth2_provider/application/edit.html:14
 #: aleksis/core/templates/oauth2_provider/authorized-token-delete.html:24
 #: aleksis/core/templates/two_factor/_wizard_actions.html:6
 msgid "Cancel"
 msgstr ""
 
-#: aleksis/core/templates/oauth2_provider/application_detail.html:27
+#: aleksis/core/templates/oauth2_provider/application/detail.html:5
+#, fuzzy
+#| msgid "Contact details"
+msgid "OAuth2 Application"
+msgstr "Détails de contact"
+
+#: aleksis/core/templates/oauth2_provider/application/detail.html:39
 msgid "Client id"
 msgstr ""
 
-#: aleksis/core/templates/oauth2_provider/application_detail.html:35
+#: aleksis/core/templates/oauth2_provider/application/detail.html:47
 msgid "Client secret"
 msgstr ""
 
-#: aleksis/core/templates/oauth2_provider/application_detail.html:43
+#: aleksis/core/templates/oauth2_provider/application/detail.html:55
 msgid "Client type"
 msgstr ""
 
-#: aleksis/core/templates/oauth2_provider/application_detail.html:51
-msgid "Authorization Grant Type"
+#: aleksis/core/templates/oauth2_provider/application/detail.html:63
+msgid "Allowed scopes"
 msgstr ""
 
-#: aleksis/core/templates/oauth2_provider/application_detail.html:59
+#: aleksis/core/templates/oauth2_provider/application/detail.html:71
 msgid "Redirect URIs"
 msgstr ""
 
-#: aleksis/core/templates/oauth2_provider/application_form.html:5
-msgid "Create OAuth2 Application"
-msgstr ""
-
-#: aleksis/core/templates/oauth2_provider/application_form.html:10
-msgid "Edit application"
+#: aleksis/core/templates/oauth2_provider/application/detail.html:79
+msgid "Skip Authorisation"
 msgstr ""
 
-#: aleksis/core/templates/oauth2_provider/application_list.html:8
-msgid "OAuth2 applications"
-msgstr ""
+#: aleksis/core/templates/oauth2_provider/application/edit.html:5
+#: aleksis/core/templates/oauth2_provider/application/edit.html:6
+#, fuzzy
+#| msgid "Contact details"
+msgid "Edit OAuth2 Application"
+msgstr "Détails de contact"
 
-#: aleksis/core/templates/oauth2_provider/application_list.html:12
-msgid "Register new applications"
+#: aleksis/core/templates/oauth2_provider/application/list.html:11
+msgid "Register new application"
 msgstr ""
 
-#: aleksis/core/templates/oauth2_provider/application_list.html:23
+#: aleksis/core/templates/oauth2_provider/application/list.html:25
 msgid "No applications defined."
 msgstr ""
 
 #: aleksis/core/templates/oauth2_provider/authorize.html:5
-#: aleksis/core/templates/oauth2_provider/authorize.html:16
+#: aleksis/core/templates/socialaccount/login.html:5
+#: aleksis/core/templates/socialaccount/login.html:6
 msgid "Authorize"
 msgstr ""
 
-#: aleksis/core/templates/oauth2_provider/authorize.html:18
+#: aleksis/core/templates/oauth2_provider/authorize.html:23
+#, fuzzy, python-format
+#| msgid "Contact details"
+msgid "Authorize %(name)s"
+msgstr "Détails de contact"
+
+#: aleksis/core/templates/oauth2_provider/authorize.html:25
 msgid "The application requests access to the following scopes:"
 msgstr ""
 
-#: aleksis/core/templates/oauth2_provider/authorize.html:33
+#: aleksis/core/templates/oauth2_provider/authorize.html:40
 msgid "Allow"
 msgstr ""
 
-#: aleksis/core/templates/oauth2_provider/authorize.html:36
+#: aleksis/core/templates/oauth2_provider/authorize.html:43
 msgid "Disallow"
 msgstr ""
 
@@ -2290,6 +2510,20 @@ msgstr ""
 msgid "Add a Third-party Account"
 msgstr ""
 
+#: aleksis/core/templates/socialaccount/login.html:12
+#, python-format
+msgid "You are about to connect a new third party account from %(provider)s."
+msgstr ""
+
+#: aleksis/core/templates/socialaccount/login.html:23
+#, python-format
+msgid "You are about to sign in using a third party account from %(provider)s."
+msgstr ""
+
+#: aleksis/core/templates/socialaccount/login.html:28
+msgid "Continue"
+msgstr ""
+
 #: aleksis/core/templates/socialaccount/login_cancelled.html:5
 #: aleksis/core/templates/socialaccount/login_cancelled.html:6
 #: aleksis/core/templates/socialaccount/login_cancelled.html:13
@@ -2334,26 +2568,64 @@ msgid ""
 "        "
 msgstr ""
 
-#: aleksis/core/templates/templated_email/data_checks.email:4
-msgid "The system detected some new problems with your data."
+#: aleksis/core/templates/templated_email/base.email:5
+#: aleksis/core/templates/templated_email/base.email:16
+msgid "Hello"
 msgstr ""
 
-#: aleksis/core/templates/templated_email/data_checks.email:8
-#: aleksis/core/templates/templated_email/data_checks.email:24
-#: aleksis/core/templates/templated_email/person_changed.email:8
-#: aleksis/core/templates/templated_email/person_changed.email:20
-msgid "Hello,"
+#: aleksis/core/templates/templated_email/celery_failure.email:4
+#, python-format
+msgid "Celery task %(task_name)s failed!"
 msgstr ""
 
-#: aleksis/core/templates/templated_email/data_checks.email:10
+#: aleksis/core/templates/templated_email/celery_failure.email:7
+#, python-format
+msgid "the celery task %(task_name)s failed with following information:"
+msgstr ""
+
+#: aleksis/core/templates/templated_email/celery_failure.email:10
+#: aleksis/core/templates/templated_email/celery_failure.email:29
+msgid "Task ID"
+msgstr ""
+
+#: aleksis/core/templates/templated_email/celery_failure.email:11
+#: aleksis/core/templates/templated_email/celery_failure.email:30
+#, fuzzy
+#| msgid "Description"
+msgid "Raised exception"
+msgstr "Description"
+
+#: aleksis/core/templates/templated_email/celery_failure.email:12
+#: aleksis/core/templates/templated_email/celery_failure.email:31
+#, fuzzy
+#| msgid "Contact details"
+msgid "Positional arguments"
+msgstr "Détails de contact"
+
+#: aleksis/core/templates/templated_email/celery_failure.email:15
+#: aleksis/core/templates/templated_email/celery_failure.email:38
+msgid "Keyword arguments"
+msgstr ""
+
+#: aleksis/core/templates/templated_email/celery_failure.email:22
+#, python-format
 msgid ""
 "\n"
-"  the system detected some new problems with your data.\n"
-"  Please take some time to inspect them and solve the issues or mark them as ignored.\n"
-" "
+"      the celery task %(task_name)s failed with following information:\n"
+"    "
+msgstr ""
+
+#: aleksis/core/templates/templated_email/data_checks.email:3
+msgid "The system detected some new problems with your data."
+msgstr ""
+
+#: aleksis/core/templates/templated_email/data_checks.email:6
+msgid ""
+"the system detected some new problems with your data.\n"
+"Please take some time to inspect them and solve the issues or mark them as ignored."
 msgstr ""
 
-#: aleksis/core/templates/templated_email/data_checks.email:26
+#: aleksis/core/templates/templated_email/data_checks.email:15
 msgid ""
 "\n"
 "   the system detected some new problems with your data.\n"
@@ -2361,45 +2633,42 @@ msgid ""
 "  "
 msgstr ""
 
-#: aleksis/core/templates/templated_email/data_checks.email:34
+#: aleksis/core/templates/templated_email/data_checks.email:23
 #, fuzzy
 #| msgid "Description"
 msgid "Problem description"
 msgstr "Description"
 
-#: aleksis/core/templates/templated_email/data_checks.email:35
+#: aleksis/core/templates/templated_email/data_checks.email:24
 msgid "Count of objects with new problems"
 msgstr ""
 
-#: aleksis/core/templates/templated_email/notification.email:3
+#: aleksis/core/templates/templated_email/notification.email:4
 msgid "New notification for"
 msgstr ""
 
 #: aleksis/core/templates/templated_email/notification.email:6
-#: aleksis/core/templates/templated_email/notification.email:27
+#: aleksis/core/templates/templated_email/notification.email:20
 #, python-format
-msgid "Dear %(notification_user)s,"
+msgid "Hello %(notification_user)s,"
 msgstr ""
 
-#: aleksis/core/templates/templated_email/notification.email:8
-#: aleksis/core/templates/templated_email/notification.email:29
+#: aleksis/core/templates/templated_email/notification.email:9
+#: aleksis/core/templates/templated_email/notification.email:23
 msgid "we got a new notification for you:"
 msgstr ""
 
 #: aleksis/core/templates/templated_email/notification.email:15
-#: aleksis/core/templates/templated_email/notification.email:35
+#: aleksis/core/templates/templated_email/notification.email:29
 msgid "More information"
 msgstr ""
 
-#: aleksis/core/templates/templated_email/notification.email:18
+#: aleksis/core/templates/templated_email/notification.email:17
 #, python-format
-msgid ""
-"\n"
-"        Sent by %(trans_sender)s at %(trans_created_at)s\n"
-"    "
+msgid "Sent by %(trans_sender)s at %(trans_created_at)s"
 msgstr ""
 
-#: aleksis/core/templates/templated_email/notification.email:40
+#: aleksis/core/templates/templated_email/notification.email:34
 #, python-format
 msgid ""
 "\n"
@@ -2412,15 +2681,12 @@ msgstr ""
 msgid "%(person)s changed their data!"
 msgstr ""
 
-#: aleksis/core/templates/templated_email/person_changed.email:10
+#: aleksis/core/templates/templated_email/person_changed.email:7
 #, python-format
-msgid ""
-"\n"
-"   the person %(person)s recently changed the following fields:\n"
-" "
+msgid "the person %(person)s recently changed the following fields:"
 msgstr ""
 
-#: aleksis/core/templates/templated_email/person_changed.email:22
+#: aleksis/core/templates/templated_email/person_changed.email:15
 #, python-format
 msgid ""
 "\n"
@@ -2471,21 +2737,31 @@ msgstr ""
 msgid "Generate Tokens"
 msgstr ""
 
-#: aleksis/core/templates/two_factor/core/login.html:20
+#: aleksis/core/templates/two_factor/core/login.html:27
+#, fuzzy, python-format
+#| msgid "Contact details"
+msgid "Login for %(name)s"
+msgstr "Détails de contact"
+
+#: aleksis/core/templates/two_factor/core/login.html:30
 msgid "Login with username and password"
 msgstr ""
 
-#: aleksis/core/templates/two_factor/core/login.html:28
+#: aleksis/core/templates/two_factor/core/login.html:38
 msgid ""
 "You have no permission to view this page. Please login with an other\n"
 "                    account."
 msgstr ""
 
-#: aleksis/core/templates/two_factor/core/login.html:36
+#: aleksis/core/templates/two_factor/core/login.html:47
+msgid "Please login with your account to use the external application."
+msgstr ""
+
+#: aleksis/core/templates/two_factor/core/login.html:54
 msgid "Please login to see this page."
 msgstr ""
 
-#: aleksis/core/templates/two_factor/core/login.html:46
+#: aleksis/core/templates/two_factor/core/login.html:65
 msgid ""
 "\n"
 "                        We are calling your phone right now, please enter the\n"
@@ -2493,7 +2769,7 @@ msgid ""
 "                      "
 msgstr ""
 
-#: aleksis/core/templates/two_factor/core/login.html:51
+#: aleksis/core/templates/two_factor/core/login.html:70
 msgid ""
 "\n"
 "                        We sent you a text message, please enter the tokens we\n"
@@ -2501,7 +2777,7 @@ msgid ""
 "                      "
 msgstr ""
 
-#: aleksis/core/templates/two_factor/core/login.html:56
+#: aleksis/core/templates/two_factor/core/login.html:75
 msgid ""
 "\n"
 "                        Please enter the tokens generated by your token\n"
@@ -2509,7 +2785,7 @@ msgid ""
 "                      "
 msgstr ""
 
-#: aleksis/core/templates/two_factor/core/login.html:62
+#: aleksis/core/templates/two_factor/core/login.html:81
 msgid ""
 "\n"
 "                      Use this form for entering backup tokens for logging in.\n"
@@ -2518,23 +2794,23 @@ msgid ""
 "                    "
 msgstr ""
 
-#: aleksis/core/templates/two_factor/core/login.html:90
+#: aleksis/core/templates/two_factor/core/login.html:109
 msgid "Device currently not available?"
 msgstr ""
 
-#: aleksis/core/templates/two_factor/core/login.html:92
+#: aleksis/core/templates/two_factor/core/login.html:111
 msgid "Or, alternatively, use one of your backup phones:"
 msgstr ""
 
-#: aleksis/core/templates/two_factor/core/login.html:102
+#: aleksis/core/templates/two_factor/core/login.html:121
 msgid "As a last resort, you can use a backup token:"
 msgstr ""
 
-#: aleksis/core/templates/two_factor/core/login.html:105
+#: aleksis/core/templates/two_factor/core/login.html:124
 msgid "Use Backup Token"
 msgstr ""
 
-#: aleksis/core/templates/two_factor/core/login.html:116
+#: aleksis/core/templates/two_factor/core/login.html:135
 msgid "Use alternative login options"
 msgstr ""
 
@@ -2771,139 +3047,190 @@ msgid ""
 "      "
 msgstr ""
 
-#: aleksis/core/util/notifications.py:65
+#: aleksis/core/util/notifications.py:63
 msgid "E-Mail"
 msgstr ""
 
-#: aleksis/core/util/notifications.py:66
+#: aleksis/core/util/notifications.py:64
 msgid "SMS"
 msgstr ""
 
-#: aleksis/core/util/pdf.py:105
+#: aleksis/core/util/pdf.py:118
 msgid "Progress: Generate PDF file"
 msgstr ""
 
-#: aleksis/core/util/pdf.py:106
+#: aleksis/core/util/pdf.py:119
 msgid "Generating PDF file …"
 msgstr ""
 
-#: aleksis/core/util/pdf.py:107
+#: aleksis/core/util/pdf.py:120
 msgid "The PDF file has been generated successfully."
 msgstr ""
 
-#: aleksis/core/util/pdf.py:108
+#: aleksis/core/util/pdf.py:121
 msgid "There was a problem while generating the PDF file."
 msgstr ""
 
-#: aleksis/core/util/pdf.py:111
+#: aleksis/core/util/pdf.py:124
 msgid "Download PDF"
 msgstr ""
 
-#: aleksis/core/views.py:251
+#: aleksis/core/views.py:285
 msgid "The school term has been created."
 msgstr ""
 
-#: aleksis/core/views.py:263
+#: aleksis/core/views.py:297
 msgid "The school term has been saved."
 msgstr ""
 
-#: aleksis/core/views.py:387
+#: aleksis/core/views.py:417
 msgid "The child groups were successfully saved."
 msgstr ""
 
-#: aleksis/core/views.py:406 aleksis/core/views.py:416
+#: aleksis/core/views.py:436 aleksis/core/views.py:446
 msgid "The person has been saved."
 msgstr ""
 
-#: aleksis/core/views.py:466
+#: aleksis/core/views.py:496
 msgid "The group has been saved."
 msgstr ""
 
-#: aleksis/core/views.py:563
+#: aleksis/core/views.py:593
 msgid "The announcement has been saved."
 msgstr ""
 
-#: aleksis/core/views.py:579
+#: aleksis/core/views.py:609
 msgid "The announcement has been deleted."
 msgstr ""
 
-#: aleksis/core/views.py:663
+#: aleksis/core/views.py:677
+msgid "The requested preference registry does not exist"
+msgstr ""
+
+#: aleksis/core/views.py:696
 msgid "The preferences have been saved successfully."
 msgstr ""
 
-#: aleksis/core/views.py:687
+#: aleksis/core/views.py:720
 msgid "The person has been deleted."
 msgstr ""
 
-#: aleksis/core/views.py:701
+#: aleksis/core/views.py:734
 msgid "The group has been deleted."
 msgstr ""
 
-#: aleksis/core/views.py:733
+#: aleksis/core/views.py:766
 msgid "The additional_field has been saved."
 msgstr ""
 
-#: aleksis/core/views.py:767
+#: aleksis/core/views.py:800
 msgid "The additional field has been deleted."
 msgstr ""
 
-#: aleksis/core/views.py:792
+#: aleksis/core/views.py:825
 msgid "The group type has been saved."
 msgstr ""
 
-#: aleksis/core/views.py:822
+#: aleksis/core/views.py:855
 msgid "The group type has been deleted."
 msgstr ""
 
-#: aleksis/core/views.py:855
+#: aleksis/core/views.py:888
 msgid "Progress: Run data checks"
 msgstr ""
 
-#: aleksis/core/views.py:856
+#: aleksis/core/views.py:889
 msgid "Run data checks …"
 msgstr ""
 
-#: aleksis/core/views.py:857
+#: aleksis/core/views.py:890
 msgid "The data checks were run successfully."
 msgstr ""
 
-#: aleksis/core/views.py:858
+#: aleksis/core/views.py:891
 msgid "There was a problem while running data checks."
 msgstr ""
 
-#: aleksis/core/views.py:874
+#: aleksis/core/views.py:907
 #, python-brace-format
 msgid "The solve option '{solve_option_obj.verbose_name}' "
 msgstr ""
 
-#: aleksis/core/views.py:916
+#: aleksis/core/views.py:917
+msgid "The requested solve option does not exist"
+msgstr ""
+
+#: aleksis/core/views.py:949
 msgid "The dashboard widget has been saved."
 msgstr ""
 
-#: aleksis/core/views.py:946
+#: aleksis/core/views.py:979
 msgid "The dashboard widget has been created."
 msgstr ""
 
-#: aleksis/core/views.py:956
+#: aleksis/core/views.py:989
 msgid "The dashboard widget has been deleted."
 msgstr ""
 
-#: aleksis/core/views.py:1023
+#: aleksis/core/views.py:1060
 msgid "Your dashboard configuration has been saved successfully."
 msgstr ""
 
-#: aleksis/core/views.py:1025
+#: aleksis/core/views.py:1062
 msgid "The configuration of the default dashboard has been saved successfully."
 msgstr ""
 
-#: aleksis/core/views.py:1153
+#: aleksis/core/views.py:1138
+#, python-brace-format
+msgid "The invitation was successfully created. The invitation code is {code}"
+msgstr ""
+
+#: aleksis/core/views.py:1229
+msgid "We have successfully assigned the permissions."
+msgstr ""
+
+#: aleksis/core/views.py:1239
+msgid "The global user permission has been deleted."
+msgstr ""
+
+#: aleksis/core/views.py:1249
+msgid "The global group permission has been deleted."
+msgstr ""
+
+#: aleksis/core/views.py:1259
+msgid "The object user permission has been deleted."
+msgstr ""
+
+#: aleksis/core/views.py:1269
+msgid "The object group permission has been deleted."
+msgstr ""
+
+#: aleksis/core/views.py:1337
+msgid "The requested PDF file does not exist"
+msgstr ""
+
+#: aleksis/core/views.py:1346 aleksis/core/views.py:1350
+msgid "The requested task does not exist or is not accessible"
+msgstr ""
+
+#: aleksis/core/views.py:1388
 msgid "The third-party account could not be disconnected because it is the only login method available."
 msgstr ""
 
-#: aleksis/core/views.py:1160
+#: aleksis/core/views.py:1395
 msgid "The third-party account has been successfully disconnected."
 msgstr ""
 
+#: aleksis/core/views.py:1466
+msgid "Person was invited successfully and an email with further instructions has been send to them."
+msgstr ""
+
+#: aleksis/core/views.py:1477
+#, fuzzy
+#| msgid "This username is already in use."
+msgid "Person was already invited."
+msgstr "Cet nom est deja en utilisation."
+
 #, fuzzy
 #~| msgid "Contact details"
 #~ msgid "Can link persons to accounts"
diff --git a/aleksis/core/locale/fr/LC_MESSAGES/djangojs.po b/aleksis/core/locale/fr/LC_MESSAGES/djangojs.po
index bbd51395a6ee28d076da3de7bd4d65b198574194..9f07be2903046e41680c481bea659aa95b695df7 100644
--- a/aleksis/core/locale/fr/LC_MESSAGES/djangojs.po
+++ b/aleksis/core/locale/fr/LC_MESSAGES/djangojs.po
@@ -8,7 +8,7 @@ msgid ""
 msgstr ""
 "Project-Id-Version: PACKAGE VERSION\n"
 "Report-Msgid-Bugs-To: \n"
-"POT-Creation-Date: 2021-10-28 16:18+0200\n"
+"POT-Creation-Date: 2022-02-08 23:16+0000\n"
 "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
 "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
 "Language-Team: LANGUAGE <LL@li.org>\n"
@@ -18,18 +18,18 @@ msgstr ""
 "Content-Transfer-Encoding: 8bit\n"
 "Plural-Forms: nplurals=2; plural=(n > 1);\n"
 
-#: aleksis/core/static/js/main.js:15
+#: aleksis/core/static/js/main.js:66
 msgid "Today"
 msgstr ""
 
-#: aleksis/core/static/js/main.js:16
+#: aleksis/core/static/js/main.js:67
 msgid "Cancel"
 msgstr ""
 
-#: aleksis/core/static/js/main.js:17
+#: aleksis/core/static/js/main.js:68
 msgid "OK"
 msgstr ""
 
-#: aleksis/core/static/js/main.js:127
+#: aleksis/core/static/js/main.js:191
 msgid "This page may contain outdated information since there is no internet connection."
 msgstr ""
diff --git a/aleksis/core/locale/la/LC_MESSAGES/django.po b/aleksis/core/locale/la/LC_MESSAGES/django.po
index c34a5cc0811a09a7a1b1bf4430463e56cff5be3f..4b2afb73bbbc2fd8a426789859152fa2d1895395 100644
--- a/aleksis/core/locale/la/LC_MESSAGES/django.po
+++ b/aleksis/core/locale/la/LC_MESSAGES/django.po
@@ -7,7 +7,7 @@ msgid ""
 msgstr ""
 "Project-Id-Version: PACKAGE VERSION\n"
 "Report-Msgid-Bugs-To: \n"
-"POT-Creation-Date: 2021-10-28 16:18+0200\n"
+"POT-Creation-Date: 2022-02-08 23:16+0000\n"
 "PO-Revision-Date: 2020-12-19 12:57+0000\n"
 "Last-Translator: Julian <leuckerj@gmail.com>\n"
 "Language-Team: Latin <https://translate.edugit.org/projects/aleksis/aleksis/la/>\n"
@@ -18,37 +18,44 @@ msgstr ""
 "Plural-Forms: nplurals=2; plural=n != 1;\n"
 "X-Generator: Weblate 4.3.2\n"
 
-#: aleksis/core/apps.py:150
+#: aleksis/core/apps.py:166
 msgid "OpenID Connect scope"
 msgstr ""
 
-#: aleksis/core/apps.py:151
+#: aleksis/core/apps.py:167
 msgid "Given name, family name, link to profile and picture if existing."
 msgstr ""
 
-#: aleksis/core/apps.py:152
+#: aleksis/core/apps.py:168
 #, fuzzy
 #| msgid "E-mail address"
 msgid "Full home postal address"
 msgstr "Inscriptio electronica"
 
-#: aleksis/core/apps.py:153
+#: aleksis/core/apps.py:169
 #, fuzzy
 #| msgid "E-mail address"
 msgid "Email address"
 msgstr "Inscriptio electronica"
 
-#: aleksis/core/apps.py:154
+#: aleksis/core/apps.py:170
 #, fuzzy
 #| msgid "Mobile phone"
 msgid "Home and mobile phone"
 msgstr "Numerus telephoni mobilis"
 
-#: aleksis/core/data_checks.py:55
+#: aleksis/core/apps.py:171 aleksis/core/forms.py:220 aleksis/core/menus.py:265
+#: aleksis/core/models.py:462 aleksis/core/templates/core/group/list.html:8
+#: aleksis/core/templates/core/group/list.html:9
+#: aleksis/core/templates/core/person/full.html:250
+msgid "Groups"
+msgstr "Greges"
+
+#: aleksis/core/data_checks.py:56
 msgid "Ignore problem"
 msgstr ""
 
-#: aleksis/core/data_checks.py:184
+#: aleksis/core/data_checks.py:185
 #, python-brace-format
 msgid "Solve option '{solve_option_obj.verbose_name}' "
 msgstr ""
@@ -67,8 +74,8 @@ msgstr ""
 msgid "The DashboardWidget was reported broken automatically."
 msgstr ""
 
-#: aleksis/core/filters.py:37 aleksis/core/templates/core/base.html:83
-#: aleksis/core/templates/core/base.html:84
+#: aleksis/core/filters.py:42 aleksis/core/templates/core/base.html:105
+#: aleksis/core/templates/core/base.html:106
 #: aleksis/core/templates/core/group/list.html:20
 #: aleksis/core/templates/core/person/list.html:24
 #: aleksis/core/templates/search/search.html:7
@@ -76,144 +83,199 @@ msgstr ""
 msgid "Search"
 msgstr "Quaerere"
 
-#: aleksis/core/filters.py:53
+#: aleksis/core/filters.py:59
 msgid "Search by name"
 msgstr "Quaerere cum breve nomine"
 
-#: aleksis/core/filters.py:65
+#: aleksis/core/filters.py:71
 #, fuzzy
 #| msgid "E-mail address"
 msgid "Search by contact details"
 msgstr "Inscriptio electronica"
 
-#: aleksis/core/forms.py:41 aleksis/core/forms.py:387
+#: aleksis/core/filters.py:92
+msgid "Permission"
+msgstr ""
+
+#: aleksis/core/filters.py:100
+msgid "Content type"
+msgstr ""
+
+#: aleksis/core/filters.py:113 aleksis/core/models.py:688
+msgid "User"
+msgstr ""
+
+#: aleksis/core/filters.py:135 aleksis/core/models.py:461
+msgid "Group"
+msgstr "Grex"
+
+#: aleksis/core/forms.py:50 aleksis/core/forms.py:581
 msgid "Base data"
 msgstr ""
 
-#: aleksis/core/forms.py:47
+#: aleksis/core/forms.py:55
 #, fuzzy
 #| msgid "E-mail address"
 msgid "Address"
 msgstr "Inscriptio electronica"
 
-#: aleksis/core/forms.py:48
+#: aleksis/core/forms.py:56 aleksis/core/forms.py:590
 msgid "Contact data"
 msgstr ""
 
-#: aleksis/core/forms.py:50
+#: aleksis/core/forms.py:58
 msgid "Advanced personal data"
 msgstr ""
 
-#: aleksis/core/forms.py:93
+#: aleksis/core/forms.py:106
 msgid "New user"
 msgstr ""
 
-#: aleksis/core/forms.py:93
+#: aleksis/core/forms.py:106
 #, fuzzy
 #| msgid "Persons and accounts"
 msgid "Create a new account"
 msgstr "Personae et computi"
 
-#: aleksis/core/forms.py:124
+#: aleksis/core/forms.py:132
 msgid "You cannot set a new username when also selecting an existing user."
 msgstr ""
 
-#: aleksis/core/forms.py:128
+#: aleksis/core/forms.py:136
 msgid "This username is already in use."
 msgstr ""
 
-#: aleksis/core/forms.py:145 aleksis/core/models.py:117
+#: aleksis/core/forms.py:153 aleksis/core/models.py:130
 msgid "School term"
 msgstr "Anus scolae"
 
-#: aleksis/core/forms.py:146
+#: aleksis/core/forms.py:154
 #, fuzzy
 #| msgid "Data management"
 msgid "Common data"
 msgstr "Adminstratio datarum"
 
-#: aleksis/core/forms.py:147 aleksis/core/forms.py:196
-#: aleksis/core/menus.py:238 aleksis/core/models.py:140
+#: aleksis/core/forms.py:155 aleksis/core/forms.py:207
+#: aleksis/core/menus.py:254 aleksis/core/models.py:153
 #: aleksis/core/templates/core/person/list.html:8
 #: aleksis/core/templates/core/person/list.html:9
 msgid "Persons"
 msgstr "personae"
 
-#: aleksis/core/forms.py:148
+#: aleksis/core/forms.py:156 aleksis/core/forms.py:592
 #, fuzzy
 #| msgid "Additional name(s)"
 msgid "Additional data"
 msgstr "addita nomines"
 
-#: aleksis/core/forms.py:188 aleksis/core/forms.py:191
-#: aleksis/core/models.py:60
+#: aleksis/core/forms.py:157 aleksis/core/models.py:206
+#: aleksis/core/models.py:514
+msgid "Photo"
+msgstr "Photographia"
+
+#: aleksis/core/forms.py:199 aleksis/core/forms.py:202
+#: aleksis/core/models.py:73
 msgid "Date"
 msgstr "dies"
 
-#: aleksis/core/forms.py:189 aleksis/core/forms.py:192
-#: aleksis/core/models.py:68
+#: aleksis/core/forms.py:200 aleksis/core/forms.py:203
+#: aleksis/core/models.py:81
 msgid "Time"
 msgstr "tempus"
 
-#: aleksis/core/forms.py:209 aleksis/core/menus.py:249
-#: aleksis/core/models.py:398 aleksis/core/templates/core/group/list.html:8
-#: aleksis/core/templates/core/group/list.html:9
-#: aleksis/core/templates/core/person/full.html:144
-msgid "Groups"
-msgstr "Greges"
-
-#: aleksis/core/forms.py:219
+#: aleksis/core/forms.py:233
 msgid "From when until when should the announcement be displayed?"
 msgstr ""
 
-#: aleksis/core/forms.py:222
+#: aleksis/core/forms.py:236
 msgid "Who should see the announcement?"
 msgstr "Quis nuntium videatne?"
 
-#: aleksis/core/forms.py:223
+#: aleksis/core/forms.py:237
 msgid "Write your announcement:"
 msgstr "Scribe nuntium:"
 
-#: aleksis/core/forms.py:262
+#: aleksis/core/forms.py:276
 msgid "You are not allowed to create announcements which are only valid in the past."
 msgstr ""
 
-#: aleksis/core/forms.py:266
+#: aleksis/core/forms.py:280
 msgid "The from date and time must be earlier then the until date and time."
 msgstr ""
 
-#: aleksis/core/forms.py:275
+#: aleksis/core/forms.py:289
 msgid "You need at least one recipient."
 msgstr ""
 
-#: aleksis/core/forms.py:389
+#: aleksis/core/forms.py:398
+msgid "Invitation code"
+msgstr ""
+
+#: aleksis/core/forms.py:399
+msgid "Please enter your invitation code."
+msgstr ""
+
+#: aleksis/core/forms.py:418 aleksis/core/models.py:181
+msgid "First name"
+msgstr "Primus nomen"
+
+#: aleksis/core/forms.py:419 aleksis/core/models.py:182
+msgid "Last name"
+msgstr "Secondus nomen"
+
+#: aleksis/core/forms.py:428
 #, fuzzy
-#| msgid "Data management"
-msgid "Account data"
-msgstr "Adminstratio datarum"
+#| msgid "E-mail address"
+msgid "A person is using this e-mail address"
+msgstr "Inscriptio electronica"
 
-#: aleksis/core/forms.py:391
-msgid "Consents"
+#: aleksis/core/forms.py:456
+#, fuzzy
+#| msgid "Who should see the announcement?"
+msgid "Who should get the permission?"
+msgstr "Quis nuntium videatne?"
+
+#: aleksis/core/forms.py:457
+msgid "On what?"
 msgstr ""
 
-#: aleksis/core/forms.py:396
-msgid "Password"
+#: aleksis/core/forms.py:483
+msgid "Select objects which the permission should be granted for:"
 msgstr ""
 
-#: aleksis/core/forms.py:402
-msgid "Password (again)"
+#: aleksis/core/forms.py:486
+msgid "Grant the permission for all objects"
 msgstr ""
 
-#: aleksis/core/forms.py:411
-#, python-brace-format
-msgid "I have read the <a href='{privacy_policy}'>Privacy policy</a> and agree with them."
+#: aleksis/core/forms.py:494
+msgid "You must select at least one group or person which should get the permission."
+msgstr ""
+
+#: aleksis/core/forms.py:499
+msgid "You must grant the permission to all objects and/or to some objects."
+msgstr ""
+
+#: aleksis/core/forms.py:586
+#, fuzzy
+#| msgid "E-mail address"
+msgid "Adress data"
+msgstr "Inscriptio electronica"
+
+#: aleksis/core/forms.py:598
+#, fuzzy
+#| msgid "Data management"
+msgid "Account data"
+msgstr "Adminstratio datarum"
+
+#: aleksis/core/forms.py:605
+msgid "Password"
 msgstr ""
 
-#: aleksis/core/forms.py:435
-msgid "You must type the same password each time."
+#: aleksis/core/forms.py:608
+msgid "Password (again)"
 msgstr ""
 
-#: aleksis/core/forms.py:580
+#: aleksis/core/forms.py:775
 msgid "No valid selection."
 msgstr ""
 
@@ -223,67 +285,67 @@ msgstr ""
 msgid "There are unresolved data problems."
 msgstr "Scribe nuntium:"
 
-#: aleksis/core/health_checks.py:38
-msgid "The backup folder doesn't exist."
+#: aleksis/core/health_checks.py:40
+msgid "Error accessing backup storage: {}"
 msgstr ""
 
-#: aleksis/core/health_checks.py:47
-#, python-brace-format
-msgid "Last backup {time_gone_since_backup}!"
+#: aleksis/core/health_checks.py:50
+msgid "Last backup {}!"
 msgstr ""
 
-#: aleksis/core/health_checks.py:49
+#: aleksis/core/health_checks.py:52
 msgid "No backup found!"
 msgstr ""
 
-#: aleksis/core/health_checks.py:76
+#: aleksis/core/health_checks.py:79
 msgid "No backup result found!"
 msgstr ""
 
-#: aleksis/core/health_checks.py:78
-#, python-brace-format
-msgid "{task.status} - {task.result}"
-msgstr ""
-
 #: aleksis/core/menus.py:9 aleksis/core/templates/two_factor/core/login.html:6
-#: aleksis/core/templates/two_factor/core/login.html:22
-#: aleksis/core/templates/two_factor/core/login.html:76
+#: aleksis/core/templates/two_factor/core/login.html:32
+#: aleksis/core/templates/two_factor/core/login.html:95
 msgid "Login"
 msgstr "nomen profiteri"
 
-#: aleksis/core/menus.py:15 aleksis/core/templates/account/signup.html:20
+#: aleksis/core/menus.py:15 aleksis/core/templates/account/signup.html:22
 #: aleksis/core/templates/socialaccount/signup.html:23
 msgid "Sign up"
 msgstr ""
 
-#: aleksis/core/menus.py:24
+#: aleksis/core/menus.py:24 aleksis/core/templates/invitations/enter.html:7
+#, fuzzy
+#| msgid "Edit school information"
+msgid "Accept invitation"
+msgstr "Muta informationes scolae"
+
+#: aleksis/core/menus.py:33
 msgid "Dashboard"
 msgstr "Forum"
 
-#: aleksis/core/menus.py:32 aleksis/core/models.py:605
-#: aleksis/core/preferences.py:26
+#: aleksis/core/menus.py:41 aleksis/core/models.py:734
+#: aleksis/core/preferences.py:29
 #: aleksis/core/templates/core/notifications.html:4
 #: aleksis/core/templates/core/notifications.html:5
 msgid "Notifications"
 msgstr "Nuntii"
 
-#: aleksis/core/menus.py:41
+#: aleksis/core/menus.py:53
 msgid "Account"
 msgstr ""
 
-#: aleksis/core/menus.py:48
+#: aleksis/core/menus.py:60
 msgid "Stop impersonation"
 msgstr "Simulandum aliquem finire"
 
-#: aleksis/core/menus.py:57 aleksis/core/templates/core/base.html:62
+#: aleksis/core/menus.py:69 aleksis/core/templates/core/base.html:80
 msgid "Logout"
 msgstr "nomen retractare"
 
-#: aleksis/core/menus.py:63
+#: aleksis/core/menus.py:75
 msgid "2FA"
 msgstr ""
 
-#: aleksis/core/menus.py:69
+#: aleksis/core/menus.py:83
 #: aleksis/core/templates/account/password_change.html:5
 #: aleksis/core/templates/account/password_change.html:6
 #: aleksis/core/templates/account/password_change.html:19
@@ -295,22 +357,22 @@ msgstr ""
 msgid "Change password"
 msgstr ""
 
-#: aleksis/core/menus.py:81
+#: aleksis/core/menus.py:95
 msgid "Me"
 msgstr ""
 
-#: aleksis/core/menus.py:90
+#: aleksis/core/menus.py:104
 #: aleksis/core/templates/dynamic_preferences/form.html:5
 msgid "Preferences"
 msgstr ""
 
-#: aleksis/core/menus.py:99
+#: aleksis/core/menus.py:113
 #, fuzzy
 #| msgid "Persons and accounts"
 msgid "Third-party accounts"
 msgstr "Personae et computi"
 
-#: aleksis/core/menus.py:108
+#: aleksis/core/menus.py:122
 #: aleksis/core/templates/oauth2_provider/authorized-tokens.html:5
 #: aleksis/core/templates/oauth2_provider/authorized-tokens.html:6
 #, fuzzy
@@ -318,23 +380,23 @@ msgstr "Personae et computi"
 msgid "Authorized applications"
 msgstr "Nuntii"
 
-#: aleksis/core/menus.py:119
+#: aleksis/core/menus.py:133
 msgid "Admin"
 msgstr "Administratio"
 
-#: aleksis/core/menus.py:127 aleksis/core/models.py:704
+#: aleksis/core/menus.py:141 aleksis/core/models.py:834
 #: aleksis/core/templates/core/announcement/list.html:7
 #: aleksis/core/templates/core/announcement/list.html:8
 msgid "Announcements"
 msgstr "Nuntii"
 
-#: aleksis/core/menus.py:138 aleksis/core/models.py:118
+#: aleksis/core/menus.py:152 aleksis/core/models.py:131
 #: aleksis/core/templates/core/school_term/list.html:8
 #: aleksis/core/templates/core/school_term/list.html:9
 msgid "School terms"
 msgstr "ani scolae"
 
-#: aleksis/core/menus.py:149
+#: aleksis/core/menus.py:163
 #: aleksis/core/templates/core/dashboard_widget/list.html:8
 #: aleksis/core/templates/core/dashboard_widget/list.html:9
 #, fuzzy
@@ -342,52 +404,53 @@ msgstr "ani scolae"
 msgid "Dashboard widgets"
 msgstr "Forum"
 
-#: aleksis/core/menus.py:160
+#: aleksis/core/menus.py:174
 #: aleksis/core/templates/core/management/data_management.html:6
 #: aleksis/core/templates/core/management/data_management.html:7
 msgid "Data management"
 msgstr "Adminstratio datarum"
 
-#: aleksis/core/menus.py:171
+#: aleksis/core/menus.py:185
 #: aleksis/core/templates/core/pages/system_status.html:5
 #: aleksis/core/templates/core/pages/system_status.html:7
 msgid "System status"
 msgstr "Status systemae"
 
-#: aleksis/core/menus.py:182
-msgid "Impersonation"
-msgstr "Simulare aliquem"
-
-#: aleksis/core/menus.py:193
+#: aleksis/core/menus.py:196
 #, fuzzy
 #| msgid "Notification"
 msgid "Configuration"
 msgstr "Nuntius"
 
-#: aleksis/core/menus.py:204 aleksis/core/templates/core/data_check/list.html:9
+#: aleksis/core/menus.py:207 aleksis/core/templates/core/data_check/list.html:9
 #: aleksis/core/templates/core/data_check/list.html:10
 #, fuzzy
 #| msgid "System status"
 msgid "Data checks"
 msgstr "Status systemae"
 
-#: aleksis/core/menus.py:210
+#: aleksis/core/menus.py:213 aleksis/core/templates/core/perms/list.html:13
+#: aleksis/core/templates/core/perms/list.html:14
+msgid "Manage permissions"
+msgstr ""
+
+#: aleksis/core/menus.py:224
 msgid "Backend Admin"
 msgstr ""
 
-#: aleksis/core/menus.py:216
-#: aleksis/core/templates/oauth2_provider/application_detail.html:5
-#: aleksis/core/templates/oauth2_provider/application_list.html:5
+#: aleksis/core/menus.py:232
+#: aleksis/core/templates/oauth2_provider/application/list.html:5
+#: aleksis/core/templates/oauth2_provider/application/list.html:6
 #, fuzzy
 #| msgid "Notifications"
 msgid "OAuth2 Applications"
 msgstr "Nuntii"
 
-#: aleksis/core/menus.py:229
+#: aleksis/core/menus.py:245
 msgid "People"
 msgstr "Personae"
 
-#: aleksis/core/menus.py:260 aleksis/core/models.py:958
+#: aleksis/core/menus.py:276 aleksis/core/models.py:1055
 #: aleksis/core/templates/core/group_type/list.html:8
 #: aleksis/core/templates/core/group_type/list.html:9
 #, fuzzy
@@ -395,11 +458,11 @@ msgstr "Personae"
 msgid "Group types"
 msgstr "Greges"
 
-#: aleksis/core/menus.py:271
+#: aleksis/core/menus.py:287
 msgid "Groups and child groups"
 msgstr ""
 
-#: aleksis/core/menus.py:282 aleksis/core/models.py:446
+#: aleksis/core/menus.py:298 aleksis/core/models.py:510
 #: aleksis/core/templates/core/additional_field/list.html:8
 #: aleksis/core/templates/core/additional_field/list.html:9
 #, fuzzy
@@ -407,829 +470,866 @@ msgstr ""
 msgid "Additional fields"
 msgstr "addita nomines"
 
-#: aleksis/core/menus.py:297
+#: aleksis/core/menus.py:309
+#, fuzzy
+#| msgid "Stop impersonation"
+msgid "Invite person"
+msgstr "Simulandum aliquem finire"
+
+#: aleksis/core/menus.py:322
 #: aleksis/core/templates/core/group/child_groups.html:7
 #: aleksis/core/templates/core/group/child_groups.html:9
 msgid "Assign child groups to groups"
 msgstr ""
 
-#: aleksis/core/mixins.py:498
+#: aleksis/core/mixins.py:511
 #, fuzzy
 #| msgid "Edit school term"
 msgid "Linked school term"
 msgstr "Muta anum scolae"
 
-#: aleksis/core/models.py:58
+#: aleksis/core/models.py:71
 msgid "Boolean (Yes/No)"
 msgstr ""
 
-#: aleksis/core/models.py:59
+#: aleksis/core/models.py:72
 msgid "Text (one line)"
 msgstr ""
 
-#: aleksis/core/models.py:61
+#: aleksis/core/models.py:74
 msgid "Date and time"
 msgstr "Dies et hora"
 
-#: aleksis/core/models.py:62
+#: aleksis/core/models.py:75
 msgid "Decimal number"
 msgstr ""
 
-#: aleksis/core/models.py:63 aleksis/core/models.py:186
+#: aleksis/core/models.py:76 aleksis/core/models.py:199
 msgid "E-mail address"
 msgstr "Inscriptio electronica"
 
-#: aleksis/core/models.py:64
+#: aleksis/core/models.py:77
 msgid "Integer"
 msgstr ""
 
-#: aleksis/core/models.py:65
+#: aleksis/core/models.py:78
 #, fuzzy
 #| msgid "E-mail address"
 msgid "IP address"
 msgstr "Inscriptio electronica"
 
-#: aleksis/core/models.py:66
+#: aleksis/core/models.py:79
 msgid "Boolean or empty (Yes/No/Neither)"
 msgstr ""
 
-#: aleksis/core/models.py:67
+#: aleksis/core/models.py:80
 msgid "Text (multi-line)"
 msgstr ""
 
-#: aleksis/core/models.py:69
+#: aleksis/core/models.py:82
 msgid "URL / Link"
 msgstr ""
 
-#: aleksis/core/models.py:81 aleksis/core/models.py:927
+#: aleksis/core/models.py:94 aleksis/core/models.py:1024
 msgid "Name"
 msgstr "Nomen"
 
-#: aleksis/core/models.py:83
+#: aleksis/core/models.py:96
 msgid "Start date"
 msgstr ""
 
-#: aleksis/core/models.py:84
+#: aleksis/core/models.py:97
 msgid "End date"
 msgstr ""
 
-#: aleksis/core/models.py:103
+#: aleksis/core/models.py:116
 msgid "The start date must be earlier than the end date."
 msgstr ""
 
-#: aleksis/core/models.py:110
+#: aleksis/core/models.py:123
 msgid "There is already a school term for this time or a part of this time."
 msgstr ""
 
-#: aleksis/core/models.py:139 aleksis/core/models.py:876
+#: aleksis/core/models.py:152 aleksis/core/models.py:973
 msgid "Person"
 msgstr "Persona"
 
-#: aleksis/core/models.py:142
+#: aleksis/core/models.py:155
 #, fuzzy
 #| msgid "E-mail address"
 msgid "Can view address"
 msgstr "Inscriptio electronica"
 
-#: aleksis/core/models.py:143
+#: aleksis/core/models.py:156
 #, fuzzy
 #| msgid "E-mail address"
 msgid "Can view contact details"
 msgstr "Inscriptio electronica"
 
-#: aleksis/core/models.py:144
+#: aleksis/core/models.py:157
 #, fuzzy
 #| msgid "E-mail address"
 msgid "Can view photo"
 msgstr "Inscriptio electronica"
 
-#: aleksis/core/models.py:145
+#: aleksis/core/models.py:158
+#, fuzzy
+#| msgid "E-mail address"
+msgid "Can view avatar image"
+msgstr "Inscriptio electronica"
+
+#: aleksis/core/models.py:159
 #, fuzzy
 #| msgid "Persons and accounts"
 msgid "Can view persons groups"
 msgstr "Personae et computi"
 
-#: aleksis/core/models.py:146
+#: aleksis/core/models.py:160
 #, fuzzy
 #| msgid "Stop impersonation"
 msgid "Can view personal details"
 msgstr "Simulandum aliquem finire"
 
-#: aleksis/core/models.py:156
+#: aleksis/core/models.py:170
 msgid "female"
 msgstr "femininum"
 
-#: aleksis/core/models.py:156
+#: aleksis/core/models.py:170
 msgid "male"
 msgstr "maskulinum"
 
-#: aleksis/core/models.py:164
+#: aleksis/core/models.py:178 aleksis/core/models.py:1227
 msgid "Linked user"
 msgstr ""
 
-#: aleksis/core/models.py:166
-#, fuzzy
-#| msgid "Impersonation"
-msgid "Is person active?"
-msgstr "Simulare aliquem"
-
-#: aleksis/core/models.py:168
-msgid "First name"
-msgstr "Primus nomen"
-
-#: aleksis/core/models.py:169
-msgid "Last name"
-msgstr "Secondus nomen"
-
-#: aleksis/core/models.py:171
+#: aleksis/core/models.py:184
 msgid "Additional name(s)"
 msgstr "addita nomines"
 
-#: aleksis/core/models.py:175 aleksis/core/models.py:415
+#: aleksis/core/models.py:188 aleksis/core/models.py:479
 msgid "Short name"
 msgstr "Breve nomen"
 
-#: aleksis/core/models.py:178
+#: aleksis/core/models.py:191
 msgid "Street"
 msgstr "Via"
 
-#: aleksis/core/models.py:179
+#: aleksis/core/models.py:192
 msgid "Street number"
 msgstr "Numerus domini"
 
-#: aleksis/core/models.py:180
+#: aleksis/core/models.py:193
 msgid "Postal code"
 msgstr "Numerus directorius"
 
-#: aleksis/core/models.py:181
+#: aleksis/core/models.py:194
 msgid "Place"
 msgstr "Urbs"
 
-#: aleksis/core/models.py:183
+#: aleksis/core/models.py:196 aleksis/core/templates/core/person/full.html:172
 msgid "Home phone"
 msgstr "Numerus telephoni domi"
 
-#: aleksis/core/models.py:184
+#: aleksis/core/models.py:197 aleksis/core/templates/core/person/full.html:182
 msgid "Mobile phone"
 msgstr "Numerus telephoni mobilis"
 
-#: aleksis/core/models.py:188
+#: aleksis/core/models.py:201
 msgid "Date of birth"
 msgstr "Dies natalis"
 
-#: aleksis/core/models.py:189
+#: aleksis/core/models.py:202
+#, fuzzy
+#| msgid "Date of birth"
+msgid "Place of birth"
+msgstr "Dies natalis"
+
+#: aleksis/core/models.py:203
 msgid "Sex"
 msgstr "Genus"
 
-#: aleksis/core/models.py:191
-msgid "Photo"
-msgstr "Photographia"
+#: aleksis/core/models.py:210 aleksis/core/models.py:518
+msgid "This is an official photo, used for official documents and for internal use cases."
+msgstr ""
+
+#: aleksis/core/models.py:215 aleksis/core/models.py:522
+msgid "Display picture / Avatar"
+msgstr ""
+
+#: aleksis/core/models.py:218 aleksis/core/models.py:525
+msgid "This is a picture or an avatar for public display."
+msgstr ""
 
-#: aleksis/core/models.py:195 aleksis/core/templates/core/person/full.html:137
+#: aleksis/core/models.py:223 aleksis/core/templates/core/person/full.html:239
 msgid "Guardians / Parents"
 msgstr "Parentes"
 
-#: aleksis/core/models.py:202
+#: aleksis/core/models.py:230
 msgid "Primary group"
 msgstr ""
 
-#: aleksis/core/models.py:205 aleksis/core/models.py:563
-#: aleksis/core/models.py:587 aleksis/core/models.py:672
-#: aleksis/core/models.py:951 aleksis/core/templates/core/person/full.html:120
+#: aleksis/core/models.py:233 aleksis/core/models.py:692
+#: aleksis/core/models.py:716 aleksis/core/models.py:801
+#: aleksis/core/models.py:1048
 msgid "Description"
 msgstr "Descriptio"
 
-#: aleksis/core/models.py:370
+#: aleksis/core/models.py:434
 msgid "Title of field"
 msgstr ""
 
-#: aleksis/core/models.py:372
+#: aleksis/core/models.py:436
 msgid "Type of field"
 msgstr ""
 
-#: aleksis/core/models.py:379
+#: aleksis/core/models.py:443
 #, fuzzy
 #| msgid "Additional name(s)"
 msgid "Addtitional field for groups"
 msgstr "addita nomines"
 
-#: aleksis/core/models.py:380
+#: aleksis/core/models.py:444
 #, fuzzy
 #| msgid "Additional name(s)"
 msgid "Addtitional fields for groups"
 msgstr "addita nomines"
 
-#: aleksis/core/models.py:397
-msgid "Group"
-msgstr "Grex"
-
-#: aleksis/core/models.py:400
+#: aleksis/core/models.py:464
 msgid "Can assign child groups to groups"
 msgstr ""
 
-#: aleksis/core/models.py:401
+#: aleksis/core/models.py:465
 #, fuzzy
 #| msgid "Persons and accounts"
 msgid "Can view statistics about group."
 msgstr "Personae et computi"
 
-#: aleksis/core/models.py:413
+#: aleksis/core/models.py:477
 #, fuzzy
 #| msgid "Last name"
 msgid "Long name"
 msgstr "Secondus nomen"
 
-#: aleksis/core/models.py:423 aleksis/core/templates/core/group/full.html:85
+#: aleksis/core/models.py:487 aleksis/core/templates/core/group/full.html:85
 msgid "Members"
 msgstr ""
 
-#: aleksis/core/models.py:426 aleksis/core/templates/core/group/full.html:82
+#: aleksis/core/models.py:490 aleksis/core/templates/core/group/full.html:82
 msgid "Owners"
 msgstr ""
 
-#: aleksis/core/models.py:433 aleksis/core/templates/core/group/full.html:55
+#: aleksis/core/models.py:497 aleksis/core/templates/core/group/full.html:55
 msgid "Parent groups"
 msgstr ""
 
-#: aleksis/core/models.py:441
+#: aleksis/core/models.py:505
 msgid "Type of group"
 msgstr ""
 
-#: aleksis/core/models.py:559
-msgid "User"
-msgstr ""
-
-#: aleksis/core/models.py:562 aleksis/core/models.py:586
-#: aleksis/core/models.py:671
+#: aleksis/core/models.py:691 aleksis/core/models.py:715
+#: aleksis/core/models.py:800
 #: aleksis/core/templates/core/announcement/list.html:18
 msgid "Title"
 msgstr "Titulus"
 
-#: aleksis/core/models.py:565
+#: aleksis/core/models.py:694
 msgid "Application"
 msgstr ""
 
-#: aleksis/core/models.py:571
+#: aleksis/core/models.py:700
 msgid "Activity"
 msgstr ""
 
-#: aleksis/core/models.py:572
+#: aleksis/core/models.py:701
 msgid "Activities"
 msgstr ""
 
-#: aleksis/core/models.py:578
+#: aleksis/core/models.py:707
 msgid "Sender"
 msgstr "Mittens"
 
-#: aleksis/core/models.py:583
+#: aleksis/core/models.py:712
 msgid "Recipient"
 msgstr ""
 
-#: aleksis/core/models.py:588 aleksis/core/models.py:928
+#: aleksis/core/models.py:717 aleksis/core/models.py:1025
 msgid "Link"
 msgstr ""
 
-#: aleksis/core/models.py:590
+#: aleksis/core/models.py:719
 msgid "Read"
 msgstr ""
 
-#: aleksis/core/models.py:591
+#: aleksis/core/models.py:720
 msgid "Sent"
 msgstr ""
 
-#: aleksis/core/models.py:604
+#: aleksis/core/models.py:733
 #, fuzzy
 #| msgid "Notifications"
 msgid "Notification"
 msgstr "Nuntii"
 
-#: aleksis/core/models.py:673
+#: aleksis/core/models.py:802
 msgid "Link to detailed view"
 msgstr ""
 
-#: aleksis/core/models.py:676
+#: aleksis/core/models.py:805
 msgid "Date and time from when to show"
 msgstr ""
 
-#: aleksis/core/models.py:679
+#: aleksis/core/models.py:808
 msgid "Date and time until when to show"
 msgstr ""
 
-#: aleksis/core/models.py:703
+#: aleksis/core/models.py:833
 #, fuzzy
 #| msgid "Announcements"
 msgid "Announcement"
 msgstr "Nuntii"
 
-#: aleksis/core/models.py:741
+#: aleksis/core/models.py:871
 #, fuzzy
 #| msgid "Announcements"
 msgid "Announcement recipient"
 msgstr "Nuntii"
 
-#: aleksis/core/models.py:742
+#: aleksis/core/models.py:872
 #, fuzzy
 #| msgid "Announcements"
 msgid "Announcement recipients"
 msgstr "Nuntii"
 
-#: aleksis/core/models.py:797
+#: aleksis/core/models.py:894
 #, fuzzy
 #| msgid "Site title"
 msgid "Widget Title"
 msgstr "Titulus paginae"
 
-#: aleksis/core/models.py:798
+#: aleksis/core/models.py:895
 msgid "Activate Widget"
 msgstr ""
 
-#: aleksis/core/models.py:799
+#: aleksis/core/models.py:896
 #, fuzzy
 #| msgid "Site title"
 msgid "Widget is broken"
 msgstr "Titulus paginae"
 
-#: aleksis/core/models.py:802
+#: aleksis/core/models.py:899
 msgid "Size on mobile devices"
 msgstr ""
 
-#: aleksis/core/models.py:803
+#: aleksis/core/models.py:900
 msgid "<= 600 px, 12 columns"
 msgstr ""
 
-#: aleksis/core/models.py:808
+#: aleksis/core/models.py:905
 msgid "Size on tablet devices"
 msgstr ""
 
-#: aleksis/core/models.py:809
+#: aleksis/core/models.py:906
 msgid "> 600 px, 12 columns"
 msgstr ""
 
-#: aleksis/core/models.py:814
+#: aleksis/core/models.py:911
 msgid "Size on desktop devices"
 msgstr ""
 
-#: aleksis/core/models.py:815
+#: aleksis/core/models.py:912
 msgid "> 992 px, 12 columns"
 msgstr ""
 
-#: aleksis/core/models.py:820
+#: aleksis/core/models.py:917
 msgid "Size on large desktop devices"
 msgstr ""
 
-#: aleksis/core/models.py:821
+#: aleksis/core/models.py:918
 msgid "> 1200 px>, 12 columns"
 msgstr ""
 
-#: aleksis/core/models.py:852
+#: aleksis/core/models.py:949
 #, fuzzy
 #| msgid "Dashboard"
 msgid "Can edit default dashboard"
 msgstr "Forum"
 
-#: aleksis/core/models.py:853
+#: aleksis/core/models.py:950
 #, fuzzy
 #| msgid "Dashboard"
 msgid "Dashboard Widget"
 msgstr "Forum"
 
-#: aleksis/core/models.py:854
+#: aleksis/core/models.py:951
 #, fuzzy
 #| msgid "Dashboard"
 msgid "Dashboard Widgets"
 msgstr "Forum"
 
-#: aleksis/core/models.py:860
+#: aleksis/core/models.py:957
 msgid "URL"
 msgstr ""
 
-#: aleksis/core/models.py:861
+#: aleksis/core/models.py:958
 #, fuzzy
 #| msgid "Icon"
 msgid "Icon URL"
 msgstr "Nota"
 
-#: aleksis/core/models.py:867
+#: aleksis/core/models.py:964
 msgid "External link widget"
 msgstr ""
 
-#: aleksis/core/models.py:868
+#: aleksis/core/models.py:965
 msgid "External link widgets"
 msgstr ""
 
-#: aleksis/core/models.py:873
+#: aleksis/core/models.py:970
 #, fuzzy
 #| msgid "Dashboard"
 msgid "Dashboard widget"
 msgstr "Forum"
 
-#: aleksis/core/models.py:878
+#: aleksis/core/models.py:975
 msgid "Order"
 msgstr ""
 
-#: aleksis/core/models.py:879
+#: aleksis/core/models.py:976
 msgid "Part of the default dashboard"
 msgstr ""
 
-#: aleksis/core/models.py:894
+#: aleksis/core/models.py:991
 #, fuzzy
 #| msgid "Dashboard"
 msgid "Dashboard widget order"
 msgstr "Forum"
 
-#: aleksis/core/models.py:895
+#: aleksis/core/models.py:992
 #, fuzzy
 #| msgid "Dashboard"
 msgid "Dashboard widget orders"
 msgstr "Forum"
 
-#: aleksis/core/models.py:901
+#: aleksis/core/models.py:998
 msgid "Menu ID"
 msgstr ""
 
-#: aleksis/core/models.py:914
+#: aleksis/core/models.py:1011
 msgid "Custom menu"
 msgstr ""
 
-#: aleksis/core/models.py:915
+#: aleksis/core/models.py:1012
 msgid "Custom menus"
 msgstr ""
 
-#: aleksis/core/models.py:925
+#: aleksis/core/models.py:1022
 msgid "Menu"
 msgstr ""
 
-#: aleksis/core/models.py:929
+#: aleksis/core/models.py:1026 aleksis/core/models.py:1274
+#: aleksis/core/templates/oauth2_provider/application/detail.html:26
 msgid "Icon"
 msgstr "Nota"
 
-#: aleksis/core/models.py:935
+#: aleksis/core/models.py:1032
 msgid "Custom menu item"
 msgstr ""
 
-#: aleksis/core/models.py:936
+#: aleksis/core/models.py:1033
 msgid "Custom menu items"
 msgstr ""
 
-#: aleksis/core/models.py:950
+#: aleksis/core/models.py:1047
 msgid "Title of type"
 msgstr ""
 
-#: aleksis/core/models.py:957 aleksis/core/templates/core/group/full.html:47
+#: aleksis/core/models.py:1054 aleksis/core/templates/core/group/full.html:47
 #, fuzzy
 #| msgid "Group"
 msgid "Group type"
 msgstr "Grex"
 
-#: aleksis/core/models.py:971
+#: aleksis/core/models.py:1068
 #, fuzzy
 #| msgid "System status"
 msgid "Can view system status"
 msgstr "Status systemae"
 
-#: aleksis/core/models.py:972
+#: aleksis/core/models.py:1069
 #, fuzzy
 #| msgid "Data management"
 msgid "Can manage data"
 msgstr "Adminstratio datarum"
 
-#: aleksis/core/models.py:973
+#: aleksis/core/models.py:1070
 #, fuzzy
 #| msgid "Stop impersonation"
 msgid "Can impersonate"
 msgstr "Simulandum aliquem finire"
 
-#: aleksis/core/models.py:974
+#: aleksis/core/models.py:1071
 msgid "Can use search"
 msgstr ""
 
-#: aleksis/core/models.py:975
+#: aleksis/core/models.py:1072
 msgid "Can change site preferences"
 msgstr ""
 
-#: aleksis/core/models.py:976
+#: aleksis/core/models.py:1073
 msgid "Can change person preferences"
 msgstr ""
 
-#: aleksis/core/models.py:977
+#: aleksis/core/models.py:1074
 msgid "Can change group preferences"
 msgstr ""
 
-#: aleksis/core/models.py:978
-msgid "Can add oauth applications"
-msgstr ""
-
-#: aleksis/core/models.py:979
-msgid "Can list oauth applications"
+#: aleksis/core/models.py:1075
+msgid "Can test PDF generation"
 msgstr ""
 
-#: aleksis/core/models.py:980
+#: aleksis/core/models.py:1076
 #, fuzzy
-#| msgid "E-mail address"
-msgid "Can view oauth applications"
-msgstr "Inscriptio electronica"
-
-#: aleksis/core/models.py:981
-msgid "Can update oauth applications"
-msgstr ""
-
-#: aleksis/core/models.py:982
-msgid "Can delete oauth applications"
-msgstr ""
-
-#: aleksis/core/models.py:983
-msgid "Can test PDF generation"
-msgstr ""
+#| msgid "Stop impersonation"
+msgid "Can invite persons"
+msgstr "Simulandum aliquem finire"
 
-#: aleksis/core/models.py:1019
+#: aleksis/core/models.py:1112
 msgid "Related data check task"
 msgstr ""
 
-#: aleksis/core/models.py:1027
+#: aleksis/core/models.py:1120
 msgid "Issue solved"
 msgstr ""
 
-#: aleksis/core/models.py:1028
+#: aleksis/core/models.py:1121
 #, fuzzy
 #| msgid "Notifications"
 msgid "Notification sent"
 msgstr "Nuntii"
 
-#: aleksis/core/models.py:1041
+#: aleksis/core/models.py:1134
 msgid "Data check result"
 msgstr ""
 
-#: aleksis/core/models.py:1042
+#: aleksis/core/models.py:1135
 msgid "Data check results"
 msgstr ""
 
-#: aleksis/core/models.py:1044
+#: aleksis/core/models.py:1137
 msgid "Can run data checks"
 msgstr ""
 
-#: aleksis/core/models.py:1045
+#: aleksis/core/models.py:1138
 msgid "Can solve data check problems"
 msgstr ""
 
-#: aleksis/core/models.py:1060
+#: aleksis/core/models.py:1145
+#, fuzzy
+#| msgid "E-mail address"
+msgid "E-Mail address"
+msgstr "Inscriptio electronica"
+
+#: aleksis/core/models.py:1177
 msgid "Owner"
 msgstr ""
 
-#: aleksis/core/models.py:1064
+#: aleksis/core/models.py:1181
 msgid "File expires at"
 msgstr ""
 
-#: aleksis/core/models.py:1066
+#: aleksis/core/models.py:1183
 msgid "Generated HTML file"
 msgstr ""
 
-#: aleksis/core/models.py:1068
+#: aleksis/core/models.py:1185
 msgid "Generated PDF file"
 msgstr ""
 
-#: aleksis/core/models.py:1075
+#: aleksis/core/models.py:1192
 msgid "PDF file"
 msgstr ""
 
-#: aleksis/core/models.py:1076
+#: aleksis/core/models.py:1193
 msgid "PDF files"
 msgstr ""
 
-#: aleksis/core/models.py:1081
+#: aleksis/core/models.py:1198
 msgid "Task result"
 msgstr ""
 
-#: aleksis/core/models.py:1084
+#: aleksis/core/models.py:1201
 msgid "Task user"
 msgstr ""
 
-#: aleksis/core/models.py:1096
+#: aleksis/core/models.py:1213
 msgid "Task user assignment"
 msgstr ""
 
-#: aleksis/core/models.py:1097
+#: aleksis/core/models.py:1214
 msgid "Task user assignments"
 msgstr ""
 
-#: aleksis/core/preferences.py:22
+#: aleksis/core/models.py:1230
+#, fuzzy
+#| msgid "Additional name(s)"
+msgid "Additional attributes"
+msgstr "addita nomines"
+
+#: aleksis/core/models.py:1268
+msgid "Allowed scopes that clients can request"
+msgstr ""
+
+#: aleksis/core/models.py:1278
+msgid "This image will be shown as icon in the authorization flow. It should be squared."
+msgstr ""
+
+#: aleksis/core/preferences.py:25
 msgid "General"
 msgstr ""
 
-#: aleksis/core/preferences.py:23
+#: aleksis/core/preferences.py:26
 msgid "School"
 msgstr "Scola"
 
-#: aleksis/core/preferences.py:24
+#: aleksis/core/preferences.py:27
 msgid "Theme"
 msgstr ""
 
-#: aleksis/core/preferences.py:25
+#: aleksis/core/preferences.py:28
 msgid "Mail"
 msgstr ""
 
-#: aleksis/core/preferences.py:27
+#: aleksis/core/preferences.py:30
 msgid "Footer"
 msgstr ""
 
-#: aleksis/core/preferences.py:28
+#: aleksis/core/preferences.py:31
 #, fuzzy
 #| msgid "Data management"
 msgid "Accounts"
 msgstr "Adminstratio datarum"
 
-#: aleksis/core/preferences.py:29
+#: aleksis/core/preferences.py:32
 #, fuzzy
 #| msgid "Notifications"
 msgid "Authentication"
 msgstr "Nuntii"
 
-#: aleksis/core/preferences.py:30
+#: aleksis/core/preferences.py:33
 #, fuzzy
 #| msgid "Impersonation"
 msgid "Internationalisation"
 msgstr "Simulare aliquem"
 
-#: aleksis/core/preferences.py:41
+#: aleksis/core/preferences.py:43
 msgid "Site title"
 msgstr "Titulus paginae"
 
-#: aleksis/core/preferences.py:52
+#: aleksis/core/preferences.py:55
 msgid "Site description"
 msgstr "Descriptio paginae"
 
-#: aleksis/core/preferences.py:63
+#: aleksis/core/preferences.py:65
 msgid "Primary colour"
 msgstr ""
 
-#: aleksis/core/preferences.py:75
+#: aleksis/core/preferences.py:77
 msgid "Secondary colour"
 msgstr ""
 
-#: aleksis/core/preferences.py:86
+#: aleksis/core/preferences.py:89
 #, fuzzy
 #| msgid "Logout"
 msgid "Logo"
 msgstr "nomen retractare"
 
-#: aleksis/core/preferences.py:96
+#: aleksis/core/preferences.py:100
 msgid "Favicon"
 msgstr ""
 
-#: aleksis/core/preferences.py:106
+#: aleksis/core/preferences.py:111
 #, fuzzy
 #| msgid "Icon"
 msgid "PWA-Icon"
 msgstr "Nota"
 
-#: aleksis/core/preferences.py:117
+#: aleksis/core/preferences.py:121
+msgid "PWA-Icon is maskable"
+msgstr ""
+
+#: aleksis/core/preferences.py:133
 #, fuzzy
 #| msgid "Last name"
 msgid "Mail out name"
 msgstr "Secondus nomen"
 
-#: aleksis/core/preferences.py:128
+#: aleksis/core/preferences.py:144
 #, fuzzy
 #| msgid "E-mail address"
 msgid "Mail out address"
 msgstr "Inscriptio electronica"
 
-#: aleksis/core/preferences.py:140
+#: aleksis/core/preferences.py:157
 msgid "Link to privacy policy"
 msgstr ""
 
-#: aleksis/core/preferences.py:152
+#: aleksis/core/preferences.py:169
 msgid "Link to imprint"
 msgstr ""
 
-#: aleksis/core/preferences.py:164
+#: aleksis/core/preferences.py:180
 msgid "Name format for addressing"
 msgstr ""
 
-#: aleksis/core/preferences.py:180
+#: aleksis/core/preferences.py:197
 msgid "Channels to use for notifications"
 msgstr ""
 
-#: aleksis/core/preferences.py:192
+#: aleksis/core/preferences.py:209
 msgid "Regular expression to match primary group, e.g. '^Class .*'"
 msgstr ""
 
-#: aleksis/core/preferences.py:203
+#: aleksis/core/preferences.py:220
 msgid "Field on person to match primary group against"
 msgstr ""
 
-#: aleksis/core/preferences.py:215
+#: aleksis/core/preferences.py:232
 msgid "Automatically create new persons for new users"
 msgstr ""
 
-#: aleksis/core/preferences.py:224
+#: aleksis/core/preferences.py:241
 msgid "Automatically link existing persons to new users by their e-mail address"
 msgstr ""
 
-#: aleksis/core/preferences.py:235
+#: aleksis/core/preferences.py:252
 msgid "Display name of the school"
 msgstr ""
 
-#: aleksis/core/preferences.py:246
+#: aleksis/core/preferences.py:263
 msgid "Official name of the school, e.g. as given by supervisory authority"
 msgstr "Officialis nomen scolae, e. g."
 
-#: aleksis/core/preferences.py:254
+#: aleksis/core/preferences.py:271
 msgid "Allow users to change their passwords"
 msgstr ""
 
-#: aleksis/core/preferences.py:262
+#: aleksis/core/preferences.py:279
 msgid "Enable signup"
 msgstr ""
 
-#: aleksis/core/preferences.py:273
+#: aleksis/core/preferences.py:287
+msgid "Enable invitations"
+msgstr ""
+
+#: aleksis/core/preferences.py:295
+msgid "Length of invite code. (Default 3: abcde-acbde-abcde)"
+msgstr ""
+
+#: aleksis/core/preferences.py:303
+msgid "Size of packets. (Default 5: abcde)"
+msgstr ""
+
+#: aleksis/core/preferences.py:314
+#, fuzzy
+#| msgid "E-mail address"
+msgid "Allowed Grant Flows for OAuth applications"
+msgstr "Inscriptio electronica"
+
+#: aleksis/core/preferences.py:328
 msgid "Available languages"
 msgstr ""
 
-#: aleksis/core/preferences.py:285
+#: aleksis/core/preferences.py:341
 msgid "Send emails if data checks detect problems"
 msgstr ""
 
-#: aleksis/core/preferences.py:296
+#: aleksis/core/preferences.py:352
 msgid "Email recipients for data checks problem emails"
 msgstr ""
 
-#: aleksis/core/preferences.py:307
+#: aleksis/core/preferences.py:363
 msgid "Email recipient groups for data checks problem emails"
 msgstr ""
 
-#: aleksis/core/preferences.py:316
+#: aleksis/core/preferences.py:372
 msgid "Show dashboard to users without login"
 msgstr ""
 
-#: aleksis/core/preferences.py:325
+#: aleksis/core/preferences.py:381
 msgid "Allow users to edit their dashboard"
 msgstr ""
 
-#: aleksis/core/preferences.py:336
+#: aleksis/core/preferences.py:392
 msgid "Fields on person model which are editable by themselves."
 msgstr ""
 
-#: aleksis/core/preferences.py:350
+#: aleksis/core/preferences.py:407
 msgid "Editable fields on person model which should trigger a notification on change"
 msgstr ""
 
-#: aleksis/core/preferences.py:363
+#: aleksis/core/preferences.py:421
 msgid "Contact for notification if a person changes their data"
 msgstr ""
 
-#: aleksis/core/preferences.py:373
+#: aleksis/core/preferences.py:432
 msgid "PDF file expiration duration"
 msgstr ""
 
-#: aleksis/core/preferences.py:374
+#: aleksis/core/preferences.py:433
 msgid "in minutes"
 msgstr ""
 
-#: aleksis/core/preferences.py:384
+#: aleksis/core/preferences.py:443
 msgid "Automatically update the dashboard and its widgets"
 msgstr ""
 
-#: aleksis/core/preferences.py:394
+#: aleksis/core/preferences.py:453
 msgid "Automatically update the dashboard and its widgets sitewide"
 msgstr ""
 
-#: aleksis/core/settings.py:452
+#: aleksis/core/preferences.py:463
+msgid "Country for phone number parsing"
+msgstr ""
+
+#: aleksis/core/settings.py:529
 msgid "English"
 msgstr "Britannicus"
 
-#: aleksis/core/settings.py:453
+#: aleksis/core/settings.py:530
 msgid "German"
 msgstr "Germanus"
 
-#: aleksis/core/tables.py:19
+#: aleksis/core/tables.py:24
 #: aleksis/core/templates/core/announcement/list.html:36
 #: aleksis/core/templates/core/group/full.html:24
-#: aleksis/core/templates/core/person/full.html:23
-#: aleksis/core/templates/oauth2_provider/application_detail.html:17
+#: aleksis/core/templates/core/person/full.html:26
+#: aleksis/core/templates/core/person/full.html:98
+#: aleksis/core/templates/oauth2_provider/application/detail.html:17
 msgid "Edit"
 msgstr ""
 
-#: aleksis/core/tables.py:21 aleksis/core/tables.py:89
+#: aleksis/core/tables.py:26 aleksis/core/tables.py:94
+#: aleksis/core/tables.py:138
 #: aleksis/core/templates/core/announcement/list.html:22
 #, fuzzy
 #| msgid "Notifications"
 msgid "Actions"
 msgstr "Nuntii"
 
-#: aleksis/core/tables.py:56 aleksis/core/tables.py:57
-#: aleksis/core/tables.py:71 aleksis/core/tables.py:87
+#: aleksis/core/tables.py:61 aleksis/core/tables.py:62
+#: aleksis/core/tables.py:76 aleksis/core/tables.py:92
+#: aleksis/core/tables.py:136
 #: aleksis/core/templates/core/announcement/list.html:42
 #: aleksis/core/templates/core/group/full.html:31
 #: aleksis/core/templates/core/pages/delete.html:22
-#: aleksis/core/templates/core/person/full.html:30
-#: aleksis/core/templates/oauth2_provider/application_confirm_delete.html:20
-#: aleksis/core/templates/oauth2_provider/application_detail.html:21
+#: aleksis/core/templates/core/person/full.html:33
+#: aleksis/core/templates/core/person/full.html:105
+#: aleksis/core/templates/oauth2_provider/application/detail.html:21
 msgid "Delete"
 msgstr ""
 
-#: aleksis/core/templates/403.html:14 aleksis/core/templates/404.html:10
-#: aleksis/core/templates/500.html:10
-#: aleksis/core/templates/oauth2_provider/authorize.html:47
+#: aleksis/core/templates/403.html:14 aleksis/core/templates/500.html:10
+#: aleksis/core/templates/oauth2_provider/authorize.html:54
 #: aleksis/core/templates/oauth2_provider/authorized-oob.html:24
 msgid "Error"
 msgstr ""
@@ -1240,7 +1340,7 @@ msgid ""
 "          object."
 msgstr ""
 
-#: aleksis/core/templates/403.html:19 aleksis/core/templates/404.html:17
+#: aleksis/core/templates/403.html:19 aleksis/core/templates/404.html:16
 msgid ""
 "\n"
 "            If you think this is an error in AlekSIS, please contact your site\n"
@@ -1248,13 +1348,7 @@ msgid ""
 "          "
 msgstr ""
 
-#: aleksis/core/templates/404.html:10
-msgid ""
-"The requested page or object was not\n"
-"          found."
-msgstr ""
-
-#: aleksis/core/templates/404.html:13
+#: aleksis/core/templates/404.html:12
 msgid ""
 "\n"
 "            If you were redirected by a link on an external page,\n"
@@ -1294,11 +1388,11 @@ msgstr ""
 msgid "Account inactive"
 msgstr ""
 
-#: aleksis/core/templates/account/account_inactive.html:13
+#: aleksis/core/templates/account/account_inactive.html:14
 msgid "Account inactive."
 msgstr ""
 
-#: aleksis/core/templates/account/account_inactive.html:15
+#: aleksis/core/templates/account/account_inactive.html:17
 msgid ""
 "\n"
 "            This account is currently inactive. If you think this is an\n"
@@ -1311,14 +1405,13 @@ msgid "Hello!"
 msgstr ""
 
 #: aleksis/core/templates/account/email/base_message.txt:9
-#: aleksis/core/templates/templated_email/notification.email:22
-#: aleksis/core/templates/templated_email/notification.email:46
 msgid "Your AlekSIS team"
 msgstr ""
 
 #: aleksis/core/templates/account/email_confirm.html:5
 #: aleksis/core/templates/account/email_confirm.html:6
 #: aleksis/core/templates/account/email_confirm.html:17
+#: aleksis/core/templates/socialaccount/login.html:17
 #, fuzzy
 #| msgid "Notification"
 msgid "Confirm"
@@ -1365,7 +1458,7 @@ msgstr ""
 #: aleksis/core/templates/account/password_reset_done.html:5
 #: aleksis/core/templates/account/verification_email_required.html:5
 #: aleksis/core/templates/account/verification_email_required.html:6
-#: aleksis/core/templates/two_factor/core/login.html:81
+#: aleksis/core/templates/two_factor/core/login.html:100
 msgid "Reset password"
 msgstr ""
 
@@ -1452,11 +1545,11 @@ msgstr ""
 msgid "Signup closed"
 msgstr ""
 
-#: aleksis/core/templates/account/signup_closed.html:13
+#: aleksis/core/templates/account/signup_closed.html:14
 msgid "Signup closed."
 msgstr ""
 
-#: aleksis/core/templates/account/signup_closed.html:15
+#: aleksis/core/templates/account/signup_closed.html:17
 msgid ""
 "\n"
 "            This sign up is currently closed. If you think this is an\n"
@@ -1496,11 +1589,6 @@ msgid ""
 "          "
 msgstr ""
 
-#: aleksis/core/templates/account/verification_sent.html:30
-#, python-format
-msgid "<strong>Note:</strong> you can still <a href=\"%(email_url)s\">change your e-mail address</a>"
-msgstr ""
-
 #: aleksis/core/templates/core/additional_field/edit.html:6
 #: aleksis/core/templates/core/additional_field/edit.html:7
 #, fuzzy
@@ -1558,24 +1646,24 @@ msgstr ""
 msgid "There are no announcements."
 msgstr "Scribe nuntium:"
 
-#: aleksis/core/templates/core/base.html:60
+#: aleksis/core/templates/core/base.html:78
 msgid "Logged in as"
 msgstr ""
 
-#: aleksis/core/templates/core/base.html:154
-msgid "About AlekSIS — The Free School Information System"
+#: aleksis/core/templates/core/base.html:179
+msgid "About AlekSIS® — The Free School Information System"
 msgstr ""
 
-#: aleksis/core/templates/core/base.html:162
-msgid "Impress"
+#: aleksis/core/templates/core/base.html:187
+msgid "Imprint"
 msgstr ""
 
-#: aleksis/core/templates/core/base.html:170
+#: aleksis/core/templates/core/base.html:195
 msgid "Privacy Policy"
 msgstr ""
 
-#: aleksis/core/templates/core/base_print.html:72
-msgid "Powered by AlekSIS"
+#: aleksis/core/templates/core/base_print.html:74
+msgid "Powered by AlekSIS®"
 msgstr ""
 
 #: aleksis/core/templates/core/dashboard_widget/create.html:8
@@ -1666,17 +1754,17 @@ msgstr ""
 msgid "Options to solve the problem"
 msgstr ""
 
-#: aleksis/core/templates/core/data_check/list.html:62
+#: aleksis/core/templates/core/data_check/list.html:63
 msgid "Show object"
 msgstr ""
 
-#: aleksis/core/templates/core/data_check/list.html:84
+#: aleksis/core/templates/core/data_check/list.html:86
 #, fuzzy
 #| msgid "System status"
 msgid "Registered checks"
 msgstr "Status systemae"
 
-#: aleksis/core/templates/core/data_check/list.html:88
+#: aleksis/core/templates/core/data_check/list.html:90
 msgid ""
 "\n"
 "            The system will check for the following problems:\n"
@@ -1775,7 +1863,7 @@ msgstr ""
 
 #: aleksis/core/templates/core/group/child_groups.html:93
 #: aleksis/core/templates/core/group/child_groups.html:128
-#: aleksis/core/templates/oauth2_provider/application_detail.html:9
+#: aleksis/core/templates/oauth2_provider/application/detail.html:9
 #: aleksis/core/templates/two_factor/_wizard_actions.html:15
 #: aleksis/core/templates/two_factor/_wizard_actions.html:20
 msgid "Back"
@@ -1804,7 +1892,8 @@ msgid "Edit group"
 msgstr ""
 
 #: aleksis/core/templates/core/group/full.html:38
-#: aleksis/core/templates/core/person/full.html:37
+#: aleksis/core/templates/core/person/full.html:40
+#: aleksis/core/templates/core/person/full.html:112
 msgid "Change preferences"
 msgstr ""
 
@@ -1903,38 +1992,48 @@ msgid "No notifications available yet."
 msgstr ""
 
 #: aleksis/core/templates/core/pages/about.html:6
-#: aleksis/core/templates/core/pages/about.html:15
-msgid "About AlekSIS"
+msgid "About AlekSIS®"
 msgstr ""
 
 #: aleksis/core/templates/core/pages/about.html:7
-msgid "AlekSIS – The Free School Information System"
+msgid "AlekSIS® – The Free School Information System"
+msgstr ""
+
+#: aleksis/core/templates/core/pages/about.html:15
+msgid "About AlekSIS"
 msgstr ""
 
 #: aleksis/core/templates/core/pages/about.html:17
 msgid ""
 "\n"
-"              This platform is powered by AlekSIS, a web-based school information system (SIS) which can be used\n"
+"              This platform is powered by AlekSIS®, a web-based school information system (SIS) which can be used\n"
 "              to manage and/or publish organisational artifacts of educational institutions. AlekSIS is free software and\n"
 "              can be used by anyone.\n"
 "            "
 msgstr ""
 
-#: aleksis/core/templates/core/pages/about.html:25
+#: aleksis/core/templates/core/pages/about.html:24
+msgid ""
+"\n"
+"              AlekSIS® is a registered trademark of the AlekSIS open source project, represented by Teckids e.V.\n"
+"            "
+msgstr ""
+
+#: aleksis/core/templates/core/pages/about.html:30
 msgid "Website of AlekSIS"
 msgstr ""
 
-#: aleksis/core/templates/core/pages/about.html:26
+#: aleksis/core/templates/core/pages/about.html:31
 msgid "Source code"
 msgstr ""
 
-#: aleksis/core/templates/core/pages/about.html:35
+#: aleksis/core/templates/core/pages/about.html:40
 #, fuzzy
 #| msgid "Edit school information"
 msgid "Licence information"
 msgstr "Muta informationes scolae"
 
-#: aleksis/core/templates/core/pages/about.html:37
+#: aleksis/core/templates/core/pages/about.html:42
 msgid ""
 "\n"
 "              The core and the official apps of AlekSIS are licenced under the EUPL, version 1.2 or later. For licence\n"
@@ -1943,25 +2042,25 @@ msgid ""
 "            "
 msgstr ""
 
-#: aleksis/core/templates/core/pages/about.html:45
+#: aleksis/core/templates/core/pages/about.html:50
 msgid "Free/Open Source Licence"
 msgstr ""
 
-#: aleksis/core/templates/core/pages/about.html:46
+#: aleksis/core/templates/core/pages/about.html:51
 msgid "Other Licence"
 msgstr ""
 
-#: aleksis/core/templates/core/pages/about.html:50
+#: aleksis/core/templates/core/pages/about.html:55
 msgid "Full licence text"
 msgstr ""
 
-#: aleksis/core/templates/core/pages/about.html:51
+#: aleksis/core/templates/core/pages/about.html:56
 #, fuzzy
 #| msgid "Edit school information"
 msgid "More information about the EUPL"
 msgstr "Muta informationes scolae"
 
-#: aleksis/core/templates/core/pages/about.html:90
+#: aleksis/core/templates/core/pages/about.html:95
 #, python-format
 msgid ""
 "\n"
@@ -2071,6 +2170,8 @@ msgid "Celery task results"
 msgstr ""
 
 #: aleksis/core/templates/core/pages/system_status.html:112
+#: aleksis/core/templates/templated_email/celery_failure.email:9
+#: aleksis/core/templates/templated_email/celery_failure.email:28
 msgid "Task"
 msgstr ""
 
@@ -2154,6 +2255,53 @@ msgid ""
 "          "
 msgstr ""
 
+#: aleksis/core/templates/core/perms/assign.html:12
+#: aleksis/core/templates/core/perms/assign.html:13
+msgid "Assign permission"
+msgstr ""
+
+#: aleksis/core/templates/core/perms/assign.html:17
+#, fuzzy
+#| msgid "Stop impersonation"
+msgid "Selected permission"
+msgstr "Simulandum aliquem finire"
+
+#: aleksis/core/templates/core/perms/assign.html:26
+msgid "Assign"
+msgstr ""
+
+#: aleksis/core/templates/core/perms/list.html:21
+msgid "Assign a new permission"
+msgstr ""
+
+#: aleksis/core/templates/core/perms/list.html:25
+msgid "Select"
+msgstr ""
+
+#: aleksis/core/templates/core/perms/list.html:34
+msgid "Global (user)"
+msgstr ""
+
+#: aleksis/core/templates/core/perms/list.html:38
+msgid "Global (group)"
+msgstr ""
+
+#: aleksis/core/templates/core/perms/list.html:42
+msgid "Object (user)"
+msgstr ""
+
+#: aleksis/core/templates/core/perms/list.html:46
+msgid "Object (group)"
+msgstr ""
+
+#: aleksis/core/templates/core/perms/list.html:52
+msgid "Filter permissions"
+msgstr ""
+
+#: aleksis/core/templates/core/perms/list.html:58
+msgid "Update"
+msgstr ""
+
 #: aleksis/core/templates/core/person/create.html:12
 #: aleksis/core/templates/core/person/create.html:13
 #: aleksis/core/templates/core/person/list.html:17
@@ -2167,19 +2315,29 @@ msgstr "Simulandum aliquem finire"
 msgid "Edit person"
 msgstr ""
 
-#: aleksis/core/templates/core/person/full.html:44
-#: aleksis/core/templates/impersonate/list_users.html:7
-#: aleksis/core/templates/impersonate/list_users.html:8
+#: aleksis/core/templates/core/person/full.html:47
+#: aleksis/core/templates/core/person/full.html:119
 #, fuzzy
 #| msgid "Impersonation"
 msgid "Impersonate"
 msgstr "Simulare aliquem"
 
-#: aleksis/core/templates/core/person/full.html:50
+#: aleksis/core/templates/core/person/full.html:54
+#: aleksis/core/templates/core/person/full.html:126
+#, fuzzy
+#| msgid "Impersonation"
+msgid "Invite user"
+msgstr "Simulare aliquem"
+
+#: aleksis/core/templates/core/person/full.html:133
 msgid "Contact details"
 msgstr ""
 
-#: aleksis/core/templates/core/person/full.html:130
+#: aleksis/core/templates/core/person/full.html:224
+msgid "This person didn't upload a personal photo."
+msgstr ""
+
+#: aleksis/core/templates/core/person/full.html:232
 msgid "Children"
 msgstr ""
 
@@ -2221,81 +2379,151 @@ msgstr ""
 msgid "Save preferences"
 msgstr ""
 
-#: aleksis/core/templates/oauth2_provider/application_confirm_delete.html:5
-#: aleksis/core/templates/oauth2_provider/application_confirm_delete.html:6
-msgid "Delete application"
+#: aleksis/core/templates/invitations/disabled.html:5
+msgid "The invite feature is disabled"
+msgstr ""
+
+#: aleksis/core/templates/invitations/disabled.html:13
+msgid "The invite feature is disabled."
+msgstr ""
+
+#: aleksis/core/templates/invitations/disabled.html:15
+msgid "To enable it, switch on the corresponding checkbox in the authentication section of the "
+msgstr ""
+
+#: aleksis/core/templates/invitations/disabled.html:16
+msgid "site preferences page"
+msgstr ""
+
+#: aleksis/core/templates/invitations/enter.html:21
+msgid "Accept your invitation"
+msgstr ""
+
+#: aleksis/core/templates/invitations/enter.html:25
+msgid ""
+"\n"
+"                Please enter your invitation code to register\n"
+"                your new user account:\n"
+"              "
+msgstr ""
+
+#: aleksis/core/templates/invitations/enter.html:37
+msgid "Accept invite"
+msgstr ""
+
+#: aleksis/core/templates/invitations/forms/_invite.html:9
+#: aleksis/core/templates/invitations/forms/_invite.html:10
+#: aleksis/core/templates/invitations/forms/_invite.html:21
+msgid "Invite"
+msgstr ""
+
+#: aleksis/core/templates/invitations/forms/_invite.html:17
+#, fuzzy
+#| msgid "E-mail address"
+msgid "Invite by email address"
+msgstr "Inscriptio electronica"
+
+#: aleksis/core/templates/invitations/forms/_invite.html:26
+msgid "Generate invitation code"
+msgstr ""
+
+#: aleksis/core/templates/invitations/forms/_invite.html:29
+msgid "Generate code"
 msgstr ""
 
-#: aleksis/core/templates/oauth2_provider/application_confirm_delete.html:12
+#: aleksis/core/templates/invitations/forms/_invite.html:33
+msgid "Invitations"
+msgstr ""
+
+#: aleksis/core/templates/invitations/messages/invite_accepted.txt:3
 #, python-format
-msgid "Are you sure to delete the application %(application_name)s?"
+msgid "The invitation for %(email)s has been accepted."
 msgstr ""
 
-#: aleksis/core/templates/oauth2_provider/application_confirm_delete.html:24
-#: aleksis/core/templates/oauth2_provider/application_form.html:18
+#: aleksis/core/templates/oauth2_provider/application/create.html:5
+#: aleksis/core/templates/oauth2_provider/application/create.html:6
+#, fuzzy
+#| msgid "Notifications"
+msgid "Register OAuth2 Application"
+msgstr "Nuntii"
+
+#: aleksis/core/templates/oauth2_provider/application/create.html:14
+#: aleksis/core/templates/oauth2_provider/application/edit.html:14
 #: aleksis/core/templates/oauth2_provider/authorized-token-delete.html:24
 #: aleksis/core/templates/two_factor/_wizard_actions.html:6
 msgid "Cancel"
 msgstr ""
 
-#: aleksis/core/templates/oauth2_provider/application_detail.html:27
+#: aleksis/core/templates/oauth2_provider/application/detail.html:5
+#, fuzzy
+#| msgid "Notifications"
+msgid "OAuth2 Application"
+msgstr "Nuntii"
+
+#: aleksis/core/templates/oauth2_provider/application/detail.html:39
 msgid "Client id"
 msgstr ""
 
-#: aleksis/core/templates/oauth2_provider/application_detail.html:35
+#: aleksis/core/templates/oauth2_provider/application/detail.html:47
 msgid "Client secret"
 msgstr ""
 
-#: aleksis/core/templates/oauth2_provider/application_detail.html:43
+#: aleksis/core/templates/oauth2_provider/application/detail.html:55
 msgid "Client type"
 msgstr ""
 
-#: aleksis/core/templates/oauth2_provider/application_detail.html:51
-msgid "Authorization Grant Type"
+#: aleksis/core/templates/oauth2_provider/application/detail.html:63
+msgid "Allowed scopes"
 msgstr ""
 
-#: aleksis/core/templates/oauth2_provider/application_detail.html:59
+#: aleksis/core/templates/oauth2_provider/application/detail.html:71
 msgid "Redirect URIs"
 msgstr ""
 
-#: aleksis/core/templates/oauth2_provider/application_form.html:5
-msgid "Create OAuth2 Application"
-msgstr ""
-
-#: aleksis/core/templates/oauth2_provider/application_form.html:10
+#: aleksis/core/templates/oauth2_provider/application/detail.html:79
 #, fuzzy
-#| msgid "Edit school information"
-msgid "Edit application"
-msgstr "Muta informationes scolae"
+#| msgid "Notifications"
+msgid "Skip Authorisation"
+msgstr "Nuntii"
 
-#: aleksis/core/templates/oauth2_provider/application_list.html:8
+#: aleksis/core/templates/oauth2_provider/application/edit.html:5
+#: aleksis/core/templates/oauth2_provider/application/edit.html:6
 #, fuzzy
 #| msgid "Notifications"
-msgid "OAuth2 applications"
+msgid "Edit OAuth2 Application"
 msgstr "Nuntii"
 
-#: aleksis/core/templates/oauth2_provider/application_list.html:12
-msgid "Register new applications"
-msgstr ""
+#: aleksis/core/templates/oauth2_provider/application/list.html:11
+#, fuzzy
+#| msgid "Edit school information"
+msgid "Register new application"
+msgstr "Muta informationes scolae"
 
-#: aleksis/core/templates/oauth2_provider/application_list.html:23
+#: aleksis/core/templates/oauth2_provider/application/list.html:25
 msgid "No applications defined."
 msgstr ""
 
 #: aleksis/core/templates/oauth2_provider/authorize.html:5
-#: aleksis/core/templates/oauth2_provider/authorize.html:16
+#: aleksis/core/templates/socialaccount/login.html:5
+#: aleksis/core/templates/socialaccount/login.html:6
 msgid "Authorize"
 msgstr ""
 
-#: aleksis/core/templates/oauth2_provider/authorize.html:18
+#: aleksis/core/templates/oauth2_provider/authorize.html:23
+#, fuzzy, python-format
+#| msgid "Stop impersonation"
+msgid "Authorize %(name)s"
+msgstr "Simulandum aliquem finire"
+
+#: aleksis/core/templates/oauth2_provider/authorize.html:25
 msgid "The application requests access to the following scopes:"
 msgstr ""
 
-#: aleksis/core/templates/oauth2_provider/authorize.html:33
+#: aleksis/core/templates/oauth2_provider/authorize.html:40
 msgid "Allow"
 msgstr ""
 
-#: aleksis/core/templates/oauth2_provider/authorize.html:36
+#: aleksis/core/templates/oauth2_provider/authorize.html:43
 msgid "Disallow"
 msgstr ""
 
@@ -2402,6 +2630,20 @@ msgstr ""
 msgid "Add a Third-party Account"
 msgstr ""
 
+#: aleksis/core/templates/socialaccount/login.html:12
+#, python-format
+msgid "You are about to connect a new third party account from %(provider)s."
+msgstr ""
+
+#: aleksis/core/templates/socialaccount/login.html:23
+#, python-format
+msgid "You are about to sign in using a third party account from %(provider)s."
+msgstr ""
+
+#: aleksis/core/templates/socialaccount/login.html:28
+msgid "Continue"
+msgstr ""
+
 #: aleksis/core/templates/socialaccount/login_cancelled.html:5
 #: aleksis/core/templates/socialaccount/login_cancelled.html:6
 #: aleksis/core/templates/socialaccount/login_cancelled.html:13
@@ -2446,26 +2688,64 @@ msgid ""
 "        "
 msgstr ""
 
-#: aleksis/core/templates/templated_email/data_checks.email:4
-msgid "The system detected some new problems with your data."
+#: aleksis/core/templates/templated_email/base.email:5
+#: aleksis/core/templates/templated_email/base.email:16
+msgid "Hello"
 msgstr ""
 
-#: aleksis/core/templates/templated_email/data_checks.email:8
-#: aleksis/core/templates/templated_email/data_checks.email:24
-#: aleksis/core/templates/templated_email/person_changed.email:8
-#: aleksis/core/templates/templated_email/person_changed.email:20
-msgid "Hello,"
+#: aleksis/core/templates/templated_email/celery_failure.email:4
+#, python-format
+msgid "Celery task %(task_name)s failed!"
+msgstr ""
+
+#: aleksis/core/templates/templated_email/celery_failure.email:7
+#, python-format
+msgid "the celery task %(task_name)s failed with following information:"
+msgstr ""
+
+#: aleksis/core/templates/templated_email/celery_failure.email:10
+#: aleksis/core/templates/templated_email/celery_failure.email:29
+msgid "Task ID"
+msgstr ""
+
+#: aleksis/core/templates/templated_email/celery_failure.email:11
+#: aleksis/core/templates/templated_email/celery_failure.email:30
+#, fuzzy
+#| msgid "Site description"
+msgid "Raised exception"
+msgstr "Descriptio paginae"
+
+#: aleksis/core/templates/templated_email/celery_failure.email:12
+#: aleksis/core/templates/templated_email/celery_failure.email:31
+#, fuzzy
+#| msgid "Additional name(s)"
+msgid "Positional arguments"
+msgstr "addita nomines"
+
+#: aleksis/core/templates/templated_email/celery_failure.email:15
+#: aleksis/core/templates/templated_email/celery_failure.email:38
+msgid "Keyword arguments"
 msgstr ""
 
-#: aleksis/core/templates/templated_email/data_checks.email:10
+#: aleksis/core/templates/templated_email/celery_failure.email:22
+#, python-format
 msgid ""
 "\n"
-"  the system detected some new problems with your data.\n"
-"  Please take some time to inspect them and solve the issues or mark them as ignored.\n"
-" "
+"      the celery task %(task_name)s failed with following information:\n"
+"    "
+msgstr ""
+
+#: aleksis/core/templates/templated_email/data_checks.email:3
+msgid "The system detected some new problems with your data."
+msgstr ""
+
+#: aleksis/core/templates/templated_email/data_checks.email:6
+msgid ""
+"the system detected some new problems with your data.\n"
+"Please take some time to inspect them and solve the issues or mark them as ignored."
 msgstr ""
 
-#: aleksis/core/templates/templated_email/data_checks.email:26
+#: aleksis/core/templates/templated_email/data_checks.email:15
 msgid ""
 "\n"
 "   the system detected some new problems with your data.\n"
@@ -2473,52 +2753,49 @@ msgid ""
 "  "
 msgstr ""
 
-#: aleksis/core/templates/templated_email/data_checks.email:34
+#: aleksis/core/templates/templated_email/data_checks.email:23
 #, fuzzy
 #| msgid "Site description"
 msgid "Problem description"
 msgstr "Descriptio paginae"
 
-#: aleksis/core/templates/templated_email/data_checks.email:35
+#: aleksis/core/templates/templated_email/data_checks.email:24
 msgid "Count of objects with new problems"
 msgstr ""
 
-#: aleksis/core/templates/templated_email/notification.email:3
+#: aleksis/core/templates/templated_email/notification.email:4
 #, fuzzy
 #| msgid "Notification"
 msgid "New notification for"
 msgstr "Nuntius"
 
 #: aleksis/core/templates/templated_email/notification.email:6
-#: aleksis/core/templates/templated_email/notification.email:27
+#: aleksis/core/templates/templated_email/notification.email:20
 #, fuzzy, python-format
 #| msgid "Notifications"
-msgid "Dear %(notification_user)s,"
+msgid "Hello %(notification_user)s,"
 msgstr "Nuntii"
 
-#: aleksis/core/templates/templated_email/notification.email:8
-#: aleksis/core/templates/templated_email/notification.email:29
+#: aleksis/core/templates/templated_email/notification.email:9
+#: aleksis/core/templates/templated_email/notification.email:23
 #, fuzzy
 #| msgid "Notification"
 msgid "we got a new notification for you:"
 msgstr "Nuntius"
 
 #: aleksis/core/templates/templated_email/notification.email:15
-#: aleksis/core/templates/templated_email/notification.email:35
+#: aleksis/core/templates/templated_email/notification.email:29
 #, fuzzy
 #| msgid "Edit school information"
 msgid "More information"
 msgstr "Muta informationes scolae"
 
-#: aleksis/core/templates/templated_email/notification.email:18
+#: aleksis/core/templates/templated_email/notification.email:17
 #, python-format
-msgid ""
-"\n"
-"        Sent by %(trans_sender)s at %(trans_created_at)s\n"
-"    "
+msgid "Sent by %(trans_sender)s at %(trans_created_at)s"
 msgstr ""
 
-#: aleksis/core/templates/templated_email/notification.email:40
+#: aleksis/core/templates/templated_email/notification.email:34
 #, python-format
 msgid ""
 "\n"
@@ -2531,15 +2808,12 @@ msgstr ""
 msgid "%(person)s changed their data!"
 msgstr ""
 
-#: aleksis/core/templates/templated_email/person_changed.email:10
+#: aleksis/core/templates/templated_email/person_changed.email:7
 #, python-format
-msgid ""
-"\n"
-"   the person %(person)s recently changed the following fields:\n"
-" "
+msgid "the person %(person)s recently changed the following fields:"
 msgstr ""
 
-#: aleksis/core/templates/templated_email/person_changed.email:22
+#: aleksis/core/templates/templated_email/person_changed.email:15
 #, python-format
 msgid ""
 "\n"
@@ -2590,21 +2864,31 @@ msgstr ""
 msgid "Generate Tokens"
 msgstr ""
 
-#: aleksis/core/templates/two_factor/core/login.html:20
+#: aleksis/core/templates/two_factor/core/login.html:27
+#, fuzzy, python-format
+#| msgid "Stop impersonation"
+msgid "Login for %(name)s"
+msgstr "Simulandum aliquem finire"
+
+#: aleksis/core/templates/two_factor/core/login.html:30
 msgid "Login with username and password"
 msgstr ""
 
-#: aleksis/core/templates/two_factor/core/login.html:28
+#: aleksis/core/templates/two_factor/core/login.html:38
 msgid ""
 "You have no permission to view this page. Please login with an other\n"
 "                    account."
 msgstr ""
 
-#: aleksis/core/templates/two_factor/core/login.html:36
+#: aleksis/core/templates/two_factor/core/login.html:47
+msgid "Please login with your account to use the external application."
+msgstr ""
+
+#: aleksis/core/templates/two_factor/core/login.html:54
 msgid "Please login to see this page."
 msgstr ""
 
-#: aleksis/core/templates/two_factor/core/login.html:46
+#: aleksis/core/templates/two_factor/core/login.html:65
 msgid ""
 "\n"
 "                        We are calling your phone right now, please enter the\n"
@@ -2612,7 +2896,7 @@ msgid ""
 "                      "
 msgstr ""
 
-#: aleksis/core/templates/two_factor/core/login.html:51
+#: aleksis/core/templates/two_factor/core/login.html:70
 msgid ""
 "\n"
 "                        We sent you a text message, please enter the tokens we\n"
@@ -2620,7 +2904,7 @@ msgid ""
 "                      "
 msgstr ""
 
-#: aleksis/core/templates/two_factor/core/login.html:56
+#: aleksis/core/templates/two_factor/core/login.html:75
 msgid ""
 "\n"
 "                        Please enter the tokens generated by your token\n"
@@ -2628,7 +2912,7 @@ msgid ""
 "                      "
 msgstr ""
 
-#: aleksis/core/templates/two_factor/core/login.html:62
+#: aleksis/core/templates/two_factor/core/login.html:81
 msgid ""
 "\n"
 "                      Use this form for entering backup tokens for logging in.\n"
@@ -2637,23 +2921,23 @@ msgid ""
 "                    "
 msgstr ""
 
-#: aleksis/core/templates/two_factor/core/login.html:90
+#: aleksis/core/templates/two_factor/core/login.html:109
 msgid "Device currently not available?"
 msgstr ""
 
-#: aleksis/core/templates/two_factor/core/login.html:92
+#: aleksis/core/templates/two_factor/core/login.html:111
 msgid "Or, alternatively, use one of your backup phones:"
 msgstr ""
 
-#: aleksis/core/templates/two_factor/core/login.html:102
+#: aleksis/core/templates/two_factor/core/login.html:121
 msgid "As a last resort, you can use a backup token:"
 msgstr ""
 
-#: aleksis/core/templates/two_factor/core/login.html:105
+#: aleksis/core/templates/two_factor/core/login.html:124
 msgid "Use Backup Token"
 msgstr ""
 
-#: aleksis/core/templates/two_factor/core/login.html:116
+#: aleksis/core/templates/two_factor/core/login.html:135
 msgid "Use alternative login options"
 msgstr ""
 
@@ -2890,141 +3174,213 @@ msgid ""
 "      "
 msgstr ""
 
-#: aleksis/core/util/notifications.py:65
+#: aleksis/core/util/notifications.py:63
 msgid "E-Mail"
 msgstr ""
 
-#: aleksis/core/util/notifications.py:66
+#: aleksis/core/util/notifications.py:64
 msgid "SMS"
 msgstr ""
 
-#: aleksis/core/util/pdf.py:105
+#: aleksis/core/util/pdf.py:118
 msgid "Progress: Generate PDF file"
 msgstr ""
 
-#: aleksis/core/util/pdf.py:106
+#: aleksis/core/util/pdf.py:119
 msgid "Generating PDF file …"
 msgstr ""
 
-#: aleksis/core/util/pdf.py:107
+#: aleksis/core/util/pdf.py:120
 msgid "The PDF file has been generated successfully."
 msgstr ""
 
-#: aleksis/core/util/pdf.py:108
+#: aleksis/core/util/pdf.py:121
 msgid "There was a problem while generating the PDF file."
 msgstr ""
 
-#: aleksis/core/util/pdf.py:111
+#: aleksis/core/util/pdf.py:124
 msgid "Download PDF"
 msgstr ""
 
-#: aleksis/core/views.py:251
+#: aleksis/core/views.py:285
 msgid "The school term has been created."
 msgstr ""
 
-#: aleksis/core/views.py:263
+#: aleksis/core/views.py:297
 msgid "The school term has been saved."
 msgstr ""
 
-#: aleksis/core/views.py:387
+#: aleksis/core/views.py:417
 msgid "The child groups were successfully saved."
 msgstr ""
 
-#: aleksis/core/views.py:406 aleksis/core/views.py:416
+#: aleksis/core/views.py:436 aleksis/core/views.py:446
 msgid "The person has been saved."
 msgstr ""
 
-#: aleksis/core/views.py:466
+#: aleksis/core/views.py:496
 msgid "The group has been saved."
 msgstr ""
 
-#: aleksis/core/views.py:563
+#: aleksis/core/views.py:593
 msgid "The announcement has been saved."
 msgstr ""
 
-#: aleksis/core/views.py:579
+#: aleksis/core/views.py:609
 msgid "The announcement has been deleted."
 msgstr ""
 
-#: aleksis/core/views.py:663
+#: aleksis/core/views.py:677
+msgid "The requested preference registry does not exist"
+msgstr ""
+
+#: aleksis/core/views.py:696
 msgid "The preferences have been saved successfully."
 msgstr ""
 
-#: aleksis/core/views.py:687
+#: aleksis/core/views.py:720
 msgid "The person has been deleted."
 msgstr ""
 
-#: aleksis/core/views.py:701
+#: aleksis/core/views.py:734
 msgid "The group has been deleted."
 msgstr ""
 
-#: aleksis/core/views.py:733
+#: aleksis/core/views.py:766
 msgid "The additional_field has been saved."
 msgstr ""
 
-#: aleksis/core/views.py:767
+#: aleksis/core/views.py:800
 msgid "The additional field has been deleted."
 msgstr ""
 
-#: aleksis/core/views.py:792
+#: aleksis/core/views.py:825
 msgid "The group type has been saved."
 msgstr ""
 
-#: aleksis/core/views.py:822
+#: aleksis/core/views.py:855
 msgid "The group type has been deleted."
 msgstr ""
 
-#: aleksis/core/views.py:855
+#: aleksis/core/views.py:888
 msgid "Progress: Run data checks"
 msgstr ""
 
-#: aleksis/core/views.py:856
+#: aleksis/core/views.py:889
 #, fuzzy
 #| msgid "System status"
 msgid "Run data checks …"
 msgstr "Status systemae"
 
-#: aleksis/core/views.py:857
+#: aleksis/core/views.py:890
 msgid "The data checks were run successfully."
 msgstr ""
 
-#: aleksis/core/views.py:858
+#: aleksis/core/views.py:891
 msgid "There was a problem while running data checks."
 msgstr ""
 
-#: aleksis/core/views.py:874
+#: aleksis/core/views.py:907
 #, python-brace-format
 msgid "The solve option '{solve_option_obj.verbose_name}' "
 msgstr ""
 
-#: aleksis/core/views.py:916
+#: aleksis/core/views.py:917
+msgid "The requested solve option does not exist"
+msgstr ""
+
+#: aleksis/core/views.py:949
 msgid "The dashboard widget has been saved."
 msgstr ""
 
-#: aleksis/core/views.py:946
+#: aleksis/core/views.py:979
 msgid "The dashboard widget has been created."
 msgstr ""
 
-#: aleksis/core/views.py:956
+#: aleksis/core/views.py:989
 msgid "The dashboard widget has been deleted."
 msgstr ""
 
-#: aleksis/core/views.py:1023
+#: aleksis/core/views.py:1060
 msgid "Your dashboard configuration has been saved successfully."
 msgstr ""
 
-#: aleksis/core/views.py:1025
+#: aleksis/core/views.py:1062
 msgid "The configuration of the default dashboard has been saved successfully."
 msgstr ""
 
-#: aleksis/core/views.py:1153
+#: aleksis/core/views.py:1138
+#, python-brace-format
+msgid "The invitation was successfully created. The invitation code is {code}"
+msgstr ""
+
+#: aleksis/core/views.py:1229
+msgid "We have successfully assigned the permissions."
+msgstr ""
+
+#: aleksis/core/views.py:1239
+msgid "The global user permission has been deleted."
+msgstr ""
+
+#: aleksis/core/views.py:1249
+msgid "The global group permission has been deleted."
+msgstr ""
+
+#: aleksis/core/views.py:1259
+msgid "The object user permission has been deleted."
+msgstr ""
+
+#: aleksis/core/views.py:1269
+msgid "The object group permission has been deleted."
+msgstr ""
+
+#: aleksis/core/views.py:1337
+msgid "The requested PDF file does not exist"
+msgstr ""
+
+#: aleksis/core/views.py:1346 aleksis/core/views.py:1350
+msgid "The requested task does not exist or is not accessible"
+msgstr ""
+
+#: aleksis/core/views.py:1388
 msgid "The third-party account could not be disconnected because it is the only login method available."
 msgstr ""
 
-#: aleksis/core/views.py:1160
+#: aleksis/core/views.py:1395
 msgid "The third-party account has been successfully disconnected."
 msgstr ""
 
+#: aleksis/core/views.py:1466
+msgid "Person was invited successfully and an email with further instructions has been send to them."
+msgstr ""
+
+#: aleksis/core/views.py:1477
+msgid "Person was already invited."
+msgstr ""
+
+#, fuzzy
+#~| msgid "Street number"
+#~ msgid "home number"
+#~ msgstr "Numerus domini"
+
+#, fuzzy
+#~| msgid "Mobile phone"
+#~ msgid "mobile number"
+#~ msgstr "Numerus telephoni mobilis"
+
+#, fuzzy
+#~| msgid "Impersonation"
+#~ msgid "Is person active?"
+#~ msgstr "Simulare aliquem"
+
+#~ msgid "Impersonation"
+#~ msgstr "Simulare aliquem"
+
+#, fuzzy
+#~| msgid "Notifications"
+#~ msgid "OAuth2 applications"
+#~ msgstr "Nuntii"
+
 #~ msgid "Persons and accounts"
 #~ msgstr "Personae et computi"
 
@@ -3038,11 +3394,6 @@ msgstr ""
 #~ msgid "Link persons to accounts"
 #~ msgstr "Personae et computi"
 
-#, fuzzy
-#~| msgid "Impersonation"
-#~ msgid "Impersonate user"
-#~ msgstr "Simulare aliquem"
-
 #~ msgid "School logo"
 #~ msgstr "Imago scolae"
 
diff --git a/aleksis/core/locale/la/LC_MESSAGES/djangojs.po b/aleksis/core/locale/la/LC_MESSAGES/djangojs.po
index dc6d028e5442a3666f35a7cb46e39e5c01cdd3f9..dfec73c5c894d3d686092593be9c21a811cd0535 100644
--- a/aleksis/core/locale/la/LC_MESSAGES/djangojs.po
+++ b/aleksis/core/locale/la/LC_MESSAGES/djangojs.po
@@ -8,7 +8,7 @@ msgid ""
 msgstr ""
 "Project-Id-Version: PACKAGE VERSION\n"
 "Report-Msgid-Bugs-To: \n"
-"POT-Creation-Date: 2021-10-28 16:18+0200\n"
+"POT-Creation-Date: 2022-02-08 23:16+0000\n"
 "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
 "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
 "Language-Team: LANGUAGE <LL@li.org>\n"
@@ -17,18 +17,18 @@ msgstr ""
 "Content-Type: text/plain; charset=UTF-8\n"
 "Content-Transfer-Encoding: 8bit\n"
 
-#: aleksis/core/static/js/main.js:15
+#: aleksis/core/static/js/main.js:66
 msgid "Today"
 msgstr ""
 
-#: aleksis/core/static/js/main.js:16
+#: aleksis/core/static/js/main.js:67
 msgid "Cancel"
 msgstr ""
 
-#: aleksis/core/static/js/main.js:17
+#: aleksis/core/static/js/main.js:68
 msgid "OK"
 msgstr ""
 
-#: aleksis/core/static/js/main.js:127
+#: aleksis/core/static/js/main.js:191
 msgid "This page may contain outdated information since there is no internet connection."
 msgstr ""
diff --git a/aleksis/core/locale/nb_NO/LC_MESSAGES/django.po b/aleksis/core/locale/nb_NO/LC_MESSAGES/django.po
index ae2f8d194fcacd2bae0d16bb3350dfbaa978aaee..ab1f6b974dc48a296d3eead2faacea0232b1461d 100644
--- a/aleksis/core/locale/nb_NO/LC_MESSAGES/django.po
+++ b/aleksis/core/locale/nb_NO/LC_MESSAGES/django.po
@@ -8,7 +8,7 @@ msgid ""
 msgstr ""
 "Project-Id-Version: AlekSIS (School Information System) 0.1\n"
 "Report-Msgid-Bugs-To: \n"
-"POT-Creation-Date: 2021-10-28 16:18+0200\n"
+"POT-Creation-Date: 2022-02-08 23:16+0000\n"
 "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
 "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
 "Language-Team: LANGUAGE <LL@li.org>\n"
@@ -17,31 +17,38 @@ msgstr ""
 "Content-Type: text/plain; charset=UTF-8\n"
 "Content-Transfer-Encoding: 8bit\n"
 
-#: aleksis/core/apps.py:150
+#: aleksis/core/apps.py:166
 msgid "OpenID Connect scope"
 msgstr ""
 
-#: aleksis/core/apps.py:151
+#: aleksis/core/apps.py:167
 msgid "Given name, family name, link to profile and picture if existing."
 msgstr ""
 
-#: aleksis/core/apps.py:152
+#: aleksis/core/apps.py:168
 msgid "Full home postal address"
 msgstr ""
 
-#: aleksis/core/apps.py:153
+#: aleksis/core/apps.py:169
 msgid "Email address"
 msgstr ""
 
-#: aleksis/core/apps.py:154
+#: aleksis/core/apps.py:170
 msgid "Home and mobile phone"
 msgstr ""
 
-#: aleksis/core/data_checks.py:55
+#: aleksis/core/apps.py:171 aleksis/core/forms.py:220 aleksis/core/menus.py:265
+#: aleksis/core/models.py:462 aleksis/core/templates/core/group/list.html:8
+#: aleksis/core/templates/core/group/list.html:9
+#: aleksis/core/templates/core/person/full.html:250
+msgid "Groups"
+msgstr ""
+
+#: aleksis/core/data_checks.py:56
 msgid "Ignore problem"
 msgstr ""
 
-#: aleksis/core/data_checks.py:184
+#: aleksis/core/data_checks.py:185
 #, python-brace-format
 msgid "Solve option '{solve_option_obj.verbose_name}' "
 msgstr ""
@@ -58,8 +65,8 @@ msgstr ""
 msgid "The DashboardWidget was reported broken automatically."
 msgstr ""
 
-#: aleksis/core/filters.py:37 aleksis/core/templates/core/base.html:83
-#: aleksis/core/templates/core/base.html:84
+#: aleksis/core/filters.py:42 aleksis/core/templates/core/base.html:105
+#: aleksis/core/templates/core/base.html:106
 #: aleksis/core/templates/core/group/list.html:20
 #: aleksis/core/templates/core/person/list.html:24
 #: aleksis/core/templates/search/search.html:7
@@ -67,132 +74,181 @@ msgstr ""
 msgid "Search"
 msgstr ""
 
-#: aleksis/core/filters.py:53
+#: aleksis/core/filters.py:59
 msgid "Search by name"
 msgstr ""
 
-#: aleksis/core/filters.py:65
+#: aleksis/core/filters.py:71
 msgid "Search by contact details"
 msgstr ""
 
-#: aleksis/core/forms.py:41 aleksis/core/forms.py:387
+#: aleksis/core/filters.py:92
+msgid "Permission"
+msgstr ""
+
+#: aleksis/core/filters.py:100
+msgid "Content type"
+msgstr ""
+
+#: aleksis/core/filters.py:113 aleksis/core/models.py:688
+msgid "User"
+msgstr ""
+
+#: aleksis/core/filters.py:135 aleksis/core/models.py:461
+msgid "Group"
+msgstr ""
+
+#: aleksis/core/forms.py:50 aleksis/core/forms.py:581
 msgid "Base data"
 msgstr ""
 
-#: aleksis/core/forms.py:47
+#: aleksis/core/forms.py:55
 msgid "Address"
 msgstr ""
 
-#: aleksis/core/forms.py:48
+#: aleksis/core/forms.py:56 aleksis/core/forms.py:590
 msgid "Contact data"
 msgstr ""
 
-#: aleksis/core/forms.py:50
+#: aleksis/core/forms.py:58
 msgid "Advanced personal data"
 msgstr ""
 
-#: aleksis/core/forms.py:93
+#: aleksis/core/forms.py:106
 msgid "New user"
 msgstr ""
 
-#: aleksis/core/forms.py:93
+#: aleksis/core/forms.py:106
 msgid "Create a new account"
 msgstr ""
 
-#: aleksis/core/forms.py:124
+#: aleksis/core/forms.py:132
 msgid "You cannot set a new username when also selecting an existing user."
 msgstr ""
 
-#: aleksis/core/forms.py:128
+#: aleksis/core/forms.py:136
 msgid "This username is already in use."
 msgstr ""
 
-#: aleksis/core/forms.py:145 aleksis/core/models.py:117
+#: aleksis/core/forms.py:153 aleksis/core/models.py:130
 msgid "School term"
 msgstr ""
 
-#: aleksis/core/forms.py:146
+#: aleksis/core/forms.py:154
 msgid "Common data"
 msgstr ""
 
-#: aleksis/core/forms.py:147 aleksis/core/forms.py:196
-#: aleksis/core/menus.py:238 aleksis/core/models.py:140
+#: aleksis/core/forms.py:155 aleksis/core/forms.py:207
+#: aleksis/core/menus.py:254 aleksis/core/models.py:153
 #: aleksis/core/templates/core/person/list.html:8
 #: aleksis/core/templates/core/person/list.html:9
 msgid "Persons"
 msgstr ""
 
-#: aleksis/core/forms.py:148
+#: aleksis/core/forms.py:156 aleksis/core/forms.py:592
 msgid "Additional data"
 msgstr ""
 
-#: aleksis/core/forms.py:188 aleksis/core/forms.py:191
-#: aleksis/core/models.py:60
-msgid "Date"
+#: aleksis/core/forms.py:157 aleksis/core/models.py:206
+#: aleksis/core/models.py:514
+msgid "Photo"
 msgstr ""
 
-#: aleksis/core/forms.py:189 aleksis/core/forms.py:192
-#: aleksis/core/models.py:68
-msgid "Time"
+#: aleksis/core/forms.py:199 aleksis/core/forms.py:202
+#: aleksis/core/models.py:73
+msgid "Date"
 msgstr ""
 
-#: aleksis/core/forms.py:209 aleksis/core/menus.py:249
-#: aleksis/core/models.py:398 aleksis/core/templates/core/group/list.html:8
-#: aleksis/core/templates/core/group/list.html:9
-#: aleksis/core/templates/core/person/full.html:144
-msgid "Groups"
+#: aleksis/core/forms.py:200 aleksis/core/forms.py:203
+#: aleksis/core/models.py:81
+msgid "Time"
 msgstr ""
 
-#: aleksis/core/forms.py:219
+#: aleksis/core/forms.py:233
 msgid "From when until when should the announcement be displayed?"
 msgstr ""
 
-#: aleksis/core/forms.py:222
+#: aleksis/core/forms.py:236
 msgid "Who should see the announcement?"
 msgstr ""
 
-#: aleksis/core/forms.py:223
+#: aleksis/core/forms.py:237
 msgid "Write your announcement:"
 msgstr ""
 
-#: aleksis/core/forms.py:262
+#: aleksis/core/forms.py:276
 msgid "You are not allowed to create announcements which are only valid in the past."
 msgstr ""
 
-#: aleksis/core/forms.py:266
+#: aleksis/core/forms.py:280
 msgid "The from date and time must be earlier then the until date and time."
 msgstr ""
 
-#: aleksis/core/forms.py:275
+#: aleksis/core/forms.py:289
 msgid "You need at least one recipient."
 msgstr ""
 
-#: aleksis/core/forms.py:389
-msgid "Account data"
+#: aleksis/core/forms.py:398
+msgid "Invitation code"
 msgstr ""
 
-#: aleksis/core/forms.py:391
-msgid "Consents"
+#: aleksis/core/forms.py:399
+msgid "Please enter your invitation code."
 msgstr ""
 
-#: aleksis/core/forms.py:396
-msgid "Password"
+#: aleksis/core/forms.py:418 aleksis/core/models.py:181
+msgid "First name"
 msgstr ""
 
-#: aleksis/core/forms.py:402
-msgid "Password (again)"
+#: aleksis/core/forms.py:419 aleksis/core/models.py:182
+msgid "Last name"
 msgstr ""
 
-#: aleksis/core/forms.py:411
-#, python-brace-format
-msgid "I have read the <a href='{privacy_policy}'>Privacy policy</a> and agree with them."
+#: aleksis/core/forms.py:428
+msgid "A person is using this e-mail address"
+msgstr ""
+
+#: aleksis/core/forms.py:456
+msgid "Who should get the permission?"
+msgstr ""
+
+#: aleksis/core/forms.py:457
+msgid "On what?"
+msgstr ""
+
+#: aleksis/core/forms.py:483
+msgid "Select objects which the permission should be granted for:"
+msgstr ""
+
+#: aleksis/core/forms.py:486
+msgid "Grant the permission for all objects"
+msgstr ""
+
+#: aleksis/core/forms.py:494
+msgid "You must select at least one group or person which should get the permission."
+msgstr ""
+
+#: aleksis/core/forms.py:499
+msgid "You must grant the permission to all objects and/or to some objects."
 msgstr ""
 
-#: aleksis/core/forms.py:435
-msgid "You must type the same password each time."
+#: aleksis/core/forms.py:586
+msgid "Adress data"
 msgstr ""
 
-#: aleksis/core/forms.py:580
+#: aleksis/core/forms.py:598
+msgid "Account data"
+msgstr ""
+
+#: aleksis/core/forms.py:605
+msgid "Password"
+msgstr ""
+
+#: aleksis/core/forms.py:608
+msgid "Password (again)"
+msgstr ""
+
+#: aleksis/core/forms.py:775
 msgid "No valid selection."
 msgstr ""
 
@@ -200,67 +256,65 @@ msgstr ""
 msgid "There are unresolved data problems."
 msgstr ""
 
-#: aleksis/core/health_checks.py:38
-msgid "The backup folder doesn't exist."
+#: aleksis/core/health_checks.py:40
+msgid "Error accessing backup storage: {}"
 msgstr ""
 
-#: aleksis/core/health_checks.py:47
-#, python-brace-format
-msgid "Last backup {time_gone_since_backup}!"
+#: aleksis/core/health_checks.py:50
+msgid "Last backup {}!"
 msgstr ""
 
-#: aleksis/core/health_checks.py:49
+#: aleksis/core/health_checks.py:52
 msgid "No backup found!"
 msgstr ""
 
-#: aleksis/core/health_checks.py:76
+#: aleksis/core/health_checks.py:79
 msgid "No backup result found!"
 msgstr ""
 
-#: aleksis/core/health_checks.py:78
-#, python-brace-format
-msgid "{task.status} - {task.result}"
-msgstr ""
-
 #: aleksis/core/menus.py:9 aleksis/core/templates/two_factor/core/login.html:6
-#: aleksis/core/templates/two_factor/core/login.html:22
-#: aleksis/core/templates/two_factor/core/login.html:76
+#: aleksis/core/templates/two_factor/core/login.html:32
+#: aleksis/core/templates/two_factor/core/login.html:95
 msgid "Login"
 msgstr ""
 
-#: aleksis/core/menus.py:15 aleksis/core/templates/account/signup.html:20
+#: aleksis/core/menus.py:15 aleksis/core/templates/account/signup.html:22
 #: aleksis/core/templates/socialaccount/signup.html:23
 msgid "Sign up"
 msgstr ""
 
-#: aleksis/core/menus.py:24
+#: aleksis/core/menus.py:24 aleksis/core/templates/invitations/enter.html:7
+msgid "Accept invitation"
+msgstr ""
+
+#: aleksis/core/menus.py:33
 msgid "Dashboard"
 msgstr ""
 
-#: aleksis/core/menus.py:32 aleksis/core/models.py:605
-#: aleksis/core/preferences.py:26
+#: aleksis/core/menus.py:41 aleksis/core/models.py:734
+#: aleksis/core/preferences.py:29
 #: aleksis/core/templates/core/notifications.html:4
 #: aleksis/core/templates/core/notifications.html:5
 msgid "Notifications"
 msgstr ""
 
-#: aleksis/core/menus.py:41
+#: aleksis/core/menus.py:53
 msgid "Account"
 msgstr ""
 
-#: aleksis/core/menus.py:48
+#: aleksis/core/menus.py:60
 msgid "Stop impersonation"
 msgstr ""
 
-#: aleksis/core/menus.py:57 aleksis/core/templates/core/base.html:62
+#: aleksis/core/menus.py:69 aleksis/core/templates/core/base.html:80
 msgid "Logout"
 msgstr ""
 
-#: aleksis/core/menus.py:63
+#: aleksis/core/menus.py:75
 msgid "2FA"
 msgstr ""
 
-#: aleksis/core/menus.py:69
+#: aleksis/core/menus.py:83
 #: aleksis/core/templates/account/password_change.html:5
 #: aleksis/core/templates/account/password_change.html:6
 #: aleksis/core/templates/account/password_change.html:19
@@ -272,847 +326,875 @@ msgstr ""
 msgid "Change password"
 msgstr ""
 
-#: aleksis/core/menus.py:81
+#: aleksis/core/menus.py:95
 msgid "Me"
 msgstr ""
 
-#: aleksis/core/menus.py:90
+#: aleksis/core/menus.py:104
 #: aleksis/core/templates/dynamic_preferences/form.html:5
 msgid "Preferences"
 msgstr ""
 
-#: aleksis/core/menus.py:99
+#: aleksis/core/menus.py:113
 msgid "Third-party accounts"
 msgstr ""
 
-#: aleksis/core/menus.py:108
+#: aleksis/core/menus.py:122
 #: aleksis/core/templates/oauth2_provider/authorized-tokens.html:5
 #: aleksis/core/templates/oauth2_provider/authorized-tokens.html:6
 msgid "Authorized applications"
 msgstr ""
 
-#: aleksis/core/menus.py:119
+#: aleksis/core/menus.py:133
 msgid "Admin"
 msgstr ""
 
-#: aleksis/core/menus.py:127 aleksis/core/models.py:704
+#: aleksis/core/menus.py:141 aleksis/core/models.py:834
 #: aleksis/core/templates/core/announcement/list.html:7
 #: aleksis/core/templates/core/announcement/list.html:8
 msgid "Announcements"
 msgstr ""
 
-#: aleksis/core/menus.py:138 aleksis/core/models.py:118
+#: aleksis/core/menus.py:152 aleksis/core/models.py:131
 #: aleksis/core/templates/core/school_term/list.html:8
 #: aleksis/core/templates/core/school_term/list.html:9
 msgid "School terms"
 msgstr ""
 
-#: aleksis/core/menus.py:149
+#: aleksis/core/menus.py:163
 #: aleksis/core/templates/core/dashboard_widget/list.html:8
 #: aleksis/core/templates/core/dashboard_widget/list.html:9
 msgid "Dashboard widgets"
 msgstr ""
 
-#: aleksis/core/menus.py:160
+#: aleksis/core/menus.py:174
 #: aleksis/core/templates/core/management/data_management.html:6
 #: aleksis/core/templates/core/management/data_management.html:7
 msgid "Data management"
 msgstr ""
 
-#: aleksis/core/menus.py:171
+#: aleksis/core/menus.py:185
 #: aleksis/core/templates/core/pages/system_status.html:5
 #: aleksis/core/templates/core/pages/system_status.html:7
 msgid "System status"
 msgstr ""
 
-#: aleksis/core/menus.py:182
-msgid "Impersonation"
-msgstr ""
-
-#: aleksis/core/menus.py:193
+#: aleksis/core/menus.py:196
 msgid "Configuration"
 msgstr ""
 
-#: aleksis/core/menus.py:204 aleksis/core/templates/core/data_check/list.html:9
+#: aleksis/core/menus.py:207 aleksis/core/templates/core/data_check/list.html:9
 #: aleksis/core/templates/core/data_check/list.html:10
 msgid "Data checks"
 msgstr ""
 
-#: aleksis/core/menus.py:210
+#: aleksis/core/menus.py:213 aleksis/core/templates/core/perms/list.html:13
+#: aleksis/core/templates/core/perms/list.html:14
+msgid "Manage permissions"
+msgstr ""
+
+#: aleksis/core/menus.py:224
 msgid "Backend Admin"
 msgstr ""
 
-#: aleksis/core/menus.py:216
-#: aleksis/core/templates/oauth2_provider/application_detail.html:5
-#: aleksis/core/templates/oauth2_provider/application_list.html:5
+#: aleksis/core/menus.py:232
+#: aleksis/core/templates/oauth2_provider/application/list.html:5
+#: aleksis/core/templates/oauth2_provider/application/list.html:6
 msgid "OAuth2 Applications"
 msgstr ""
 
-#: aleksis/core/menus.py:229
+#: aleksis/core/menus.py:245
 msgid "People"
 msgstr ""
 
-#: aleksis/core/menus.py:260 aleksis/core/models.py:958
+#: aleksis/core/menus.py:276 aleksis/core/models.py:1055
 #: aleksis/core/templates/core/group_type/list.html:8
 #: aleksis/core/templates/core/group_type/list.html:9
 msgid "Group types"
 msgstr ""
 
-#: aleksis/core/menus.py:271
+#: aleksis/core/menus.py:287
 msgid "Groups and child groups"
 msgstr ""
 
-#: aleksis/core/menus.py:282 aleksis/core/models.py:446
+#: aleksis/core/menus.py:298 aleksis/core/models.py:510
 #: aleksis/core/templates/core/additional_field/list.html:8
 #: aleksis/core/templates/core/additional_field/list.html:9
 msgid "Additional fields"
 msgstr ""
 
-#: aleksis/core/menus.py:297
+#: aleksis/core/menus.py:309
+msgid "Invite person"
+msgstr ""
+
+#: aleksis/core/menus.py:322
 #: aleksis/core/templates/core/group/child_groups.html:7
 #: aleksis/core/templates/core/group/child_groups.html:9
 msgid "Assign child groups to groups"
 msgstr ""
 
-#: aleksis/core/mixins.py:498
+#: aleksis/core/mixins.py:511
 msgid "Linked school term"
 msgstr ""
 
-#: aleksis/core/models.py:58
+#: aleksis/core/models.py:71
 msgid "Boolean (Yes/No)"
 msgstr ""
 
-#: aleksis/core/models.py:59
+#: aleksis/core/models.py:72
 msgid "Text (one line)"
 msgstr ""
 
-#: aleksis/core/models.py:61
+#: aleksis/core/models.py:74
 msgid "Date and time"
 msgstr ""
 
-#: aleksis/core/models.py:62
+#: aleksis/core/models.py:75
 msgid "Decimal number"
 msgstr ""
 
-#: aleksis/core/models.py:63 aleksis/core/models.py:186
+#: aleksis/core/models.py:76 aleksis/core/models.py:199
 msgid "E-mail address"
 msgstr ""
 
-#: aleksis/core/models.py:64
+#: aleksis/core/models.py:77
 msgid "Integer"
 msgstr ""
 
-#: aleksis/core/models.py:65
+#: aleksis/core/models.py:78
 msgid "IP address"
 msgstr ""
 
-#: aleksis/core/models.py:66
+#: aleksis/core/models.py:79
 msgid "Boolean or empty (Yes/No/Neither)"
 msgstr ""
 
-#: aleksis/core/models.py:67
+#: aleksis/core/models.py:80
 msgid "Text (multi-line)"
 msgstr ""
 
-#: aleksis/core/models.py:69
+#: aleksis/core/models.py:82
 msgid "URL / Link"
 msgstr ""
 
-#: aleksis/core/models.py:81 aleksis/core/models.py:927
+#: aleksis/core/models.py:94 aleksis/core/models.py:1024
 msgid "Name"
 msgstr ""
 
-#: aleksis/core/models.py:83
+#: aleksis/core/models.py:96
 msgid "Start date"
 msgstr ""
 
-#: aleksis/core/models.py:84
+#: aleksis/core/models.py:97
 msgid "End date"
 msgstr ""
 
-#: aleksis/core/models.py:103
+#: aleksis/core/models.py:116
 msgid "The start date must be earlier than the end date."
 msgstr ""
 
-#: aleksis/core/models.py:110
+#: aleksis/core/models.py:123
 msgid "There is already a school term for this time or a part of this time."
 msgstr ""
 
-#: aleksis/core/models.py:139 aleksis/core/models.py:876
+#: aleksis/core/models.py:152 aleksis/core/models.py:973
 msgid "Person"
 msgstr ""
 
-#: aleksis/core/models.py:142
+#: aleksis/core/models.py:155
 msgid "Can view address"
 msgstr ""
 
-#: aleksis/core/models.py:143
+#: aleksis/core/models.py:156
 msgid "Can view contact details"
 msgstr ""
 
-#: aleksis/core/models.py:144
+#: aleksis/core/models.py:157
 msgid "Can view photo"
 msgstr ""
 
-#: aleksis/core/models.py:145
+#: aleksis/core/models.py:158
+msgid "Can view avatar image"
+msgstr ""
+
+#: aleksis/core/models.py:159
 msgid "Can view persons groups"
 msgstr ""
 
-#: aleksis/core/models.py:146
+#: aleksis/core/models.py:160
 msgid "Can view personal details"
 msgstr ""
 
-#: aleksis/core/models.py:156
+#: aleksis/core/models.py:170
 msgid "female"
 msgstr ""
 
-#: aleksis/core/models.py:156
+#: aleksis/core/models.py:170
 msgid "male"
 msgstr ""
 
-#: aleksis/core/models.py:164
+#: aleksis/core/models.py:178 aleksis/core/models.py:1227
 msgid "Linked user"
 msgstr ""
 
-#: aleksis/core/models.py:166
-msgid "Is person active?"
-msgstr ""
-
-#: aleksis/core/models.py:168
-msgid "First name"
-msgstr ""
-
-#: aleksis/core/models.py:169
-msgid "Last name"
-msgstr ""
-
-#: aleksis/core/models.py:171
+#: aleksis/core/models.py:184
 msgid "Additional name(s)"
 msgstr ""
 
-#: aleksis/core/models.py:175 aleksis/core/models.py:415
+#: aleksis/core/models.py:188 aleksis/core/models.py:479
 msgid "Short name"
 msgstr ""
 
-#: aleksis/core/models.py:178
+#: aleksis/core/models.py:191
 msgid "Street"
 msgstr ""
 
-#: aleksis/core/models.py:179
+#: aleksis/core/models.py:192
 msgid "Street number"
 msgstr ""
 
-#: aleksis/core/models.py:180
+#: aleksis/core/models.py:193
 msgid "Postal code"
 msgstr ""
 
-#: aleksis/core/models.py:181
+#: aleksis/core/models.py:194
 msgid "Place"
 msgstr ""
 
-#: aleksis/core/models.py:183
+#: aleksis/core/models.py:196 aleksis/core/templates/core/person/full.html:172
 msgid "Home phone"
 msgstr ""
 
-#: aleksis/core/models.py:184
+#: aleksis/core/models.py:197 aleksis/core/templates/core/person/full.html:182
 msgid "Mobile phone"
 msgstr ""
 
-#: aleksis/core/models.py:188
+#: aleksis/core/models.py:201
 msgid "Date of birth"
 msgstr ""
 
-#: aleksis/core/models.py:189
+#: aleksis/core/models.py:202
+msgid "Place of birth"
+msgstr ""
+
+#: aleksis/core/models.py:203
 msgid "Sex"
 msgstr ""
 
-#: aleksis/core/models.py:191
-msgid "Photo"
+#: aleksis/core/models.py:210 aleksis/core/models.py:518
+msgid "This is an official photo, used for official documents and for internal use cases."
 msgstr ""
 
-#: aleksis/core/models.py:195 aleksis/core/templates/core/person/full.html:137
+#: aleksis/core/models.py:215 aleksis/core/models.py:522
+msgid "Display picture / Avatar"
+msgstr ""
+
+#: aleksis/core/models.py:218 aleksis/core/models.py:525
+msgid "This is a picture or an avatar for public display."
+msgstr ""
+
+#: aleksis/core/models.py:223 aleksis/core/templates/core/person/full.html:239
 msgid "Guardians / Parents"
 msgstr ""
 
-#: aleksis/core/models.py:202
+#: aleksis/core/models.py:230
 msgid "Primary group"
 msgstr ""
 
-#: aleksis/core/models.py:205 aleksis/core/models.py:563
-#: aleksis/core/models.py:587 aleksis/core/models.py:672
-#: aleksis/core/models.py:951 aleksis/core/templates/core/person/full.html:120
+#: aleksis/core/models.py:233 aleksis/core/models.py:692
+#: aleksis/core/models.py:716 aleksis/core/models.py:801
+#: aleksis/core/models.py:1048
 msgid "Description"
 msgstr ""
 
-#: aleksis/core/models.py:370
+#: aleksis/core/models.py:434
 msgid "Title of field"
 msgstr ""
 
-#: aleksis/core/models.py:372
+#: aleksis/core/models.py:436
 msgid "Type of field"
 msgstr ""
 
-#: aleksis/core/models.py:379
+#: aleksis/core/models.py:443
 msgid "Addtitional field for groups"
 msgstr ""
 
-#: aleksis/core/models.py:380
+#: aleksis/core/models.py:444
 msgid "Addtitional fields for groups"
 msgstr ""
 
-#: aleksis/core/models.py:397
-msgid "Group"
-msgstr ""
-
-#: aleksis/core/models.py:400
+#: aleksis/core/models.py:464
 msgid "Can assign child groups to groups"
 msgstr ""
 
-#: aleksis/core/models.py:401
+#: aleksis/core/models.py:465
 msgid "Can view statistics about group."
 msgstr ""
 
-#: aleksis/core/models.py:413
+#: aleksis/core/models.py:477
 msgid "Long name"
 msgstr ""
 
-#: aleksis/core/models.py:423 aleksis/core/templates/core/group/full.html:85
+#: aleksis/core/models.py:487 aleksis/core/templates/core/group/full.html:85
 msgid "Members"
 msgstr ""
 
-#: aleksis/core/models.py:426 aleksis/core/templates/core/group/full.html:82
+#: aleksis/core/models.py:490 aleksis/core/templates/core/group/full.html:82
 msgid "Owners"
 msgstr ""
 
-#: aleksis/core/models.py:433 aleksis/core/templates/core/group/full.html:55
+#: aleksis/core/models.py:497 aleksis/core/templates/core/group/full.html:55
 msgid "Parent groups"
 msgstr ""
 
-#: aleksis/core/models.py:441
+#: aleksis/core/models.py:505
 msgid "Type of group"
 msgstr ""
 
-#: aleksis/core/models.py:559
-msgid "User"
-msgstr ""
-
-#: aleksis/core/models.py:562 aleksis/core/models.py:586
-#: aleksis/core/models.py:671
+#: aleksis/core/models.py:691 aleksis/core/models.py:715
+#: aleksis/core/models.py:800
 #: aleksis/core/templates/core/announcement/list.html:18
 msgid "Title"
 msgstr ""
 
-#: aleksis/core/models.py:565
+#: aleksis/core/models.py:694
 msgid "Application"
 msgstr ""
 
-#: aleksis/core/models.py:571
+#: aleksis/core/models.py:700
 msgid "Activity"
 msgstr ""
 
-#: aleksis/core/models.py:572
+#: aleksis/core/models.py:701
 msgid "Activities"
 msgstr ""
 
-#: aleksis/core/models.py:578
+#: aleksis/core/models.py:707
 msgid "Sender"
 msgstr ""
 
-#: aleksis/core/models.py:583
+#: aleksis/core/models.py:712
 msgid "Recipient"
 msgstr ""
 
-#: aleksis/core/models.py:588 aleksis/core/models.py:928
+#: aleksis/core/models.py:717 aleksis/core/models.py:1025
 msgid "Link"
 msgstr ""
 
-#: aleksis/core/models.py:590
+#: aleksis/core/models.py:719
 msgid "Read"
 msgstr ""
 
-#: aleksis/core/models.py:591
+#: aleksis/core/models.py:720
 msgid "Sent"
 msgstr ""
 
-#: aleksis/core/models.py:604
+#: aleksis/core/models.py:733
 msgid "Notification"
 msgstr ""
 
-#: aleksis/core/models.py:673
+#: aleksis/core/models.py:802
 msgid "Link to detailed view"
 msgstr ""
 
-#: aleksis/core/models.py:676
+#: aleksis/core/models.py:805
 msgid "Date and time from when to show"
 msgstr ""
 
-#: aleksis/core/models.py:679
+#: aleksis/core/models.py:808
 msgid "Date and time until when to show"
 msgstr ""
 
-#: aleksis/core/models.py:703
+#: aleksis/core/models.py:833
 msgid "Announcement"
 msgstr ""
 
-#: aleksis/core/models.py:741
+#: aleksis/core/models.py:871
 msgid "Announcement recipient"
 msgstr ""
 
-#: aleksis/core/models.py:742
+#: aleksis/core/models.py:872
 msgid "Announcement recipients"
 msgstr ""
 
-#: aleksis/core/models.py:797
+#: aleksis/core/models.py:894
 msgid "Widget Title"
 msgstr ""
 
-#: aleksis/core/models.py:798
+#: aleksis/core/models.py:895
 msgid "Activate Widget"
 msgstr ""
 
-#: aleksis/core/models.py:799
+#: aleksis/core/models.py:896
 msgid "Widget is broken"
 msgstr ""
 
-#: aleksis/core/models.py:802
+#: aleksis/core/models.py:899
 msgid "Size on mobile devices"
 msgstr ""
 
-#: aleksis/core/models.py:803
+#: aleksis/core/models.py:900
 msgid "<= 600 px, 12 columns"
 msgstr ""
 
-#: aleksis/core/models.py:808
+#: aleksis/core/models.py:905
 msgid "Size on tablet devices"
 msgstr ""
 
-#: aleksis/core/models.py:809
+#: aleksis/core/models.py:906
 msgid "> 600 px, 12 columns"
 msgstr ""
 
-#: aleksis/core/models.py:814
+#: aleksis/core/models.py:911
 msgid "Size on desktop devices"
 msgstr ""
 
-#: aleksis/core/models.py:815
+#: aleksis/core/models.py:912
 msgid "> 992 px, 12 columns"
 msgstr ""
 
-#: aleksis/core/models.py:820
+#: aleksis/core/models.py:917
 msgid "Size on large desktop devices"
 msgstr ""
 
-#: aleksis/core/models.py:821
+#: aleksis/core/models.py:918
 msgid "> 1200 px>, 12 columns"
 msgstr ""
 
-#: aleksis/core/models.py:852
+#: aleksis/core/models.py:949
 msgid "Can edit default dashboard"
 msgstr ""
 
-#: aleksis/core/models.py:853
+#: aleksis/core/models.py:950
 msgid "Dashboard Widget"
 msgstr ""
 
-#: aleksis/core/models.py:854
+#: aleksis/core/models.py:951
 msgid "Dashboard Widgets"
 msgstr ""
 
-#: aleksis/core/models.py:860
+#: aleksis/core/models.py:957
 msgid "URL"
 msgstr ""
 
-#: aleksis/core/models.py:861
+#: aleksis/core/models.py:958
 msgid "Icon URL"
 msgstr ""
 
-#: aleksis/core/models.py:867
+#: aleksis/core/models.py:964
 msgid "External link widget"
 msgstr ""
 
-#: aleksis/core/models.py:868
+#: aleksis/core/models.py:965
 msgid "External link widgets"
 msgstr ""
 
-#: aleksis/core/models.py:873
+#: aleksis/core/models.py:970
 msgid "Dashboard widget"
 msgstr ""
 
-#: aleksis/core/models.py:878
+#: aleksis/core/models.py:975
 msgid "Order"
 msgstr ""
 
-#: aleksis/core/models.py:879
+#: aleksis/core/models.py:976
 msgid "Part of the default dashboard"
 msgstr ""
 
-#: aleksis/core/models.py:894
+#: aleksis/core/models.py:991
 msgid "Dashboard widget order"
 msgstr ""
 
-#: aleksis/core/models.py:895
+#: aleksis/core/models.py:992
 msgid "Dashboard widget orders"
 msgstr ""
 
-#: aleksis/core/models.py:901
+#: aleksis/core/models.py:998
 msgid "Menu ID"
 msgstr ""
 
-#: aleksis/core/models.py:914
+#: aleksis/core/models.py:1011
 msgid "Custom menu"
 msgstr ""
 
-#: aleksis/core/models.py:915
+#: aleksis/core/models.py:1012
 msgid "Custom menus"
 msgstr ""
 
-#: aleksis/core/models.py:925
+#: aleksis/core/models.py:1022
 msgid "Menu"
 msgstr ""
 
-#: aleksis/core/models.py:929
+#: aleksis/core/models.py:1026 aleksis/core/models.py:1274
+#: aleksis/core/templates/oauth2_provider/application/detail.html:26
 msgid "Icon"
 msgstr ""
 
-#: aleksis/core/models.py:935
+#: aleksis/core/models.py:1032
 msgid "Custom menu item"
 msgstr ""
 
-#: aleksis/core/models.py:936
+#: aleksis/core/models.py:1033
 msgid "Custom menu items"
 msgstr ""
 
-#: aleksis/core/models.py:950
+#: aleksis/core/models.py:1047
 msgid "Title of type"
 msgstr ""
 
-#: aleksis/core/models.py:957 aleksis/core/templates/core/group/full.html:47
+#: aleksis/core/models.py:1054 aleksis/core/templates/core/group/full.html:47
 msgid "Group type"
 msgstr ""
 
-#: aleksis/core/models.py:971
+#: aleksis/core/models.py:1068
 msgid "Can view system status"
 msgstr ""
 
-#: aleksis/core/models.py:972
+#: aleksis/core/models.py:1069
 msgid "Can manage data"
 msgstr ""
 
-#: aleksis/core/models.py:973
+#: aleksis/core/models.py:1070
 msgid "Can impersonate"
 msgstr ""
 
-#: aleksis/core/models.py:974
+#: aleksis/core/models.py:1071
 msgid "Can use search"
 msgstr ""
 
-#: aleksis/core/models.py:975
+#: aleksis/core/models.py:1072
 msgid "Can change site preferences"
 msgstr ""
 
-#: aleksis/core/models.py:976
+#: aleksis/core/models.py:1073
 msgid "Can change person preferences"
 msgstr ""
 
-#: aleksis/core/models.py:977
+#: aleksis/core/models.py:1074
 msgid "Can change group preferences"
 msgstr ""
 
-#: aleksis/core/models.py:978
-msgid "Can add oauth applications"
-msgstr ""
-
-#: aleksis/core/models.py:979
-msgid "Can list oauth applications"
-msgstr ""
-
-#: aleksis/core/models.py:980
-msgid "Can view oauth applications"
-msgstr ""
-
-#: aleksis/core/models.py:981
-msgid "Can update oauth applications"
-msgstr ""
-
-#: aleksis/core/models.py:982
-msgid "Can delete oauth applications"
+#: aleksis/core/models.py:1075
+msgid "Can test PDF generation"
 msgstr ""
 
-#: aleksis/core/models.py:983
-msgid "Can test PDF generation"
+#: aleksis/core/models.py:1076
+msgid "Can invite persons"
 msgstr ""
 
-#: aleksis/core/models.py:1019
+#: aleksis/core/models.py:1112
 msgid "Related data check task"
 msgstr ""
 
-#: aleksis/core/models.py:1027
+#: aleksis/core/models.py:1120
 msgid "Issue solved"
 msgstr ""
 
-#: aleksis/core/models.py:1028
+#: aleksis/core/models.py:1121
 msgid "Notification sent"
 msgstr ""
 
-#: aleksis/core/models.py:1041
+#: aleksis/core/models.py:1134
 msgid "Data check result"
 msgstr ""
 
-#: aleksis/core/models.py:1042
+#: aleksis/core/models.py:1135
 msgid "Data check results"
 msgstr ""
 
-#: aleksis/core/models.py:1044
+#: aleksis/core/models.py:1137
 msgid "Can run data checks"
 msgstr ""
 
-#: aleksis/core/models.py:1045
+#: aleksis/core/models.py:1138
 msgid "Can solve data check problems"
 msgstr ""
 
-#: aleksis/core/models.py:1060
+#: aleksis/core/models.py:1145
+msgid "E-Mail address"
+msgstr ""
+
+#: aleksis/core/models.py:1177
 msgid "Owner"
 msgstr ""
 
-#: aleksis/core/models.py:1064
+#: aleksis/core/models.py:1181
 msgid "File expires at"
 msgstr ""
 
-#: aleksis/core/models.py:1066
+#: aleksis/core/models.py:1183
 msgid "Generated HTML file"
 msgstr ""
 
-#: aleksis/core/models.py:1068
+#: aleksis/core/models.py:1185
 msgid "Generated PDF file"
 msgstr ""
 
-#: aleksis/core/models.py:1075
+#: aleksis/core/models.py:1192
 msgid "PDF file"
 msgstr ""
 
-#: aleksis/core/models.py:1076
+#: aleksis/core/models.py:1193
 msgid "PDF files"
 msgstr ""
 
-#: aleksis/core/models.py:1081
+#: aleksis/core/models.py:1198
 msgid "Task result"
 msgstr ""
 
-#: aleksis/core/models.py:1084
+#: aleksis/core/models.py:1201
 msgid "Task user"
 msgstr ""
 
-#: aleksis/core/models.py:1096
+#: aleksis/core/models.py:1213
 msgid "Task user assignment"
 msgstr ""
 
-#: aleksis/core/models.py:1097
+#: aleksis/core/models.py:1214
 msgid "Task user assignments"
 msgstr ""
 
-#: aleksis/core/preferences.py:22
+#: aleksis/core/models.py:1230
+msgid "Additional attributes"
+msgstr ""
+
+#: aleksis/core/models.py:1268
+msgid "Allowed scopes that clients can request"
+msgstr ""
+
+#: aleksis/core/models.py:1278
+msgid "This image will be shown as icon in the authorization flow. It should be squared."
+msgstr ""
+
+#: aleksis/core/preferences.py:25
 msgid "General"
 msgstr ""
 
-#: aleksis/core/preferences.py:23
+#: aleksis/core/preferences.py:26
 msgid "School"
 msgstr ""
 
-#: aleksis/core/preferences.py:24
+#: aleksis/core/preferences.py:27
 msgid "Theme"
 msgstr ""
 
-#: aleksis/core/preferences.py:25
+#: aleksis/core/preferences.py:28
 msgid "Mail"
 msgstr ""
 
-#: aleksis/core/preferences.py:27
+#: aleksis/core/preferences.py:30
 msgid "Footer"
 msgstr ""
 
-#: aleksis/core/preferences.py:28
+#: aleksis/core/preferences.py:31
 msgid "Accounts"
 msgstr ""
 
-#: aleksis/core/preferences.py:29
+#: aleksis/core/preferences.py:32
 msgid "Authentication"
 msgstr ""
 
-#: aleksis/core/preferences.py:30
+#: aleksis/core/preferences.py:33
 msgid "Internationalisation"
 msgstr ""
 
-#: aleksis/core/preferences.py:41
+#: aleksis/core/preferences.py:43
 msgid "Site title"
 msgstr ""
 
-#: aleksis/core/preferences.py:52
+#: aleksis/core/preferences.py:55
 msgid "Site description"
 msgstr ""
 
-#: aleksis/core/preferences.py:63
+#: aleksis/core/preferences.py:65
 msgid "Primary colour"
 msgstr ""
 
-#: aleksis/core/preferences.py:75
+#: aleksis/core/preferences.py:77
 msgid "Secondary colour"
 msgstr ""
 
-#: aleksis/core/preferences.py:86
+#: aleksis/core/preferences.py:89
 msgid "Logo"
 msgstr ""
 
-#: aleksis/core/preferences.py:96
+#: aleksis/core/preferences.py:100
 msgid "Favicon"
 msgstr ""
 
-#: aleksis/core/preferences.py:106
+#: aleksis/core/preferences.py:111
 msgid "PWA-Icon"
 msgstr ""
 
-#: aleksis/core/preferences.py:117
+#: aleksis/core/preferences.py:121
+msgid "PWA-Icon is maskable"
+msgstr ""
+
+#: aleksis/core/preferences.py:133
 msgid "Mail out name"
 msgstr ""
 
-#: aleksis/core/preferences.py:128
+#: aleksis/core/preferences.py:144
 msgid "Mail out address"
 msgstr ""
 
-#: aleksis/core/preferences.py:140
+#: aleksis/core/preferences.py:157
 msgid "Link to privacy policy"
 msgstr ""
 
-#: aleksis/core/preferences.py:152
+#: aleksis/core/preferences.py:169
 msgid "Link to imprint"
 msgstr ""
 
-#: aleksis/core/preferences.py:164
+#: aleksis/core/preferences.py:180
 msgid "Name format for addressing"
 msgstr ""
 
-#: aleksis/core/preferences.py:180
+#: aleksis/core/preferences.py:197
 msgid "Channels to use for notifications"
 msgstr ""
 
-#: aleksis/core/preferences.py:192
+#: aleksis/core/preferences.py:209
 msgid "Regular expression to match primary group, e.g. '^Class .*'"
 msgstr ""
 
-#: aleksis/core/preferences.py:203
+#: aleksis/core/preferences.py:220
 msgid "Field on person to match primary group against"
 msgstr ""
 
-#: aleksis/core/preferences.py:215
+#: aleksis/core/preferences.py:232
 msgid "Automatically create new persons for new users"
 msgstr ""
 
-#: aleksis/core/preferences.py:224
+#: aleksis/core/preferences.py:241
 msgid "Automatically link existing persons to new users by their e-mail address"
 msgstr ""
 
-#: aleksis/core/preferences.py:235
+#: aleksis/core/preferences.py:252
 msgid "Display name of the school"
 msgstr ""
 
-#: aleksis/core/preferences.py:246
+#: aleksis/core/preferences.py:263
 msgid "Official name of the school, e.g. as given by supervisory authority"
 msgstr ""
 
-#: aleksis/core/preferences.py:254
+#: aleksis/core/preferences.py:271
 msgid "Allow users to change their passwords"
 msgstr ""
 
-#: aleksis/core/preferences.py:262
+#: aleksis/core/preferences.py:279
 msgid "Enable signup"
 msgstr ""
 
-#: aleksis/core/preferences.py:273
+#: aleksis/core/preferences.py:287
+msgid "Enable invitations"
+msgstr ""
+
+#: aleksis/core/preferences.py:295
+msgid "Length of invite code. (Default 3: abcde-acbde-abcde)"
+msgstr ""
+
+#: aleksis/core/preferences.py:303
+msgid "Size of packets. (Default 5: abcde)"
+msgstr ""
+
+#: aleksis/core/preferences.py:314
+msgid "Allowed Grant Flows for OAuth applications"
+msgstr ""
+
+#: aleksis/core/preferences.py:328
 msgid "Available languages"
 msgstr ""
 
-#: aleksis/core/preferences.py:285
+#: aleksis/core/preferences.py:341
 msgid "Send emails if data checks detect problems"
 msgstr ""
 
-#: aleksis/core/preferences.py:296
+#: aleksis/core/preferences.py:352
 msgid "Email recipients for data checks problem emails"
 msgstr ""
 
-#: aleksis/core/preferences.py:307
+#: aleksis/core/preferences.py:363
 msgid "Email recipient groups for data checks problem emails"
 msgstr ""
 
-#: aleksis/core/preferences.py:316
+#: aleksis/core/preferences.py:372
 msgid "Show dashboard to users without login"
 msgstr ""
 
-#: aleksis/core/preferences.py:325
+#: aleksis/core/preferences.py:381
 msgid "Allow users to edit their dashboard"
 msgstr ""
 
-#: aleksis/core/preferences.py:336
+#: aleksis/core/preferences.py:392
 msgid "Fields on person model which are editable by themselves."
 msgstr ""
 
-#: aleksis/core/preferences.py:350
+#: aleksis/core/preferences.py:407
 msgid "Editable fields on person model which should trigger a notification on change"
 msgstr ""
 
-#: aleksis/core/preferences.py:363
+#: aleksis/core/preferences.py:421
 msgid "Contact for notification if a person changes their data"
 msgstr ""
 
-#: aleksis/core/preferences.py:373
+#: aleksis/core/preferences.py:432
 msgid "PDF file expiration duration"
 msgstr ""
 
-#: aleksis/core/preferences.py:374
+#: aleksis/core/preferences.py:433
 msgid "in minutes"
 msgstr ""
 
-#: aleksis/core/preferences.py:384
+#: aleksis/core/preferences.py:443
 msgid "Automatically update the dashboard and its widgets"
 msgstr ""
 
-#: aleksis/core/preferences.py:394
+#: aleksis/core/preferences.py:453
 msgid "Automatically update the dashboard and its widgets sitewide"
 msgstr ""
 
-#: aleksis/core/settings.py:452
+#: aleksis/core/preferences.py:463
+msgid "Country for phone number parsing"
+msgstr ""
+
+#: aleksis/core/settings.py:529
 msgid "English"
 msgstr ""
 
-#: aleksis/core/settings.py:453
+#: aleksis/core/settings.py:530
 msgid "German"
 msgstr ""
 
-#: aleksis/core/tables.py:19
+#: aleksis/core/tables.py:24
 #: aleksis/core/templates/core/announcement/list.html:36
 #: aleksis/core/templates/core/group/full.html:24
-#: aleksis/core/templates/core/person/full.html:23
-#: aleksis/core/templates/oauth2_provider/application_detail.html:17
+#: aleksis/core/templates/core/person/full.html:26
+#: aleksis/core/templates/core/person/full.html:98
+#: aleksis/core/templates/oauth2_provider/application/detail.html:17
 msgid "Edit"
 msgstr ""
 
-#: aleksis/core/tables.py:21 aleksis/core/tables.py:89
+#: aleksis/core/tables.py:26 aleksis/core/tables.py:94
+#: aleksis/core/tables.py:138
 #: aleksis/core/templates/core/announcement/list.html:22
 msgid "Actions"
 msgstr ""
 
-#: aleksis/core/tables.py:56 aleksis/core/tables.py:57
-#: aleksis/core/tables.py:71 aleksis/core/tables.py:87
+#: aleksis/core/tables.py:61 aleksis/core/tables.py:62
+#: aleksis/core/tables.py:76 aleksis/core/tables.py:92
+#: aleksis/core/tables.py:136
 #: aleksis/core/templates/core/announcement/list.html:42
 #: aleksis/core/templates/core/group/full.html:31
 #: aleksis/core/templates/core/pages/delete.html:22
-#: aleksis/core/templates/core/person/full.html:30
-#: aleksis/core/templates/oauth2_provider/application_confirm_delete.html:20
-#: aleksis/core/templates/oauth2_provider/application_detail.html:21
+#: aleksis/core/templates/core/person/full.html:33
+#: aleksis/core/templates/core/person/full.html:105
+#: aleksis/core/templates/oauth2_provider/application/detail.html:21
 msgid "Delete"
 msgstr ""
 
-#: aleksis/core/templates/403.html:14 aleksis/core/templates/404.html:10
-#: aleksis/core/templates/500.html:10
-#: aleksis/core/templates/oauth2_provider/authorize.html:47
+#: aleksis/core/templates/403.html:14 aleksis/core/templates/500.html:10
+#: aleksis/core/templates/oauth2_provider/authorize.html:54
 #: aleksis/core/templates/oauth2_provider/authorized-oob.html:24
 msgid "Error"
 msgstr ""
@@ -1123,7 +1205,7 @@ msgid ""
 "          object."
 msgstr ""
 
-#: aleksis/core/templates/403.html:19 aleksis/core/templates/404.html:17
+#: aleksis/core/templates/403.html:19 aleksis/core/templates/404.html:16
 msgid ""
 "\n"
 "            If you think this is an error in AlekSIS, please contact your site\n"
@@ -1131,13 +1213,7 @@ msgid ""
 "          "
 msgstr ""
 
-#: aleksis/core/templates/404.html:10
-msgid ""
-"The requested page or object was not\n"
-"          found."
-msgstr ""
-
-#: aleksis/core/templates/404.html:13
+#: aleksis/core/templates/404.html:12
 msgid ""
 "\n"
 "            If you were redirected by a link on an external page,\n"
@@ -1177,11 +1253,11 @@ msgstr ""
 msgid "Account inactive"
 msgstr ""
 
-#: aleksis/core/templates/account/account_inactive.html:13
+#: aleksis/core/templates/account/account_inactive.html:14
 msgid "Account inactive."
 msgstr ""
 
-#: aleksis/core/templates/account/account_inactive.html:15
+#: aleksis/core/templates/account/account_inactive.html:17
 msgid ""
 "\n"
 "            This account is currently inactive. If you think this is an\n"
@@ -1194,14 +1270,13 @@ msgid "Hello!"
 msgstr ""
 
 #: aleksis/core/templates/account/email/base_message.txt:9
-#: aleksis/core/templates/templated_email/notification.email:22
-#: aleksis/core/templates/templated_email/notification.email:46
 msgid "Your AlekSIS team"
 msgstr ""
 
 #: aleksis/core/templates/account/email_confirm.html:5
 #: aleksis/core/templates/account/email_confirm.html:6
 #: aleksis/core/templates/account/email_confirm.html:17
+#: aleksis/core/templates/socialaccount/login.html:17
 msgid "Confirm"
 msgstr ""
 
@@ -1246,7 +1321,7 @@ msgstr ""
 #: aleksis/core/templates/account/password_reset_done.html:5
 #: aleksis/core/templates/account/verification_email_required.html:5
 #: aleksis/core/templates/account/verification_email_required.html:6
-#: aleksis/core/templates/two_factor/core/login.html:81
+#: aleksis/core/templates/two_factor/core/login.html:100
 msgid "Reset password"
 msgstr ""
 
@@ -1333,11 +1408,11 @@ msgstr ""
 msgid "Signup closed"
 msgstr ""
 
-#: aleksis/core/templates/account/signup_closed.html:13
+#: aleksis/core/templates/account/signup_closed.html:14
 msgid "Signup closed."
 msgstr ""
 
-#: aleksis/core/templates/account/signup_closed.html:15
+#: aleksis/core/templates/account/signup_closed.html:17
 msgid ""
 "\n"
 "            This sign up is currently closed. If you think this is an\n"
@@ -1375,11 +1450,6 @@ msgid ""
 "          "
 msgstr ""
 
-#: aleksis/core/templates/account/verification_sent.html:30
-#, python-format
-msgid "<strong>Note:</strong> you can still <a href=\"%(email_url)s\">change your e-mail address</a>"
-msgstr ""
-
 #: aleksis/core/templates/core/additional_field/edit.html:6
 #: aleksis/core/templates/core/additional_field/edit.html:7
 msgid "Edit additional field"
@@ -1423,24 +1493,24 @@ msgstr ""
 msgid "There are no announcements."
 msgstr ""
 
-#: aleksis/core/templates/core/base.html:60
+#: aleksis/core/templates/core/base.html:78
 msgid "Logged in as"
 msgstr ""
 
-#: aleksis/core/templates/core/base.html:154
-msgid "About AlekSIS — The Free School Information System"
+#: aleksis/core/templates/core/base.html:179
+msgid "About AlekSIS® — The Free School Information System"
 msgstr ""
 
-#: aleksis/core/templates/core/base.html:162
-msgid "Impress"
+#: aleksis/core/templates/core/base.html:187
+msgid "Imprint"
 msgstr ""
 
-#: aleksis/core/templates/core/base.html:170
+#: aleksis/core/templates/core/base.html:195
 msgid "Privacy Policy"
 msgstr ""
 
-#: aleksis/core/templates/core/base_print.html:72
-msgid "Powered by AlekSIS"
+#: aleksis/core/templates/core/base_print.html:74
+msgid "Powered by AlekSIS®"
 msgstr ""
 
 #: aleksis/core/templates/core/dashboard_widget/create.html:8
@@ -1525,15 +1595,15 @@ msgstr ""
 msgid "Options to solve the problem"
 msgstr ""
 
-#: aleksis/core/templates/core/data_check/list.html:62
+#: aleksis/core/templates/core/data_check/list.html:63
 msgid "Show object"
 msgstr ""
 
-#: aleksis/core/templates/core/data_check/list.html:84
+#: aleksis/core/templates/core/data_check/list.html:86
 msgid "Registered checks"
 msgstr ""
 
-#: aleksis/core/templates/core/data_check/list.html:88
+#: aleksis/core/templates/core/data_check/list.html:90
 msgid ""
 "\n"
 "            The system will check for the following problems:\n"
@@ -1626,7 +1696,7 @@ msgstr ""
 
 #: aleksis/core/templates/core/group/child_groups.html:93
 #: aleksis/core/templates/core/group/child_groups.html:128
-#: aleksis/core/templates/oauth2_provider/application_detail.html:9
+#: aleksis/core/templates/oauth2_provider/application/detail.html:9
 #: aleksis/core/templates/two_factor/_wizard_actions.html:15
 #: aleksis/core/templates/two_factor/_wizard_actions.html:20
 msgid "Back"
@@ -1655,7 +1725,8 @@ msgid "Edit group"
 msgstr ""
 
 #: aleksis/core/templates/core/group/full.html:38
-#: aleksis/core/templates/core/person/full.html:37
+#: aleksis/core/templates/core/person/full.html:40
+#: aleksis/core/templates/core/person/full.html:112
 msgid "Change preferences"
 msgstr ""
 
@@ -1744,36 +1815,46 @@ msgid "No notifications available yet."
 msgstr ""
 
 #: aleksis/core/templates/core/pages/about.html:6
-#: aleksis/core/templates/core/pages/about.html:15
-msgid "About AlekSIS"
+msgid "About AlekSIS®"
 msgstr ""
 
 #: aleksis/core/templates/core/pages/about.html:7
-msgid "AlekSIS – The Free School Information System"
+msgid "AlekSIS® – The Free School Information System"
+msgstr ""
+
+#: aleksis/core/templates/core/pages/about.html:15
+msgid "About AlekSIS"
 msgstr ""
 
 #: aleksis/core/templates/core/pages/about.html:17
 msgid ""
 "\n"
-"              This platform is powered by AlekSIS, a web-based school information system (SIS) which can be used\n"
+"              This platform is powered by AlekSIS®, a web-based school information system (SIS) which can be used\n"
 "              to manage and/or publish organisational artifacts of educational institutions. AlekSIS is free software and\n"
 "              can be used by anyone.\n"
 "            "
 msgstr ""
 
-#: aleksis/core/templates/core/pages/about.html:25
+#: aleksis/core/templates/core/pages/about.html:24
+msgid ""
+"\n"
+"              AlekSIS® is a registered trademark of the AlekSIS open source project, represented by Teckids e.V.\n"
+"            "
+msgstr ""
+
+#: aleksis/core/templates/core/pages/about.html:30
 msgid "Website of AlekSIS"
 msgstr ""
 
-#: aleksis/core/templates/core/pages/about.html:26
+#: aleksis/core/templates/core/pages/about.html:31
 msgid "Source code"
 msgstr ""
 
-#: aleksis/core/templates/core/pages/about.html:35
+#: aleksis/core/templates/core/pages/about.html:40
 msgid "Licence information"
 msgstr ""
 
-#: aleksis/core/templates/core/pages/about.html:37
+#: aleksis/core/templates/core/pages/about.html:42
 msgid ""
 "\n"
 "              The core and the official apps of AlekSIS are licenced under the EUPL, version 1.2 or later. For licence\n"
@@ -1782,23 +1863,23 @@ msgid ""
 "            "
 msgstr ""
 
-#: aleksis/core/templates/core/pages/about.html:45
+#: aleksis/core/templates/core/pages/about.html:50
 msgid "Free/Open Source Licence"
 msgstr ""
 
-#: aleksis/core/templates/core/pages/about.html:46
+#: aleksis/core/templates/core/pages/about.html:51
 msgid "Other Licence"
 msgstr ""
 
-#: aleksis/core/templates/core/pages/about.html:50
+#: aleksis/core/templates/core/pages/about.html:55
 msgid "Full licence text"
 msgstr ""
 
-#: aleksis/core/templates/core/pages/about.html:51
+#: aleksis/core/templates/core/pages/about.html:56
 msgid "More information about the EUPL"
 msgstr ""
 
-#: aleksis/core/templates/core/pages/about.html:90
+#: aleksis/core/templates/core/pages/about.html:95
 #, python-format
 msgid ""
 "\n"
@@ -1902,6 +1983,8 @@ msgid "Celery task results"
 msgstr ""
 
 #: aleksis/core/templates/core/pages/system_status.html:112
+#: aleksis/core/templates/templated_email/celery_failure.email:9
+#: aleksis/core/templates/templated_email/celery_failure.email:28
 msgid "Task"
 msgstr ""
 
@@ -1983,6 +2066,51 @@ msgid ""
 "          "
 msgstr ""
 
+#: aleksis/core/templates/core/perms/assign.html:12
+#: aleksis/core/templates/core/perms/assign.html:13
+msgid "Assign permission"
+msgstr ""
+
+#: aleksis/core/templates/core/perms/assign.html:17
+msgid "Selected permission"
+msgstr ""
+
+#: aleksis/core/templates/core/perms/assign.html:26
+msgid "Assign"
+msgstr ""
+
+#: aleksis/core/templates/core/perms/list.html:21
+msgid "Assign a new permission"
+msgstr ""
+
+#: aleksis/core/templates/core/perms/list.html:25
+msgid "Select"
+msgstr ""
+
+#: aleksis/core/templates/core/perms/list.html:34
+msgid "Global (user)"
+msgstr ""
+
+#: aleksis/core/templates/core/perms/list.html:38
+msgid "Global (group)"
+msgstr ""
+
+#: aleksis/core/templates/core/perms/list.html:42
+msgid "Object (user)"
+msgstr ""
+
+#: aleksis/core/templates/core/perms/list.html:46
+msgid "Object (group)"
+msgstr ""
+
+#: aleksis/core/templates/core/perms/list.html:52
+msgid "Filter permissions"
+msgstr ""
+
+#: aleksis/core/templates/core/perms/list.html:58
+msgid "Update"
+msgstr ""
+
 #: aleksis/core/templates/core/person/create.html:12
 #: aleksis/core/templates/core/person/create.html:13
 #: aleksis/core/templates/core/person/list.html:17
@@ -1994,17 +2122,25 @@ msgstr ""
 msgid "Edit person"
 msgstr ""
 
-#: aleksis/core/templates/core/person/full.html:44
-#: aleksis/core/templates/impersonate/list_users.html:7
-#: aleksis/core/templates/impersonate/list_users.html:8
+#: aleksis/core/templates/core/person/full.html:47
+#: aleksis/core/templates/core/person/full.html:119
 msgid "Impersonate"
 msgstr ""
 
-#: aleksis/core/templates/core/person/full.html:50
+#: aleksis/core/templates/core/person/full.html:54
+#: aleksis/core/templates/core/person/full.html:126
+msgid "Invite user"
+msgstr ""
+
+#: aleksis/core/templates/core/person/full.html:133
 msgid "Contact details"
 msgstr ""
 
-#: aleksis/core/templates/core/person/full.html:130
+#: aleksis/core/templates/core/person/full.html:224
+msgid "This person didn't upload a personal photo."
+msgstr ""
+
+#: aleksis/core/templates/core/person/full.html:232
 msgid "Children"
 msgstr ""
 
@@ -2044,77 +2180,138 @@ msgstr ""
 msgid "Save preferences"
 msgstr ""
 
-#: aleksis/core/templates/oauth2_provider/application_confirm_delete.html:5
-#: aleksis/core/templates/oauth2_provider/application_confirm_delete.html:6
-msgid "Delete application"
+#: aleksis/core/templates/invitations/disabled.html:5
+msgid "The invite feature is disabled"
+msgstr ""
+
+#: aleksis/core/templates/invitations/disabled.html:13
+msgid "The invite feature is disabled."
+msgstr ""
+
+#: aleksis/core/templates/invitations/disabled.html:15
+msgid "To enable it, switch on the corresponding checkbox in the authentication section of the "
+msgstr ""
+
+#: aleksis/core/templates/invitations/disabled.html:16
+msgid "site preferences page"
+msgstr ""
+
+#: aleksis/core/templates/invitations/enter.html:21
+msgid "Accept your invitation"
 msgstr ""
 
-#: aleksis/core/templates/oauth2_provider/application_confirm_delete.html:12
+#: aleksis/core/templates/invitations/enter.html:25
+msgid ""
+"\n"
+"                Please enter your invitation code to register\n"
+"                your new user account:\n"
+"              "
+msgstr ""
+
+#: aleksis/core/templates/invitations/enter.html:37
+msgid "Accept invite"
+msgstr ""
+
+#: aleksis/core/templates/invitations/forms/_invite.html:9
+#: aleksis/core/templates/invitations/forms/_invite.html:10
+#: aleksis/core/templates/invitations/forms/_invite.html:21
+msgid "Invite"
+msgstr ""
+
+#: aleksis/core/templates/invitations/forms/_invite.html:17
+msgid "Invite by email address"
+msgstr ""
+
+#: aleksis/core/templates/invitations/forms/_invite.html:26
+msgid "Generate invitation code"
+msgstr ""
+
+#: aleksis/core/templates/invitations/forms/_invite.html:29
+msgid "Generate code"
+msgstr ""
+
+#: aleksis/core/templates/invitations/forms/_invite.html:33
+msgid "Invitations"
+msgstr ""
+
+#: aleksis/core/templates/invitations/messages/invite_accepted.txt:3
 #, python-format
-msgid "Are you sure to delete the application %(application_name)s?"
+msgid "The invitation for %(email)s has been accepted."
+msgstr ""
+
+#: aleksis/core/templates/oauth2_provider/application/create.html:5
+#: aleksis/core/templates/oauth2_provider/application/create.html:6
+msgid "Register OAuth2 Application"
 msgstr ""
 
-#: aleksis/core/templates/oauth2_provider/application_confirm_delete.html:24
-#: aleksis/core/templates/oauth2_provider/application_form.html:18
+#: aleksis/core/templates/oauth2_provider/application/create.html:14
+#: aleksis/core/templates/oauth2_provider/application/edit.html:14
 #: aleksis/core/templates/oauth2_provider/authorized-token-delete.html:24
 #: aleksis/core/templates/two_factor/_wizard_actions.html:6
 msgid "Cancel"
 msgstr ""
 
-#: aleksis/core/templates/oauth2_provider/application_detail.html:27
+#: aleksis/core/templates/oauth2_provider/application/detail.html:5
+msgid "OAuth2 Application"
+msgstr ""
+
+#: aleksis/core/templates/oauth2_provider/application/detail.html:39
 msgid "Client id"
 msgstr ""
 
-#: aleksis/core/templates/oauth2_provider/application_detail.html:35
+#: aleksis/core/templates/oauth2_provider/application/detail.html:47
 msgid "Client secret"
 msgstr ""
 
-#: aleksis/core/templates/oauth2_provider/application_detail.html:43
+#: aleksis/core/templates/oauth2_provider/application/detail.html:55
 msgid "Client type"
 msgstr ""
 
-#: aleksis/core/templates/oauth2_provider/application_detail.html:51
-msgid "Authorization Grant Type"
+#: aleksis/core/templates/oauth2_provider/application/detail.html:63
+msgid "Allowed scopes"
 msgstr ""
 
-#: aleksis/core/templates/oauth2_provider/application_detail.html:59
+#: aleksis/core/templates/oauth2_provider/application/detail.html:71
 msgid "Redirect URIs"
 msgstr ""
 
-#: aleksis/core/templates/oauth2_provider/application_form.html:5
-msgid "Create OAuth2 Application"
+#: aleksis/core/templates/oauth2_provider/application/detail.html:79
+msgid "Skip Authorisation"
 msgstr ""
 
-#: aleksis/core/templates/oauth2_provider/application_form.html:10
-msgid "Edit application"
+#: aleksis/core/templates/oauth2_provider/application/edit.html:5
+#: aleksis/core/templates/oauth2_provider/application/edit.html:6
+msgid "Edit OAuth2 Application"
 msgstr ""
 
-#: aleksis/core/templates/oauth2_provider/application_list.html:8
-msgid "OAuth2 applications"
+#: aleksis/core/templates/oauth2_provider/application/list.html:11
+msgid "Register new application"
 msgstr ""
 
-#: aleksis/core/templates/oauth2_provider/application_list.html:12
-msgid "Register new applications"
-msgstr ""
-
-#: aleksis/core/templates/oauth2_provider/application_list.html:23
+#: aleksis/core/templates/oauth2_provider/application/list.html:25
 msgid "No applications defined."
 msgstr ""
 
 #: aleksis/core/templates/oauth2_provider/authorize.html:5
-#: aleksis/core/templates/oauth2_provider/authorize.html:16
+#: aleksis/core/templates/socialaccount/login.html:5
+#: aleksis/core/templates/socialaccount/login.html:6
 msgid "Authorize"
 msgstr ""
 
-#: aleksis/core/templates/oauth2_provider/authorize.html:18
+#: aleksis/core/templates/oauth2_provider/authorize.html:23
+#, python-format
+msgid "Authorize %(name)s"
+msgstr ""
+
+#: aleksis/core/templates/oauth2_provider/authorize.html:25
 msgid "The application requests access to the following scopes:"
 msgstr ""
 
-#: aleksis/core/templates/oauth2_provider/authorize.html:33
+#: aleksis/core/templates/oauth2_provider/authorize.html:40
 msgid "Allow"
 msgstr ""
 
-#: aleksis/core/templates/oauth2_provider/authorize.html:36
+#: aleksis/core/templates/oauth2_provider/authorize.html:43
 msgid "Disallow"
 msgstr ""
 
@@ -2217,6 +2414,20 @@ msgstr ""
 msgid "Add a Third-party Account"
 msgstr ""
 
+#: aleksis/core/templates/socialaccount/login.html:12
+#, python-format
+msgid "You are about to connect a new third party account from %(provider)s."
+msgstr ""
+
+#: aleksis/core/templates/socialaccount/login.html:23
+#, python-format
+msgid "You are about to sign in using a third party account from %(provider)s."
+msgstr ""
+
+#: aleksis/core/templates/socialaccount/login.html:28
+msgid "Continue"
+msgstr ""
+
 #: aleksis/core/templates/socialaccount/login_cancelled.html:5
 #: aleksis/core/templates/socialaccount/login_cancelled.html:6
 #: aleksis/core/templates/socialaccount/login_cancelled.html:13
@@ -2261,26 +2472,60 @@ msgid ""
 "        "
 msgstr ""
 
-#: aleksis/core/templates/templated_email/data_checks.email:4
-msgid "The system detected some new problems with your data."
+#: aleksis/core/templates/templated_email/base.email:5
+#: aleksis/core/templates/templated_email/base.email:16
+msgid "Hello"
 msgstr ""
 
-#: aleksis/core/templates/templated_email/data_checks.email:8
-#: aleksis/core/templates/templated_email/data_checks.email:24
-#: aleksis/core/templates/templated_email/person_changed.email:8
-#: aleksis/core/templates/templated_email/person_changed.email:20
-msgid "Hello,"
+#: aleksis/core/templates/templated_email/celery_failure.email:4
+#, python-format
+msgid "Celery task %(task_name)s failed!"
 msgstr ""
 
-#: aleksis/core/templates/templated_email/data_checks.email:10
+#: aleksis/core/templates/templated_email/celery_failure.email:7
+#, python-format
+msgid "the celery task %(task_name)s failed with following information:"
+msgstr ""
+
+#: aleksis/core/templates/templated_email/celery_failure.email:10
+#: aleksis/core/templates/templated_email/celery_failure.email:29
+msgid "Task ID"
+msgstr ""
+
+#: aleksis/core/templates/templated_email/celery_failure.email:11
+#: aleksis/core/templates/templated_email/celery_failure.email:30
+msgid "Raised exception"
+msgstr ""
+
+#: aleksis/core/templates/templated_email/celery_failure.email:12
+#: aleksis/core/templates/templated_email/celery_failure.email:31
+msgid "Positional arguments"
+msgstr ""
+
+#: aleksis/core/templates/templated_email/celery_failure.email:15
+#: aleksis/core/templates/templated_email/celery_failure.email:38
+msgid "Keyword arguments"
+msgstr ""
+
+#: aleksis/core/templates/templated_email/celery_failure.email:22
+#, python-format
 msgid ""
 "\n"
-"  the system detected some new problems with your data.\n"
-"  Please take some time to inspect them and solve the issues or mark them as ignored.\n"
-" "
+"      the celery task %(task_name)s failed with following information:\n"
+"    "
 msgstr ""
 
-#: aleksis/core/templates/templated_email/data_checks.email:26
+#: aleksis/core/templates/templated_email/data_checks.email:3
+msgid "The system detected some new problems with your data."
+msgstr ""
+
+#: aleksis/core/templates/templated_email/data_checks.email:6
+msgid ""
+"the system detected some new problems with your data.\n"
+"Please take some time to inspect them and solve the issues or mark them as ignored."
+msgstr ""
+
+#: aleksis/core/templates/templated_email/data_checks.email:15
 msgid ""
 "\n"
 "   the system detected some new problems with your data.\n"
@@ -2288,43 +2533,40 @@ msgid ""
 "  "
 msgstr ""
 
-#: aleksis/core/templates/templated_email/data_checks.email:34
+#: aleksis/core/templates/templated_email/data_checks.email:23
 msgid "Problem description"
 msgstr ""
 
-#: aleksis/core/templates/templated_email/data_checks.email:35
+#: aleksis/core/templates/templated_email/data_checks.email:24
 msgid "Count of objects with new problems"
 msgstr ""
 
-#: aleksis/core/templates/templated_email/notification.email:3
+#: aleksis/core/templates/templated_email/notification.email:4
 msgid "New notification for"
 msgstr ""
 
 #: aleksis/core/templates/templated_email/notification.email:6
-#: aleksis/core/templates/templated_email/notification.email:27
+#: aleksis/core/templates/templated_email/notification.email:20
 #, python-format
-msgid "Dear %(notification_user)s,"
+msgid "Hello %(notification_user)s,"
 msgstr ""
 
-#: aleksis/core/templates/templated_email/notification.email:8
-#: aleksis/core/templates/templated_email/notification.email:29
+#: aleksis/core/templates/templated_email/notification.email:9
+#: aleksis/core/templates/templated_email/notification.email:23
 msgid "we got a new notification for you:"
 msgstr ""
 
 #: aleksis/core/templates/templated_email/notification.email:15
-#: aleksis/core/templates/templated_email/notification.email:35
+#: aleksis/core/templates/templated_email/notification.email:29
 msgid "More information"
 msgstr ""
 
-#: aleksis/core/templates/templated_email/notification.email:18
+#: aleksis/core/templates/templated_email/notification.email:17
 #, python-format
-msgid ""
-"\n"
-"        Sent by %(trans_sender)s at %(trans_created_at)s\n"
-"    "
+msgid "Sent by %(trans_sender)s at %(trans_created_at)s"
 msgstr ""
 
-#: aleksis/core/templates/templated_email/notification.email:40
+#: aleksis/core/templates/templated_email/notification.email:34
 #, python-format
 msgid ""
 "\n"
@@ -2337,15 +2579,12 @@ msgstr ""
 msgid "%(person)s changed their data!"
 msgstr ""
 
-#: aleksis/core/templates/templated_email/person_changed.email:10
+#: aleksis/core/templates/templated_email/person_changed.email:7
 #, python-format
-msgid ""
-"\n"
-"   the person %(person)s recently changed the following fields:\n"
-" "
+msgid "the person %(person)s recently changed the following fields:"
 msgstr ""
 
-#: aleksis/core/templates/templated_email/person_changed.email:22
+#: aleksis/core/templates/templated_email/person_changed.email:15
 #, python-format
 msgid ""
 "\n"
@@ -2396,21 +2635,30 @@ msgstr ""
 msgid "Generate Tokens"
 msgstr ""
 
-#: aleksis/core/templates/two_factor/core/login.html:20
+#: aleksis/core/templates/two_factor/core/login.html:27
+#, python-format
+msgid "Login for %(name)s"
+msgstr ""
+
+#: aleksis/core/templates/two_factor/core/login.html:30
 msgid "Login with username and password"
 msgstr ""
 
-#: aleksis/core/templates/two_factor/core/login.html:28
+#: aleksis/core/templates/two_factor/core/login.html:38
 msgid ""
 "You have no permission to view this page. Please login with an other\n"
 "                    account."
 msgstr ""
 
-#: aleksis/core/templates/two_factor/core/login.html:36
+#: aleksis/core/templates/two_factor/core/login.html:47
+msgid "Please login with your account to use the external application."
+msgstr ""
+
+#: aleksis/core/templates/two_factor/core/login.html:54
 msgid "Please login to see this page."
 msgstr ""
 
-#: aleksis/core/templates/two_factor/core/login.html:46
+#: aleksis/core/templates/two_factor/core/login.html:65
 msgid ""
 "\n"
 "                        We are calling your phone right now, please enter the\n"
@@ -2418,7 +2666,7 @@ msgid ""
 "                      "
 msgstr ""
 
-#: aleksis/core/templates/two_factor/core/login.html:51
+#: aleksis/core/templates/two_factor/core/login.html:70
 msgid ""
 "\n"
 "                        We sent you a text message, please enter the tokens we\n"
@@ -2426,7 +2674,7 @@ msgid ""
 "                      "
 msgstr ""
 
-#: aleksis/core/templates/two_factor/core/login.html:56
+#: aleksis/core/templates/two_factor/core/login.html:75
 msgid ""
 "\n"
 "                        Please enter the tokens generated by your token\n"
@@ -2434,7 +2682,7 @@ msgid ""
 "                      "
 msgstr ""
 
-#: aleksis/core/templates/two_factor/core/login.html:62
+#: aleksis/core/templates/two_factor/core/login.html:81
 msgid ""
 "\n"
 "                      Use this form for entering backup tokens for logging in.\n"
@@ -2443,23 +2691,23 @@ msgid ""
 "                    "
 msgstr ""
 
-#: aleksis/core/templates/two_factor/core/login.html:90
+#: aleksis/core/templates/two_factor/core/login.html:109
 msgid "Device currently not available?"
 msgstr ""
 
-#: aleksis/core/templates/two_factor/core/login.html:92
+#: aleksis/core/templates/two_factor/core/login.html:111
 msgid "Or, alternatively, use one of your backup phones:"
 msgstr ""
 
-#: aleksis/core/templates/two_factor/core/login.html:102
+#: aleksis/core/templates/two_factor/core/login.html:121
 msgid "As a last resort, you can use a backup token:"
 msgstr ""
 
-#: aleksis/core/templates/two_factor/core/login.html:105
+#: aleksis/core/templates/two_factor/core/login.html:124
 msgid "Use Backup Token"
 msgstr ""
 
-#: aleksis/core/templates/two_factor/core/login.html:116
+#: aleksis/core/templates/two_factor/core/login.html:135
 msgid "Use alternative login options"
 msgstr ""
 
@@ -2696,138 +2944,187 @@ msgid ""
 "      "
 msgstr ""
 
-#: aleksis/core/util/notifications.py:65
+#: aleksis/core/util/notifications.py:63
 msgid "E-Mail"
 msgstr ""
 
-#: aleksis/core/util/notifications.py:66
+#: aleksis/core/util/notifications.py:64
 msgid "SMS"
 msgstr ""
 
-#: aleksis/core/util/pdf.py:105
+#: aleksis/core/util/pdf.py:118
 msgid "Progress: Generate PDF file"
 msgstr ""
 
-#: aleksis/core/util/pdf.py:106
+#: aleksis/core/util/pdf.py:119
 msgid "Generating PDF file …"
 msgstr ""
 
-#: aleksis/core/util/pdf.py:107
+#: aleksis/core/util/pdf.py:120
 msgid "The PDF file has been generated successfully."
 msgstr ""
 
-#: aleksis/core/util/pdf.py:108
+#: aleksis/core/util/pdf.py:121
 msgid "There was a problem while generating the PDF file."
 msgstr ""
 
-#: aleksis/core/util/pdf.py:111
+#: aleksis/core/util/pdf.py:124
 msgid "Download PDF"
 msgstr ""
 
-#: aleksis/core/views.py:251
+#: aleksis/core/views.py:285
 msgid "The school term has been created."
 msgstr ""
 
-#: aleksis/core/views.py:263
+#: aleksis/core/views.py:297
 msgid "The school term has been saved."
 msgstr ""
 
-#: aleksis/core/views.py:387
+#: aleksis/core/views.py:417
 msgid "The child groups were successfully saved."
 msgstr ""
 
-#: aleksis/core/views.py:406 aleksis/core/views.py:416
+#: aleksis/core/views.py:436 aleksis/core/views.py:446
 msgid "The person has been saved."
 msgstr ""
 
-#: aleksis/core/views.py:466
+#: aleksis/core/views.py:496
 msgid "The group has been saved."
 msgstr ""
 
-#: aleksis/core/views.py:563
+#: aleksis/core/views.py:593
 msgid "The announcement has been saved."
 msgstr ""
 
-#: aleksis/core/views.py:579
+#: aleksis/core/views.py:609
 msgid "The announcement has been deleted."
 msgstr ""
 
-#: aleksis/core/views.py:663
+#: aleksis/core/views.py:677
+msgid "The requested preference registry does not exist"
+msgstr ""
+
+#: aleksis/core/views.py:696
 msgid "The preferences have been saved successfully."
 msgstr ""
 
-#: aleksis/core/views.py:687
+#: aleksis/core/views.py:720
 msgid "The person has been deleted."
 msgstr ""
 
-#: aleksis/core/views.py:701
+#: aleksis/core/views.py:734
 msgid "The group has been deleted."
 msgstr ""
 
-#: aleksis/core/views.py:733
+#: aleksis/core/views.py:766
 msgid "The additional_field has been saved."
 msgstr ""
 
-#: aleksis/core/views.py:767
+#: aleksis/core/views.py:800
 msgid "The additional field has been deleted."
 msgstr ""
 
-#: aleksis/core/views.py:792
+#: aleksis/core/views.py:825
 msgid "The group type has been saved."
 msgstr ""
 
-#: aleksis/core/views.py:822
+#: aleksis/core/views.py:855
 msgid "The group type has been deleted."
 msgstr ""
 
-#: aleksis/core/views.py:855
+#: aleksis/core/views.py:888
 msgid "Progress: Run data checks"
 msgstr ""
 
-#: aleksis/core/views.py:856
+#: aleksis/core/views.py:889
 msgid "Run data checks …"
 msgstr ""
 
-#: aleksis/core/views.py:857
+#: aleksis/core/views.py:890
 msgid "The data checks were run successfully."
 msgstr ""
 
-#: aleksis/core/views.py:858
+#: aleksis/core/views.py:891
 msgid "There was a problem while running data checks."
 msgstr ""
 
-#: aleksis/core/views.py:874
+#: aleksis/core/views.py:907
 #, python-brace-format
 msgid "The solve option '{solve_option_obj.verbose_name}' "
 msgstr ""
 
-#: aleksis/core/views.py:916
+#: aleksis/core/views.py:917
+msgid "The requested solve option does not exist"
+msgstr ""
+
+#: aleksis/core/views.py:949
 msgid "The dashboard widget has been saved."
 msgstr ""
 
-#: aleksis/core/views.py:946
+#: aleksis/core/views.py:979
 msgid "The dashboard widget has been created."
 msgstr ""
 
-#: aleksis/core/views.py:956
+#: aleksis/core/views.py:989
 msgid "The dashboard widget has been deleted."
 msgstr ""
 
-#: aleksis/core/views.py:1023
+#: aleksis/core/views.py:1060
 msgid "Your dashboard configuration has been saved successfully."
 msgstr ""
 
-#: aleksis/core/views.py:1025
+#: aleksis/core/views.py:1062
 msgid "The configuration of the default dashboard has been saved successfully."
 msgstr ""
 
-#: aleksis/core/views.py:1153
+#: aleksis/core/views.py:1138
+#, python-brace-format
+msgid "The invitation was successfully created. The invitation code is {code}"
+msgstr ""
+
+#: aleksis/core/views.py:1229
+msgid "We have successfully assigned the permissions."
+msgstr ""
+
+#: aleksis/core/views.py:1239
+msgid "The global user permission has been deleted."
+msgstr ""
+
+#: aleksis/core/views.py:1249
+msgid "The global group permission has been deleted."
+msgstr ""
+
+#: aleksis/core/views.py:1259
+msgid "The object user permission has been deleted."
+msgstr ""
+
+#: aleksis/core/views.py:1269
+msgid "The object group permission has been deleted."
+msgstr ""
+
+#: aleksis/core/views.py:1337
+msgid "The requested PDF file does not exist"
+msgstr ""
+
+#: aleksis/core/views.py:1346 aleksis/core/views.py:1350
+msgid "The requested task does not exist or is not accessible"
+msgstr ""
+
+#: aleksis/core/views.py:1388
 msgid "The third-party account could not be disconnected because it is the only login method available."
 msgstr ""
 
-#: aleksis/core/views.py:1160
+#: aleksis/core/views.py:1395
 msgid "The third-party account has been successfully disconnected."
 msgstr ""
 
+#: aleksis/core/views.py:1466
+msgid "Person was invited successfully and an email with further instructions has been send to them."
+msgstr ""
+
+#: aleksis/core/views.py:1477
+msgid "Person was already invited."
+msgstr ""
+
 #~ msgid "Norwegian (bokmål)"
 #~ msgstr "Norsk (bokmål)"
diff --git a/aleksis/core/locale/nb_NO/LC_MESSAGES/djangojs.po b/aleksis/core/locale/nb_NO/LC_MESSAGES/djangojs.po
index dc6d028e5442a3666f35a7cb46e39e5c01cdd3f9..dfec73c5c894d3d686092593be9c21a811cd0535 100644
--- a/aleksis/core/locale/nb_NO/LC_MESSAGES/djangojs.po
+++ b/aleksis/core/locale/nb_NO/LC_MESSAGES/djangojs.po
@@ -8,7 +8,7 @@ msgid ""
 msgstr ""
 "Project-Id-Version: PACKAGE VERSION\n"
 "Report-Msgid-Bugs-To: \n"
-"POT-Creation-Date: 2021-10-28 16:18+0200\n"
+"POT-Creation-Date: 2022-02-08 23:16+0000\n"
 "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
 "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
 "Language-Team: LANGUAGE <LL@li.org>\n"
@@ -17,18 +17,18 @@ msgstr ""
 "Content-Type: text/plain; charset=UTF-8\n"
 "Content-Transfer-Encoding: 8bit\n"
 
-#: aleksis/core/static/js/main.js:15
+#: aleksis/core/static/js/main.js:66
 msgid "Today"
 msgstr ""
 
-#: aleksis/core/static/js/main.js:16
+#: aleksis/core/static/js/main.js:67
 msgid "Cancel"
 msgstr ""
 
-#: aleksis/core/static/js/main.js:17
+#: aleksis/core/static/js/main.js:68
 msgid "OK"
 msgstr ""
 
-#: aleksis/core/static/js/main.js:127
+#: aleksis/core/static/js/main.js:191
 msgid "This page may contain outdated information since there is no internet connection."
 msgstr ""
diff --git a/aleksis/core/locale/tr_TR/LC_MESSAGES/django.po b/aleksis/core/locale/tr_TR/LC_MESSAGES/django.po
index c1f28664922a7b5cbdac80d557c812092765b16f..2973720cd0f3f85a06ca9d68c21a2c09a92961a5 100644
--- a/aleksis/core/locale/tr_TR/LC_MESSAGES/django.po
+++ b/aleksis/core/locale/tr_TR/LC_MESSAGES/django.po
@@ -8,7 +8,7 @@ msgid ""
 msgstr ""
 "Project-Id-Version: AlekSIS (School Information System) 0.1\n"
 "Report-Msgid-Bugs-To: \n"
-"POT-Creation-Date: 2021-10-28 16:18+0200\n"
+"POT-Creation-Date: 2022-02-08 23:16+0000\n"
 "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
 "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
 "Language-Team: LANGUAGE <LL@li.org>\n"
@@ -17,31 +17,38 @@ msgstr ""
 "Content-Type: text/plain; charset=UTF-8\n"
 "Content-Transfer-Encoding: 8bit\n"
 
-#: aleksis/core/apps.py:150
+#: aleksis/core/apps.py:166
 msgid "OpenID Connect scope"
 msgstr ""
 
-#: aleksis/core/apps.py:151
+#: aleksis/core/apps.py:167
 msgid "Given name, family name, link to profile and picture if existing."
 msgstr ""
 
-#: aleksis/core/apps.py:152
+#: aleksis/core/apps.py:168
 msgid "Full home postal address"
 msgstr ""
 
-#: aleksis/core/apps.py:153
+#: aleksis/core/apps.py:169
 msgid "Email address"
 msgstr ""
 
-#: aleksis/core/apps.py:154
+#: aleksis/core/apps.py:170
 msgid "Home and mobile phone"
 msgstr ""
 
-#: aleksis/core/data_checks.py:55
+#: aleksis/core/apps.py:171 aleksis/core/forms.py:220 aleksis/core/menus.py:265
+#: aleksis/core/models.py:462 aleksis/core/templates/core/group/list.html:8
+#: aleksis/core/templates/core/group/list.html:9
+#: aleksis/core/templates/core/person/full.html:250
+msgid "Groups"
+msgstr ""
+
+#: aleksis/core/data_checks.py:56
 msgid "Ignore problem"
 msgstr ""
 
-#: aleksis/core/data_checks.py:184
+#: aleksis/core/data_checks.py:185
 #, python-brace-format
 msgid "Solve option '{solve_option_obj.verbose_name}' "
 msgstr ""
@@ -58,8 +65,8 @@ msgstr ""
 msgid "The DashboardWidget was reported broken automatically."
 msgstr ""
 
-#: aleksis/core/filters.py:37 aleksis/core/templates/core/base.html:83
-#: aleksis/core/templates/core/base.html:84
+#: aleksis/core/filters.py:42 aleksis/core/templates/core/base.html:105
+#: aleksis/core/templates/core/base.html:106
 #: aleksis/core/templates/core/group/list.html:20
 #: aleksis/core/templates/core/person/list.html:24
 #: aleksis/core/templates/search/search.html:7
@@ -67,132 +74,181 @@ msgstr ""
 msgid "Search"
 msgstr ""
 
-#: aleksis/core/filters.py:53
+#: aleksis/core/filters.py:59
 msgid "Search by name"
 msgstr ""
 
-#: aleksis/core/filters.py:65
+#: aleksis/core/filters.py:71
 msgid "Search by contact details"
 msgstr ""
 
-#: aleksis/core/forms.py:41 aleksis/core/forms.py:387
+#: aleksis/core/filters.py:92
+msgid "Permission"
+msgstr ""
+
+#: aleksis/core/filters.py:100
+msgid "Content type"
+msgstr ""
+
+#: aleksis/core/filters.py:113 aleksis/core/models.py:688
+msgid "User"
+msgstr ""
+
+#: aleksis/core/filters.py:135 aleksis/core/models.py:461
+msgid "Group"
+msgstr ""
+
+#: aleksis/core/forms.py:50 aleksis/core/forms.py:581
 msgid "Base data"
 msgstr ""
 
-#: aleksis/core/forms.py:47
+#: aleksis/core/forms.py:55
 msgid "Address"
 msgstr ""
 
-#: aleksis/core/forms.py:48
+#: aleksis/core/forms.py:56 aleksis/core/forms.py:590
 msgid "Contact data"
 msgstr ""
 
-#: aleksis/core/forms.py:50
+#: aleksis/core/forms.py:58
 msgid "Advanced personal data"
 msgstr ""
 
-#: aleksis/core/forms.py:93
+#: aleksis/core/forms.py:106
 msgid "New user"
 msgstr ""
 
-#: aleksis/core/forms.py:93
+#: aleksis/core/forms.py:106
 msgid "Create a new account"
 msgstr ""
 
-#: aleksis/core/forms.py:124
+#: aleksis/core/forms.py:132
 msgid "You cannot set a new username when also selecting an existing user."
 msgstr ""
 
-#: aleksis/core/forms.py:128
+#: aleksis/core/forms.py:136
 msgid "This username is already in use."
 msgstr ""
 
-#: aleksis/core/forms.py:145 aleksis/core/models.py:117
+#: aleksis/core/forms.py:153 aleksis/core/models.py:130
 msgid "School term"
 msgstr ""
 
-#: aleksis/core/forms.py:146
+#: aleksis/core/forms.py:154
 msgid "Common data"
 msgstr ""
 
-#: aleksis/core/forms.py:147 aleksis/core/forms.py:196
-#: aleksis/core/menus.py:238 aleksis/core/models.py:140
+#: aleksis/core/forms.py:155 aleksis/core/forms.py:207
+#: aleksis/core/menus.py:254 aleksis/core/models.py:153
 #: aleksis/core/templates/core/person/list.html:8
 #: aleksis/core/templates/core/person/list.html:9
 msgid "Persons"
 msgstr ""
 
-#: aleksis/core/forms.py:148
+#: aleksis/core/forms.py:156 aleksis/core/forms.py:592
 msgid "Additional data"
 msgstr ""
 
-#: aleksis/core/forms.py:188 aleksis/core/forms.py:191
-#: aleksis/core/models.py:60
-msgid "Date"
+#: aleksis/core/forms.py:157 aleksis/core/models.py:206
+#: aleksis/core/models.py:514
+msgid "Photo"
 msgstr ""
 
-#: aleksis/core/forms.py:189 aleksis/core/forms.py:192
-#: aleksis/core/models.py:68
-msgid "Time"
+#: aleksis/core/forms.py:199 aleksis/core/forms.py:202
+#: aleksis/core/models.py:73
+msgid "Date"
 msgstr ""
 
-#: aleksis/core/forms.py:209 aleksis/core/menus.py:249
-#: aleksis/core/models.py:398 aleksis/core/templates/core/group/list.html:8
-#: aleksis/core/templates/core/group/list.html:9
-#: aleksis/core/templates/core/person/full.html:144
-msgid "Groups"
+#: aleksis/core/forms.py:200 aleksis/core/forms.py:203
+#: aleksis/core/models.py:81
+msgid "Time"
 msgstr ""
 
-#: aleksis/core/forms.py:219
+#: aleksis/core/forms.py:233
 msgid "From when until when should the announcement be displayed?"
 msgstr ""
 
-#: aleksis/core/forms.py:222
+#: aleksis/core/forms.py:236
 msgid "Who should see the announcement?"
 msgstr ""
 
-#: aleksis/core/forms.py:223
+#: aleksis/core/forms.py:237
 msgid "Write your announcement:"
 msgstr ""
 
-#: aleksis/core/forms.py:262
+#: aleksis/core/forms.py:276
 msgid "You are not allowed to create announcements which are only valid in the past."
 msgstr ""
 
-#: aleksis/core/forms.py:266
+#: aleksis/core/forms.py:280
 msgid "The from date and time must be earlier then the until date and time."
 msgstr ""
 
-#: aleksis/core/forms.py:275
+#: aleksis/core/forms.py:289
 msgid "You need at least one recipient."
 msgstr ""
 
-#: aleksis/core/forms.py:389
-msgid "Account data"
+#: aleksis/core/forms.py:398
+msgid "Invitation code"
 msgstr ""
 
-#: aleksis/core/forms.py:391
-msgid "Consents"
+#: aleksis/core/forms.py:399
+msgid "Please enter your invitation code."
 msgstr ""
 
-#: aleksis/core/forms.py:396
-msgid "Password"
+#: aleksis/core/forms.py:418 aleksis/core/models.py:181
+msgid "First name"
 msgstr ""
 
-#: aleksis/core/forms.py:402
-msgid "Password (again)"
+#: aleksis/core/forms.py:419 aleksis/core/models.py:182
+msgid "Last name"
 msgstr ""
 
-#: aleksis/core/forms.py:411
-#, python-brace-format
-msgid "I have read the <a href='{privacy_policy}'>Privacy policy</a> and agree with them."
+#: aleksis/core/forms.py:428
+msgid "A person is using this e-mail address"
+msgstr ""
+
+#: aleksis/core/forms.py:456
+msgid "Who should get the permission?"
+msgstr ""
+
+#: aleksis/core/forms.py:457
+msgid "On what?"
+msgstr ""
+
+#: aleksis/core/forms.py:483
+msgid "Select objects which the permission should be granted for:"
+msgstr ""
+
+#: aleksis/core/forms.py:486
+msgid "Grant the permission for all objects"
+msgstr ""
+
+#: aleksis/core/forms.py:494
+msgid "You must select at least one group or person which should get the permission."
+msgstr ""
+
+#: aleksis/core/forms.py:499
+msgid "You must grant the permission to all objects and/or to some objects."
 msgstr ""
 
-#: aleksis/core/forms.py:435
-msgid "You must type the same password each time."
+#: aleksis/core/forms.py:586
+msgid "Adress data"
 msgstr ""
 
-#: aleksis/core/forms.py:580
+#: aleksis/core/forms.py:598
+msgid "Account data"
+msgstr ""
+
+#: aleksis/core/forms.py:605
+msgid "Password"
+msgstr ""
+
+#: aleksis/core/forms.py:608
+msgid "Password (again)"
+msgstr ""
+
+#: aleksis/core/forms.py:775
 msgid "No valid selection."
 msgstr ""
 
@@ -200,67 +256,65 @@ msgstr ""
 msgid "There are unresolved data problems."
 msgstr ""
 
-#: aleksis/core/health_checks.py:38
-msgid "The backup folder doesn't exist."
+#: aleksis/core/health_checks.py:40
+msgid "Error accessing backup storage: {}"
 msgstr ""
 
-#: aleksis/core/health_checks.py:47
-#, python-brace-format
-msgid "Last backup {time_gone_since_backup}!"
+#: aleksis/core/health_checks.py:50
+msgid "Last backup {}!"
 msgstr ""
 
-#: aleksis/core/health_checks.py:49
+#: aleksis/core/health_checks.py:52
 msgid "No backup found!"
 msgstr ""
 
-#: aleksis/core/health_checks.py:76
+#: aleksis/core/health_checks.py:79
 msgid "No backup result found!"
 msgstr ""
 
-#: aleksis/core/health_checks.py:78
-#, python-brace-format
-msgid "{task.status} - {task.result}"
-msgstr ""
-
 #: aleksis/core/menus.py:9 aleksis/core/templates/two_factor/core/login.html:6
-#: aleksis/core/templates/two_factor/core/login.html:22
-#: aleksis/core/templates/two_factor/core/login.html:76
+#: aleksis/core/templates/two_factor/core/login.html:32
+#: aleksis/core/templates/two_factor/core/login.html:95
 msgid "Login"
 msgstr ""
 
-#: aleksis/core/menus.py:15 aleksis/core/templates/account/signup.html:20
+#: aleksis/core/menus.py:15 aleksis/core/templates/account/signup.html:22
 #: aleksis/core/templates/socialaccount/signup.html:23
 msgid "Sign up"
 msgstr ""
 
-#: aleksis/core/menus.py:24
+#: aleksis/core/menus.py:24 aleksis/core/templates/invitations/enter.html:7
+msgid "Accept invitation"
+msgstr ""
+
+#: aleksis/core/menus.py:33
 msgid "Dashboard"
 msgstr ""
 
-#: aleksis/core/menus.py:32 aleksis/core/models.py:605
-#: aleksis/core/preferences.py:26
+#: aleksis/core/menus.py:41 aleksis/core/models.py:734
+#: aleksis/core/preferences.py:29
 #: aleksis/core/templates/core/notifications.html:4
 #: aleksis/core/templates/core/notifications.html:5
 msgid "Notifications"
 msgstr ""
 
-#: aleksis/core/menus.py:41
+#: aleksis/core/menus.py:53
 msgid "Account"
 msgstr ""
 
-#: aleksis/core/menus.py:48
+#: aleksis/core/menus.py:60
 msgid "Stop impersonation"
 msgstr ""
 
-#: aleksis/core/menus.py:57 aleksis/core/templates/core/base.html:62
+#: aleksis/core/menus.py:69 aleksis/core/templates/core/base.html:80
 msgid "Logout"
 msgstr ""
 
-#: aleksis/core/menus.py:63
+#: aleksis/core/menus.py:75
 msgid "2FA"
 msgstr ""
 
-#: aleksis/core/menus.py:69
+#: aleksis/core/menus.py:83
 #: aleksis/core/templates/account/password_change.html:5
 #: aleksis/core/templates/account/password_change.html:6
 #: aleksis/core/templates/account/password_change.html:19
@@ -272,847 +326,875 @@ msgstr ""
 msgid "Change password"
 msgstr ""
 
-#: aleksis/core/menus.py:81
+#: aleksis/core/menus.py:95
 msgid "Me"
 msgstr ""
 
-#: aleksis/core/menus.py:90
+#: aleksis/core/menus.py:104
 #: aleksis/core/templates/dynamic_preferences/form.html:5
 msgid "Preferences"
 msgstr ""
 
-#: aleksis/core/menus.py:99
+#: aleksis/core/menus.py:113
 msgid "Third-party accounts"
 msgstr ""
 
-#: aleksis/core/menus.py:108
+#: aleksis/core/menus.py:122
 #: aleksis/core/templates/oauth2_provider/authorized-tokens.html:5
 #: aleksis/core/templates/oauth2_provider/authorized-tokens.html:6
 msgid "Authorized applications"
 msgstr ""
 
-#: aleksis/core/menus.py:119
+#: aleksis/core/menus.py:133
 msgid "Admin"
 msgstr ""
 
-#: aleksis/core/menus.py:127 aleksis/core/models.py:704
+#: aleksis/core/menus.py:141 aleksis/core/models.py:834
 #: aleksis/core/templates/core/announcement/list.html:7
 #: aleksis/core/templates/core/announcement/list.html:8
 msgid "Announcements"
 msgstr ""
 
-#: aleksis/core/menus.py:138 aleksis/core/models.py:118
+#: aleksis/core/menus.py:152 aleksis/core/models.py:131
 #: aleksis/core/templates/core/school_term/list.html:8
 #: aleksis/core/templates/core/school_term/list.html:9
 msgid "School terms"
 msgstr ""
 
-#: aleksis/core/menus.py:149
+#: aleksis/core/menus.py:163
 #: aleksis/core/templates/core/dashboard_widget/list.html:8
 #: aleksis/core/templates/core/dashboard_widget/list.html:9
 msgid "Dashboard widgets"
 msgstr ""
 
-#: aleksis/core/menus.py:160
+#: aleksis/core/menus.py:174
 #: aleksis/core/templates/core/management/data_management.html:6
 #: aleksis/core/templates/core/management/data_management.html:7
 msgid "Data management"
 msgstr ""
 
-#: aleksis/core/menus.py:171
+#: aleksis/core/menus.py:185
 #: aleksis/core/templates/core/pages/system_status.html:5
 #: aleksis/core/templates/core/pages/system_status.html:7
 msgid "System status"
 msgstr ""
 
-#: aleksis/core/menus.py:182
-msgid "Impersonation"
-msgstr ""
-
-#: aleksis/core/menus.py:193
+#: aleksis/core/menus.py:196
 msgid "Configuration"
 msgstr ""
 
-#: aleksis/core/menus.py:204 aleksis/core/templates/core/data_check/list.html:9
+#: aleksis/core/menus.py:207 aleksis/core/templates/core/data_check/list.html:9
 #: aleksis/core/templates/core/data_check/list.html:10
 msgid "Data checks"
 msgstr ""
 
-#: aleksis/core/menus.py:210
+#: aleksis/core/menus.py:213 aleksis/core/templates/core/perms/list.html:13
+#: aleksis/core/templates/core/perms/list.html:14
+msgid "Manage permissions"
+msgstr ""
+
+#: aleksis/core/menus.py:224
 msgid "Backend Admin"
 msgstr ""
 
-#: aleksis/core/menus.py:216
-#: aleksis/core/templates/oauth2_provider/application_detail.html:5
-#: aleksis/core/templates/oauth2_provider/application_list.html:5
+#: aleksis/core/menus.py:232
+#: aleksis/core/templates/oauth2_provider/application/list.html:5
+#: aleksis/core/templates/oauth2_provider/application/list.html:6
 msgid "OAuth2 Applications"
 msgstr ""
 
-#: aleksis/core/menus.py:229
+#: aleksis/core/menus.py:245
 msgid "People"
 msgstr ""
 
-#: aleksis/core/menus.py:260 aleksis/core/models.py:958
+#: aleksis/core/menus.py:276 aleksis/core/models.py:1055
 #: aleksis/core/templates/core/group_type/list.html:8
 #: aleksis/core/templates/core/group_type/list.html:9
 msgid "Group types"
 msgstr ""
 
-#: aleksis/core/menus.py:271
+#: aleksis/core/menus.py:287
 msgid "Groups and child groups"
 msgstr ""
 
-#: aleksis/core/menus.py:282 aleksis/core/models.py:446
+#: aleksis/core/menus.py:298 aleksis/core/models.py:510
 #: aleksis/core/templates/core/additional_field/list.html:8
 #: aleksis/core/templates/core/additional_field/list.html:9
 msgid "Additional fields"
 msgstr ""
 
-#: aleksis/core/menus.py:297
+#: aleksis/core/menus.py:309
+msgid "Invite person"
+msgstr ""
+
+#: aleksis/core/menus.py:322
 #: aleksis/core/templates/core/group/child_groups.html:7
 #: aleksis/core/templates/core/group/child_groups.html:9
 msgid "Assign child groups to groups"
 msgstr ""
 
-#: aleksis/core/mixins.py:498
+#: aleksis/core/mixins.py:511
 msgid "Linked school term"
 msgstr ""
 
-#: aleksis/core/models.py:58
+#: aleksis/core/models.py:71
 msgid "Boolean (Yes/No)"
 msgstr ""
 
-#: aleksis/core/models.py:59
+#: aleksis/core/models.py:72
 msgid "Text (one line)"
 msgstr ""
 
-#: aleksis/core/models.py:61
+#: aleksis/core/models.py:74
 msgid "Date and time"
 msgstr ""
 
-#: aleksis/core/models.py:62
+#: aleksis/core/models.py:75
 msgid "Decimal number"
 msgstr ""
 
-#: aleksis/core/models.py:63 aleksis/core/models.py:186
+#: aleksis/core/models.py:76 aleksis/core/models.py:199
 msgid "E-mail address"
 msgstr ""
 
-#: aleksis/core/models.py:64
+#: aleksis/core/models.py:77
 msgid "Integer"
 msgstr ""
 
-#: aleksis/core/models.py:65
+#: aleksis/core/models.py:78
 msgid "IP address"
 msgstr ""
 
-#: aleksis/core/models.py:66
+#: aleksis/core/models.py:79
 msgid "Boolean or empty (Yes/No/Neither)"
 msgstr ""
 
-#: aleksis/core/models.py:67
+#: aleksis/core/models.py:80
 msgid "Text (multi-line)"
 msgstr ""
 
-#: aleksis/core/models.py:69
+#: aleksis/core/models.py:82
 msgid "URL / Link"
 msgstr ""
 
-#: aleksis/core/models.py:81 aleksis/core/models.py:927
+#: aleksis/core/models.py:94 aleksis/core/models.py:1024
 msgid "Name"
 msgstr ""
 
-#: aleksis/core/models.py:83
+#: aleksis/core/models.py:96
 msgid "Start date"
 msgstr ""
 
-#: aleksis/core/models.py:84
+#: aleksis/core/models.py:97
 msgid "End date"
 msgstr ""
 
-#: aleksis/core/models.py:103
+#: aleksis/core/models.py:116
 msgid "The start date must be earlier than the end date."
 msgstr ""
 
-#: aleksis/core/models.py:110
+#: aleksis/core/models.py:123
 msgid "There is already a school term for this time or a part of this time."
 msgstr ""
 
-#: aleksis/core/models.py:139 aleksis/core/models.py:876
+#: aleksis/core/models.py:152 aleksis/core/models.py:973
 msgid "Person"
 msgstr ""
 
-#: aleksis/core/models.py:142
+#: aleksis/core/models.py:155
 msgid "Can view address"
 msgstr ""
 
-#: aleksis/core/models.py:143
+#: aleksis/core/models.py:156
 msgid "Can view contact details"
 msgstr ""
 
-#: aleksis/core/models.py:144
+#: aleksis/core/models.py:157
 msgid "Can view photo"
 msgstr ""
 
-#: aleksis/core/models.py:145
+#: aleksis/core/models.py:158
+msgid "Can view avatar image"
+msgstr ""
+
+#: aleksis/core/models.py:159
 msgid "Can view persons groups"
 msgstr ""
 
-#: aleksis/core/models.py:146
+#: aleksis/core/models.py:160
 msgid "Can view personal details"
 msgstr ""
 
-#: aleksis/core/models.py:156
+#: aleksis/core/models.py:170
 msgid "female"
 msgstr ""
 
-#: aleksis/core/models.py:156
+#: aleksis/core/models.py:170
 msgid "male"
 msgstr ""
 
-#: aleksis/core/models.py:164
+#: aleksis/core/models.py:178 aleksis/core/models.py:1227
 msgid "Linked user"
 msgstr ""
 
-#: aleksis/core/models.py:166
-msgid "Is person active?"
-msgstr ""
-
-#: aleksis/core/models.py:168
-msgid "First name"
-msgstr ""
-
-#: aleksis/core/models.py:169
-msgid "Last name"
-msgstr ""
-
-#: aleksis/core/models.py:171
+#: aleksis/core/models.py:184
 msgid "Additional name(s)"
 msgstr ""
 
-#: aleksis/core/models.py:175 aleksis/core/models.py:415
+#: aleksis/core/models.py:188 aleksis/core/models.py:479
 msgid "Short name"
 msgstr ""
 
-#: aleksis/core/models.py:178
+#: aleksis/core/models.py:191
 msgid "Street"
 msgstr ""
 
-#: aleksis/core/models.py:179
+#: aleksis/core/models.py:192
 msgid "Street number"
 msgstr ""
 
-#: aleksis/core/models.py:180
+#: aleksis/core/models.py:193
 msgid "Postal code"
 msgstr ""
 
-#: aleksis/core/models.py:181
+#: aleksis/core/models.py:194
 msgid "Place"
 msgstr ""
 
-#: aleksis/core/models.py:183
+#: aleksis/core/models.py:196 aleksis/core/templates/core/person/full.html:172
 msgid "Home phone"
 msgstr ""
 
-#: aleksis/core/models.py:184
+#: aleksis/core/models.py:197 aleksis/core/templates/core/person/full.html:182
 msgid "Mobile phone"
 msgstr ""
 
-#: aleksis/core/models.py:188
+#: aleksis/core/models.py:201
 msgid "Date of birth"
 msgstr ""
 
-#: aleksis/core/models.py:189
+#: aleksis/core/models.py:202
+msgid "Place of birth"
+msgstr ""
+
+#: aleksis/core/models.py:203
 msgid "Sex"
 msgstr ""
 
-#: aleksis/core/models.py:191
-msgid "Photo"
+#: aleksis/core/models.py:210 aleksis/core/models.py:518
+msgid "This is an official photo, used for official documents and for internal use cases."
 msgstr ""
 
-#: aleksis/core/models.py:195 aleksis/core/templates/core/person/full.html:137
+#: aleksis/core/models.py:215 aleksis/core/models.py:522
+msgid "Display picture / Avatar"
+msgstr ""
+
+#: aleksis/core/models.py:218 aleksis/core/models.py:525
+msgid "This is a picture or an avatar for public display."
+msgstr ""
+
+#: aleksis/core/models.py:223 aleksis/core/templates/core/person/full.html:239
 msgid "Guardians / Parents"
 msgstr ""
 
-#: aleksis/core/models.py:202
+#: aleksis/core/models.py:230
 msgid "Primary group"
 msgstr ""
 
-#: aleksis/core/models.py:205 aleksis/core/models.py:563
-#: aleksis/core/models.py:587 aleksis/core/models.py:672
-#: aleksis/core/models.py:951 aleksis/core/templates/core/person/full.html:120
+#: aleksis/core/models.py:233 aleksis/core/models.py:692
+#: aleksis/core/models.py:716 aleksis/core/models.py:801
+#: aleksis/core/models.py:1048
 msgid "Description"
 msgstr ""
 
-#: aleksis/core/models.py:370
+#: aleksis/core/models.py:434
 msgid "Title of field"
 msgstr ""
 
-#: aleksis/core/models.py:372
+#: aleksis/core/models.py:436
 msgid "Type of field"
 msgstr ""
 
-#: aleksis/core/models.py:379
+#: aleksis/core/models.py:443
 msgid "Addtitional field for groups"
 msgstr ""
 
-#: aleksis/core/models.py:380
+#: aleksis/core/models.py:444
 msgid "Addtitional fields for groups"
 msgstr ""
 
-#: aleksis/core/models.py:397
-msgid "Group"
-msgstr ""
-
-#: aleksis/core/models.py:400
+#: aleksis/core/models.py:464
 msgid "Can assign child groups to groups"
 msgstr ""
 
-#: aleksis/core/models.py:401
+#: aleksis/core/models.py:465
 msgid "Can view statistics about group."
 msgstr ""
 
-#: aleksis/core/models.py:413
+#: aleksis/core/models.py:477
 msgid "Long name"
 msgstr ""
 
-#: aleksis/core/models.py:423 aleksis/core/templates/core/group/full.html:85
+#: aleksis/core/models.py:487 aleksis/core/templates/core/group/full.html:85
 msgid "Members"
 msgstr ""
 
-#: aleksis/core/models.py:426 aleksis/core/templates/core/group/full.html:82
+#: aleksis/core/models.py:490 aleksis/core/templates/core/group/full.html:82
 msgid "Owners"
 msgstr ""
 
-#: aleksis/core/models.py:433 aleksis/core/templates/core/group/full.html:55
+#: aleksis/core/models.py:497 aleksis/core/templates/core/group/full.html:55
 msgid "Parent groups"
 msgstr ""
 
-#: aleksis/core/models.py:441
+#: aleksis/core/models.py:505
 msgid "Type of group"
 msgstr ""
 
-#: aleksis/core/models.py:559
-msgid "User"
-msgstr ""
-
-#: aleksis/core/models.py:562 aleksis/core/models.py:586
-#: aleksis/core/models.py:671
+#: aleksis/core/models.py:691 aleksis/core/models.py:715
+#: aleksis/core/models.py:800
 #: aleksis/core/templates/core/announcement/list.html:18
 msgid "Title"
 msgstr ""
 
-#: aleksis/core/models.py:565
+#: aleksis/core/models.py:694
 msgid "Application"
 msgstr ""
 
-#: aleksis/core/models.py:571
+#: aleksis/core/models.py:700
 msgid "Activity"
 msgstr ""
 
-#: aleksis/core/models.py:572
+#: aleksis/core/models.py:701
 msgid "Activities"
 msgstr ""
 
-#: aleksis/core/models.py:578
+#: aleksis/core/models.py:707
 msgid "Sender"
 msgstr ""
 
-#: aleksis/core/models.py:583
+#: aleksis/core/models.py:712
 msgid "Recipient"
 msgstr ""
 
-#: aleksis/core/models.py:588 aleksis/core/models.py:928
+#: aleksis/core/models.py:717 aleksis/core/models.py:1025
 msgid "Link"
 msgstr ""
 
-#: aleksis/core/models.py:590
+#: aleksis/core/models.py:719
 msgid "Read"
 msgstr ""
 
-#: aleksis/core/models.py:591
+#: aleksis/core/models.py:720
 msgid "Sent"
 msgstr ""
 
-#: aleksis/core/models.py:604
+#: aleksis/core/models.py:733
 msgid "Notification"
 msgstr ""
 
-#: aleksis/core/models.py:673
+#: aleksis/core/models.py:802
 msgid "Link to detailed view"
 msgstr ""
 
-#: aleksis/core/models.py:676
+#: aleksis/core/models.py:805
 msgid "Date and time from when to show"
 msgstr ""
 
-#: aleksis/core/models.py:679
+#: aleksis/core/models.py:808
 msgid "Date and time until when to show"
 msgstr ""
 
-#: aleksis/core/models.py:703
+#: aleksis/core/models.py:833
 msgid "Announcement"
 msgstr ""
 
-#: aleksis/core/models.py:741
+#: aleksis/core/models.py:871
 msgid "Announcement recipient"
 msgstr ""
 
-#: aleksis/core/models.py:742
+#: aleksis/core/models.py:872
 msgid "Announcement recipients"
 msgstr ""
 
-#: aleksis/core/models.py:797
+#: aleksis/core/models.py:894
 msgid "Widget Title"
 msgstr ""
 
-#: aleksis/core/models.py:798
+#: aleksis/core/models.py:895
 msgid "Activate Widget"
 msgstr ""
 
-#: aleksis/core/models.py:799
+#: aleksis/core/models.py:896
 msgid "Widget is broken"
 msgstr ""
 
-#: aleksis/core/models.py:802
+#: aleksis/core/models.py:899
 msgid "Size on mobile devices"
 msgstr ""
 
-#: aleksis/core/models.py:803
+#: aleksis/core/models.py:900
 msgid "<= 600 px, 12 columns"
 msgstr ""
 
-#: aleksis/core/models.py:808
+#: aleksis/core/models.py:905
 msgid "Size on tablet devices"
 msgstr ""
 
-#: aleksis/core/models.py:809
+#: aleksis/core/models.py:906
 msgid "> 600 px, 12 columns"
 msgstr ""
 
-#: aleksis/core/models.py:814
+#: aleksis/core/models.py:911
 msgid "Size on desktop devices"
 msgstr ""
 
-#: aleksis/core/models.py:815
+#: aleksis/core/models.py:912
 msgid "> 992 px, 12 columns"
 msgstr ""
 
-#: aleksis/core/models.py:820
+#: aleksis/core/models.py:917
 msgid "Size on large desktop devices"
 msgstr ""
 
-#: aleksis/core/models.py:821
+#: aleksis/core/models.py:918
 msgid "> 1200 px>, 12 columns"
 msgstr ""
 
-#: aleksis/core/models.py:852
+#: aleksis/core/models.py:949
 msgid "Can edit default dashboard"
 msgstr ""
 
-#: aleksis/core/models.py:853
+#: aleksis/core/models.py:950
 msgid "Dashboard Widget"
 msgstr ""
 
-#: aleksis/core/models.py:854
+#: aleksis/core/models.py:951
 msgid "Dashboard Widgets"
 msgstr ""
 
-#: aleksis/core/models.py:860
+#: aleksis/core/models.py:957
 msgid "URL"
 msgstr ""
 
-#: aleksis/core/models.py:861
+#: aleksis/core/models.py:958
 msgid "Icon URL"
 msgstr ""
 
-#: aleksis/core/models.py:867
+#: aleksis/core/models.py:964
 msgid "External link widget"
 msgstr ""
 
-#: aleksis/core/models.py:868
+#: aleksis/core/models.py:965
 msgid "External link widgets"
 msgstr ""
 
-#: aleksis/core/models.py:873
+#: aleksis/core/models.py:970
 msgid "Dashboard widget"
 msgstr ""
 
-#: aleksis/core/models.py:878
+#: aleksis/core/models.py:975
 msgid "Order"
 msgstr ""
 
-#: aleksis/core/models.py:879
+#: aleksis/core/models.py:976
 msgid "Part of the default dashboard"
 msgstr ""
 
-#: aleksis/core/models.py:894
+#: aleksis/core/models.py:991
 msgid "Dashboard widget order"
 msgstr ""
 
-#: aleksis/core/models.py:895
+#: aleksis/core/models.py:992
 msgid "Dashboard widget orders"
 msgstr ""
 
-#: aleksis/core/models.py:901
+#: aleksis/core/models.py:998
 msgid "Menu ID"
 msgstr ""
 
-#: aleksis/core/models.py:914
+#: aleksis/core/models.py:1011
 msgid "Custom menu"
 msgstr ""
 
-#: aleksis/core/models.py:915
+#: aleksis/core/models.py:1012
 msgid "Custom menus"
 msgstr ""
 
-#: aleksis/core/models.py:925
+#: aleksis/core/models.py:1022
 msgid "Menu"
 msgstr ""
 
-#: aleksis/core/models.py:929
+#: aleksis/core/models.py:1026 aleksis/core/models.py:1274
+#: aleksis/core/templates/oauth2_provider/application/detail.html:26
 msgid "Icon"
 msgstr ""
 
-#: aleksis/core/models.py:935
+#: aleksis/core/models.py:1032
 msgid "Custom menu item"
 msgstr ""
 
-#: aleksis/core/models.py:936
+#: aleksis/core/models.py:1033
 msgid "Custom menu items"
 msgstr ""
 
-#: aleksis/core/models.py:950
+#: aleksis/core/models.py:1047
 msgid "Title of type"
 msgstr ""
 
-#: aleksis/core/models.py:957 aleksis/core/templates/core/group/full.html:47
+#: aleksis/core/models.py:1054 aleksis/core/templates/core/group/full.html:47
 msgid "Group type"
 msgstr ""
 
-#: aleksis/core/models.py:971
+#: aleksis/core/models.py:1068
 msgid "Can view system status"
 msgstr ""
 
-#: aleksis/core/models.py:972
+#: aleksis/core/models.py:1069
 msgid "Can manage data"
 msgstr ""
 
-#: aleksis/core/models.py:973
+#: aleksis/core/models.py:1070
 msgid "Can impersonate"
 msgstr ""
 
-#: aleksis/core/models.py:974
+#: aleksis/core/models.py:1071
 msgid "Can use search"
 msgstr ""
 
-#: aleksis/core/models.py:975
+#: aleksis/core/models.py:1072
 msgid "Can change site preferences"
 msgstr ""
 
-#: aleksis/core/models.py:976
+#: aleksis/core/models.py:1073
 msgid "Can change person preferences"
 msgstr ""
 
-#: aleksis/core/models.py:977
+#: aleksis/core/models.py:1074
 msgid "Can change group preferences"
 msgstr ""
 
-#: aleksis/core/models.py:978
-msgid "Can add oauth applications"
-msgstr ""
-
-#: aleksis/core/models.py:979
-msgid "Can list oauth applications"
-msgstr ""
-
-#: aleksis/core/models.py:980
-msgid "Can view oauth applications"
-msgstr ""
-
-#: aleksis/core/models.py:981
-msgid "Can update oauth applications"
-msgstr ""
-
-#: aleksis/core/models.py:982
-msgid "Can delete oauth applications"
+#: aleksis/core/models.py:1075
+msgid "Can test PDF generation"
 msgstr ""
 
-#: aleksis/core/models.py:983
-msgid "Can test PDF generation"
+#: aleksis/core/models.py:1076
+msgid "Can invite persons"
 msgstr ""
 
-#: aleksis/core/models.py:1019
+#: aleksis/core/models.py:1112
 msgid "Related data check task"
 msgstr ""
 
-#: aleksis/core/models.py:1027
+#: aleksis/core/models.py:1120
 msgid "Issue solved"
 msgstr ""
 
-#: aleksis/core/models.py:1028
+#: aleksis/core/models.py:1121
 msgid "Notification sent"
 msgstr ""
 
-#: aleksis/core/models.py:1041
+#: aleksis/core/models.py:1134
 msgid "Data check result"
 msgstr ""
 
-#: aleksis/core/models.py:1042
+#: aleksis/core/models.py:1135
 msgid "Data check results"
 msgstr ""
 
-#: aleksis/core/models.py:1044
+#: aleksis/core/models.py:1137
 msgid "Can run data checks"
 msgstr ""
 
-#: aleksis/core/models.py:1045
+#: aleksis/core/models.py:1138
 msgid "Can solve data check problems"
 msgstr ""
 
-#: aleksis/core/models.py:1060
+#: aleksis/core/models.py:1145
+msgid "E-Mail address"
+msgstr ""
+
+#: aleksis/core/models.py:1177
 msgid "Owner"
 msgstr ""
 
-#: aleksis/core/models.py:1064
+#: aleksis/core/models.py:1181
 msgid "File expires at"
 msgstr ""
 
-#: aleksis/core/models.py:1066
+#: aleksis/core/models.py:1183
 msgid "Generated HTML file"
 msgstr ""
 
-#: aleksis/core/models.py:1068
+#: aleksis/core/models.py:1185
 msgid "Generated PDF file"
 msgstr ""
 
-#: aleksis/core/models.py:1075
+#: aleksis/core/models.py:1192
 msgid "PDF file"
 msgstr ""
 
-#: aleksis/core/models.py:1076
+#: aleksis/core/models.py:1193
 msgid "PDF files"
 msgstr ""
 
-#: aleksis/core/models.py:1081
+#: aleksis/core/models.py:1198
 msgid "Task result"
 msgstr ""
 
-#: aleksis/core/models.py:1084
+#: aleksis/core/models.py:1201
 msgid "Task user"
 msgstr ""
 
-#: aleksis/core/models.py:1096
+#: aleksis/core/models.py:1213
 msgid "Task user assignment"
 msgstr ""
 
-#: aleksis/core/models.py:1097
+#: aleksis/core/models.py:1214
 msgid "Task user assignments"
 msgstr ""
 
-#: aleksis/core/preferences.py:22
+#: aleksis/core/models.py:1230
+msgid "Additional attributes"
+msgstr ""
+
+#: aleksis/core/models.py:1268
+msgid "Allowed scopes that clients can request"
+msgstr ""
+
+#: aleksis/core/models.py:1278
+msgid "This image will be shown as icon in the authorization flow. It should be squared."
+msgstr ""
+
+#: aleksis/core/preferences.py:25
 msgid "General"
 msgstr ""
 
-#: aleksis/core/preferences.py:23
+#: aleksis/core/preferences.py:26
 msgid "School"
 msgstr ""
 
-#: aleksis/core/preferences.py:24
+#: aleksis/core/preferences.py:27
 msgid "Theme"
 msgstr ""
 
-#: aleksis/core/preferences.py:25
+#: aleksis/core/preferences.py:28
 msgid "Mail"
 msgstr ""
 
-#: aleksis/core/preferences.py:27
+#: aleksis/core/preferences.py:30
 msgid "Footer"
 msgstr ""
 
-#: aleksis/core/preferences.py:28
+#: aleksis/core/preferences.py:31
 msgid "Accounts"
 msgstr ""
 
-#: aleksis/core/preferences.py:29
+#: aleksis/core/preferences.py:32
 msgid "Authentication"
 msgstr ""
 
-#: aleksis/core/preferences.py:30
+#: aleksis/core/preferences.py:33
 msgid "Internationalisation"
 msgstr ""
 
-#: aleksis/core/preferences.py:41
+#: aleksis/core/preferences.py:43
 msgid "Site title"
 msgstr ""
 
-#: aleksis/core/preferences.py:52
+#: aleksis/core/preferences.py:55
 msgid "Site description"
 msgstr ""
 
-#: aleksis/core/preferences.py:63
+#: aleksis/core/preferences.py:65
 msgid "Primary colour"
 msgstr ""
 
-#: aleksis/core/preferences.py:75
+#: aleksis/core/preferences.py:77
 msgid "Secondary colour"
 msgstr ""
 
-#: aleksis/core/preferences.py:86
+#: aleksis/core/preferences.py:89
 msgid "Logo"
 msgstr ""
 
-#: aleksis/core/preferences.py:96
+#: aleksis/core/preferences.py:100
 msgid "Favicon"
 msgstr ""
 
-#: aleksis/core/preferences.py:106
+#: aleksis/core/preferences.py:111
 msgid "PWA-Icon"
 msgstr ""
 
-#: aleksis/core/preferences.py:117
+#: aleksis/core/preferences.py:121
+msgid "PWA-Icon is maskable"
+msgstr ""
+
+#: aleksis/core/preferences.py:133
 msgid "Mail out name"
 msgstr ""
 
-#: aleksis/core/preferences.py:128
+#: aleksis/core/preferences.py:144
 msgid "Mail out address"
 msgstr ""
 
-#: aleksis/core/preferences.py:140
+#: aleksis/core/preferences.py:157
 msgid "Link to privacy policy"
 msgstr ""
 
-#: aleksis/core/preferences.py:152
+#: aleksis/core/preferences.py:169
 msgid "Link to imprint"
 msgstr ""
 
-#: aleksis/core/preferences.py:164
+#: aleksis/core/preferences.py:180
 msgid "Name format for addressing"
 msgstr ""
 
-#: aleksis/core/preferences.py:180
+#: aleksis/core/preferences.py:197
 msgid "Channels to use for notifications"
 msgstr ""
 
-#: aleksis/core/preferences.py:192
+#: aleksis/core/preferences.py:209
 msgid "Regular expression to match primary group, e.g. '^Class .*'"
 msgstr ""
 
-#: aleksis/core/preferences.py:203
+#: aleksis/core/preferences.py:220
 msgid "Field on person to match primary group against"
 msgstr ""
 
-#: aleksis/core/preferences.py:215
+#: aleksis/core/preferences.py:232
 msgid "Automatically create new persons for new users"
 msgstr ""
 
-#: aleksis/core/preferences.py:224
+#: aleksis/core/preferences.py:241
 msgid "Automatically link existing persons to new users by their e-mail address"
 msgstr ""
 
-#: aleksis/core/preferences.py:235
+#: aleksis/core/preferences.py:252
 msgid "Display name of the school"
 msgstr ""
 
-#: aleksis/core/preferences.py:246
+#: aleksis/core/preferences.py:263
 msgid "Official name of the school, e.g. as given by supervisory authority"
 msgstr ""
 
-#: aleksis/core/preferences.py:254
+#: aleksis/core/preferences.py:271
 msgid "Allow users to change their passwords"
 msgstr ""
 
-#: aleksis/core/preferences.py:262
+#: aleksis/core/preferences.py:279
 msgid "Enable signup"
 msgstr ""
 
-#: aleksis/core/preferences.py:273
+#: aleksis/core/preferences.py:287
+msgid "Enable invitations"
+msgstr ""
+
+#: aleksis/core/preferences.py:295
+msgid "Length of invite code. (Default 3: abcde-acbde-abcde)"
+msgstr ""
+
+#: aleksis/core/preferences.py:303
+msgid "Size of packets. (Default 5: abcde)"
+msgstr ""
+
+#: aleksis/core/preferences.py:314
+msgid "Allowed Grant Flows for OAuth applications"
+msgstr ""
+
+#: aleksis/core/preferences.py:328
 msgid "Available languages"
 msgstr ""
 
-#: aleksis/core/preferences.py:285
+#: aleksis/core/preferences.py:341
 msgid "Send emails if data checks detect problems"
 msgstr ""
 
-#: aleksis/core/preferences.py:296
+#: aleksis/core/preferences.py:352
 msgid "Email recipients for data checks problem emails"
 msgstr ""
 
-#: aleksis/core/preferences.py:307
+#: aleksis/core/preferences.py:363
 msgid "Email recipient groups for data checks problem emails"
 msgstr ""
 
-#: aleksis/core/preferences.py:316
+#: aleksis/core/preferences.py:372
 msgid "Show dashboard to users without login"
 msgstr ""
 
-#: aleksis/core/preferences.py:325
+#: aleksis/core/preferences.py:381
 msgid "Allow users to edit their dashboard"
 msgstr ""
 
-#: aleksis/core/preferences.py:336
+#: aleksis/core/preferences.py:392
 msgid "Fields on person model which are editable by themselves."
 msgstr ""
 
-#: aleksis/core/preferences.py:350
+#: aleksis/core/preferences.py:407
 msgid "Editable fields on person model which should trigger a notification on change"
 msgstr ""
 
-#: aleksis/core/preferences.py:363
+#: aleksis/core/preferences.py:421
 msgid "Contact for notification if a person changes their data"
 msgstr ""
 
-#: aleksis/core/preferences.py:373
+#: aleksis/core/preferences.py:432
 msgid "PDF file expiration duration"
 msgstr ""
 
-#: aleksis/core/preferences.py:374
+#: aleksis/core/preferences.py:433
 msgid "in minutes"
 msgstr ""
 
-#: aleksis/core/preferences.py:384
+#: aleksis/core/preferences.py:443
 msgid "Automatically update the dashboard and its widgets"
 msgstr ""
 
-#: aleksis/core/preferences.py:394
+#: aleksis/core/preferences.py:453
 msgid "Automatically update the dashboard and its widgets sitewide"
 msgstr ""
 
-#: aleksis/core/settings.py:452
+#: aleksis/core/preferences.py:463
+msgid "Country for phone number parsing"
+msgstr ""
+
+#: aleksis/core/settings.py:529
 msgid "English"
 msgstr ""
 
-#: aleksis/core/settings.py:453
+#: aleksis/core/settings.py:530
 msgid "German"
 msgstr ""
 
-#: aleksis/core/tables.py:19
+#: aleksis/core/tables.py:24
 #: aleksis/core/templates/core/announcement/list.html:36
 #: aleksis/core/templates/core/group/full.html:24
-#: aleksis/core/templates/core/person/full.html:23
-#: aleksis/core/templates/oauth2_provider/application_detail.html:17
+#: aleksis/core/templates/core/person/full.html:26
+#: aleksis/core/templates/core/person/full.html:98
+#: aleksis/core/templates/oauth2_provider/application/detail.html:17
 msgid "Edit"
 msgstr ""
 
-#: aleksis/core/tables.py:21 aleksis/core/tables.py:89
+#: aleksis/core/tables.py:26 aleksis/core/tables.py:94
+#: aleksis/core/tables.py:138
 #: aleksis/core/templates/core/announcement/list.html:22
 msgid "Actions"
 msgstr ""
 
-#: aleksis/core/tables.py:56 aleksis/core/tables.py:57
-#: aleksis/core/tables.py:71 aleksis/core/tables.py:87
+#: aleksis/core/tables.py:61 aleksis/core/tables.py:62
+#: aleksis/core/tables.py:76 aleksis/core/tables.py:92
+#: aleksis/core/tables.py:136
 #: aleksis/core/templates/core/announcement/list.html:42
 #: aleksis/core/templates/core/group/full.html:31
 #: aleksis/core/templates/core/pages/delete.html:22
-#: aleksis/core/templates/core/person/full.html:30
-#: aleksis/core/templates/oauth2_provider/application_confirm_delete.html:20
-#: aleksis/core/templates/oauth2_provider/application_detail.html:21
+#: aleksis/core/templates/core/person/full.html:33
+#: aleksis/core/templates/core/person/full.html:105
+#: aleksis/core/templates/oauth2_provider/application/detail.html:21
 msgid "Delete"
 msgstr ""
 
-#: aleksis/core/templates/403.html:14 aleksis/core/templates/404.html:10
-#: aleksis/core/templates/500.html:10
-#: aleksis/core/templates/oauth2_provider/authorize.html:47
+#: aleksis/core/templates/403.html:14 aleksis/core/templates/500.html:10
+#: aleksis/core/templates/oauth2_provider/authorize.html:54
 #: aleksis/core/templates/oauth2_provider/authorized-oob.html:24
 msgid "Error"
 msgstr ""
@@ -1123,7 +1205,7 @@ msgid ""
 "          object."
 msgstr ""
 
-#: aleksis/core/templates/403.html:19 aleksis/core/templates/404.html:17
+#: aleksis/core/templates/403.html:19 aleksis/core/templates/404.html:16
 msgid ""
 "\n"
 "            If you think this is an error in AlekSIS, please contact your site\n"
@@ -1131,13 +1213,7 @@ msgid ""
 "          "
 msgstr ""
 
-#: aleksis/core/templates/404.html:10
-msgid ""
-"The requested page or object was not\n"
-"          found."
-msgstr ""
-
-#: aleksis/core/templates/404.html:13
+#: aleksis/core/templates/404.html:12
 msgid ""
 "\n"
 "            If you were redirected by a link on an external page,\n"
@@ -1177,11 +1253,11 @@ msgstr ""
 msgid "Account inactive"
 msgstr ""
 
-#: aleksis/core/templates/account/account_inactive.html:13
+#: aleksis/core/templates/account/account_inactive.html:14
 msgid "Account inactive."
 msgstr ""
 
-#: aleksis/core/templates/account/account_inactive.html:15
+#: aleksis/core/templates/account/account_inactive.html:17
 msgid ""
 "\n"
 "            This account is currently inactive. If you think this is an\n"
@@ -1194,14 +1270,13 @@ msgid "Hello!"
 msgstr ""
 
 #: aleksis/core/templates/account/email/base_message.txt:9
-#: aleksis/core/templates/templated_email/notification.email:22
-#: aleksis/core/templates/templated_email/notification.email:46
 msgid "Your AlekSIS team"
 msgstr ""
 
 #: aleksis/core/templates/account/email_confirm.html:5
 #: aleksis/core/templates/account/email_confirm.html:6
 #: aleksis/core/templates/account/email_confirm.html:17
+#: aleksis/core/templates/socialaccount/login.html:17
 msgid "Confirm"
 msgstr ""
 
@@ -1246,7 +1321,7 @@ msgstr ""
 #: aleksis/core/templates/account/password_reset_done.html:5
 #: aleksis/core/templates/account/verification_email_required.html:5
 #: aleksis/core/templates/account/verification_email_required.html:6
-#: aleksis/core/templates/two_factor/core/login.html:81
+#: aleksis/core/templates/two_factor/core/login.html:100
 msgid "Reset password"
 msgstr ""
 
@@ -1333,11 +1408,11 @@ msgstr ""
 msgid "Signup closed"
 msgstr ""
 
-#: aleksis/core/templates/account/signup_closed.html:13
+#: aleksis/core/templates/account/signup_closed.html:14
 msgid "Signup closed."
 msgstr ""
 
-#: aleksis/core/templates/account/signup_closed.html:15
+#: aleksis/core/templates/account/signup_closed.html:17
 msgid ""
 "\n"
 "            This sign up is currently closed. If you think this is an\n"
@@ -1375,11 +1450,6 @@ msgid ""
 "          "
 msgstr ""
 
-#: aleksis/core/templates/account/verification_sent.html:30
-#, python-format
-msgid "<strong>Note:</strong> you can still <a href=\"%(email_url)s\">change your e-mail address</a>"
-msgstr ""
-
 #: aleksis/core/templates/core/additional_field/edit.html:6
 #: aleksis/core/templates/core/additional_field/edit.html:7
 msgid "Edit additional field"
@@ -1423,24 +1493,24 @@ msgstr ""
 msgid "There are no announcements."
 msgstr ""
 
-#: aleksis/core/templates/core/base.html:60
+#: aleksis/core/templates/core/base.html:78
 msgid "Logged in as"
 msgstr ""
 
-#: aleksis/core/templates/core/base.html:154
-msgid "About AlekSIS — The Free School Information System"
+#: aleksis/core/templates/core/base.html:179
+msgid "About AlekSIS® — The Free School Information System"
 msgstr ""
 
-#: aleksis/core/templates/core/base.html:162
-msgid "Impress"
+#: aleksis/core/templates/core/base.html:187
+msgid "Imprint"
 msgstr ""
 
-#: aleksis/core/templates/core/base.html:170
+#: aleksis/core/templates/core/base.html:195
 msgid "Privacy Policy"
 msgstr ""
 
-#: aleksis/core/templates/core/base_print.html:72
-msgid "Powered by AlekSIS"
+#: aleksis/core/templates/core/base_print.html:74
+msgid "Powered by AlekSIS®"
 msgstr ""
 
 #: aleksis/core/templates/core/dashboard_widget/create.html:8
@@ -1525,15 +1595,15 @@ msgstr ""
 msgid "Options to solve the problem"
 msgstr ""
 
-#: aleksis/core/templates/core/data_check/list.html:62
+#: aleksis/core/templates/core/data_check/list.html:63
 msgid "Show object"
 msgstr ""
 
-#: aleksis/core/templates/core/data_check/list.html:84
+#: aleksis/core/templates/core/data_check/list.html:86
 msgid "Registered checks"
 msgstr ""
 
-#: aleksis/core/templates/core/data_check/list.html:88
+#: aleksis/core/templates/core/data_check/list.html:90
 msgid ""
 "\n"
 "            The system will check for the following problems:\n"
@@ -1626,7 +1696,7 @@ msgstr ""
 
 #: aleksis/core/templates/core/group/child_groups.html:93
 #: aleksis/core/templates/core/group/child_groups.html:128
-#: aleksis/core/templates/oauth2_provider/application_detail.html:9
+#: aleksis/core/templates/oauth2_provider/application/detail.html:9
 #: aleksis/core/templates/two_factor/_wizard_actions.html:15
 #: aleksis/core/templates/two_factor/_wizard_actions.html:20
 msgid "Back"
@@ -1655,7 +1725,8 @@ msgid "Edit group"
 msgstr ""
 
 #: aleksis/core/templates/core/group/full.html:38
-#: aleksis/core/templates/core/person/full.html:37
+#: aleksis/core/templates/core/person/full.html:40
+#: aleksis/core/templates/core/person/full.html:112
 msgid "Change preferences"
 msgstr ""
 
@@ -1744,36 +1815,46 @@ msgid "No notifications available yet."
 msgstr ""
 
 #: aleksis/core/templates/core/pages/about.html:6
-#: aleksis/core/templates/core/pages/about.html:15
-msgid "About AlekSIS"
+msgid "About AlekSIS®"
 msgstr ""
 
 #: aleksis/core/templates/core/pages/about.html:7
-msgid "AlekSIS – The Free School Information System"
+msgid "AlekSIS® – The Free School Information System"
+msgstr ""
+
+#: aleksis/core/templates/core/pages/about.html:15
+msgid "About AlekSIS"
 msgstr ""
 
 #: aleksis/core/templates/core/pages/about.html:17
 msgid ""
 "\n"
-"              This platform is powered by AlekSIS, a web-based school information system (SIS) which can be used\n"
+"              This platform is powered by AlekSIS®, a web-based school information system (SIS) which can be used\n"
 "              to manage and/or publish organisational artifacts of educational institutions. AlekSIS is free software and\n"
 "              can be used by anyone.\n"
 "            "
 msgstr ""
 
-#: aleksis/core/templates/core/pages/about.html:25
+#: aleksis/core/templates/core/pages/about.html:24
+msgid ""
+"\n"
+"              AlekSIS® is a registered trademark of the AlekSIS open source project, represented by Teckids e.V.\n"
+"            "
+msgstr ""
+
+#: aleksis/core/templates/core/pages/about.html:30
 msgid "Website of AlekSIS"
 msgstr ""
 
-#: aleksis/core/templates/core/pages/about.html:26
+#: aleksis/core/templates/core/pages/about.html:31
 msgid "Source code"
 msgstr ""
 
-#: aleksis/core/templates/core/pages/about.html:35
+#: aleksis/core/templates/core/pages/about.html:40
 msgid "Licence information"
 msgstr ""
 
-#: aleksis/core/templates/core/pages/about.html:37
+#: aleksis/core/templates/core/pages/about.html:42
 msgid ""
 "\n"
 "              The core and the official apps of AlekSIS are licenced under the EUPL, version 1.2 or later. For licence\n"
@@ -1782,23 +1863,23 @@ msgid ""
 "            "
 msgstr ""
 
-#: aleksis/core/templates/core/pages/about.html:45
+#: aleksis/core/templates/core/pages/about.html:50
 msgid "Free/Open Source Licence"
 msgstr ""
 
-#: aleksis/core/templates/core/pages/about.html:46
+#: aleksis/core/templates/core/pages/about.html:51
 msgid "Other Licence"
 msgstr ""
 
-#: aleksis/core/templates/core/pages/about.html:50
+#: aleksis/core/templates/core/pages/about.html:55
 msgid "Full licence text"
 msgstr ""
 
-#: aleksis/core/templates/core/pages/about.html:51
+#: aleksis/core/templates/core/pages/about.html:56
 msgid "More information about the EUPL"
 msgstr ""
 
-#: aleksis/core/templates/core/pages/about.html:90
+#: aleksis/core/templates/core/pages/about.html:95
 #, python-format
 msgid ""
 "\n"
@@ -1902,6 +1983,8 @@ msgid "Celery task results"
 msgstr ""
 
 #: aleksis/core/templates/core/pages/system_status.html:112
+#: aleksis/core/templates/templated_email/celery_failure.email:9
+#: aleksis/core/templates/templated_email/celery_failure.email:28
 msgid "Task"
 msgstr ""
 
@@ -1983,6 +2066,51 @@ msgid ""
 "          "
 msgstr ""
 
+#: aleksis/core/templates/core/perms/assign.html:12
+#: aleksis/core/templates/core/perms/assign.html:13
+msgid "Assign permission"
+msgstr ""
+
+#: aleksis/core/templates/core/perms/assign.html:17
+msgid "Selected permission"
+msgstr ""
+
+#: aleksis/core/templates/core/perms/assign.html:26
+msgid "Assign"
+msgstr ""
+
+#: aleksis/core/templates/core/perms/list.html:21
+msgid "Assign a new permission"
+msgstr ""
+
+#: aleksis/core/templates/core/perms/list.html:25
+msgid "Select"
+msgstr ""
+
+#: aleksis/core/templates/core/perms/list.html:34
+msgid "Global (user)"
+msgstr ""
+
+#: aleksis/core/templates/core/perms/list.html:38
+msgid "Global (group)"
+msgstr ""
+
+#: aleksis/core/templates/core/perms/list.html:42
+msgid "Object (user)"
+msgstr ""
+
+#: aleksis/core/templates/core/perms/list.html:46
+msgid "Object (group)"
+msgstr ""
+
+#: aleksis/core/templates/core/perms/list.html:52
+msgid "Filter permissions"
+msgstr ""
+
+#: aleksis/core/templates/core/perms/list.html:58
+msgid "Update"
+msgstr ""
+
 #: aleksis/core/templates/core/person/create.html:12
 #: aleksis/core/templates/core/person/create.html:13
 #: aleksis/core/templates/core/person/list.html:17
@@ -1994,17 +2122,25 @@ msgstr ""
 msgid "Edit person"
 msgstr ""
 
-#: aleksis/core/templates/core/person/full.html:44
-#: aleksis/core/templates/impersonate/list_users.html:7
-#: aleksis/core/templates/impersonate/list_users.html:8
+#: aleksis/core/templates/core/person/full.html:47
+#: aleksis/core/templates/core/person/full.html:119
 msgid "Impersonate"
 msgstr ""
 
-#: aleksis/core/templates/core/person/full.html:50
+#: aleksis/core/templates/core/person/full.html:54
+#: aleksis/core/templates/core/person/full.html:126
+msgid "Invite user"
+msgstr ""
+
+#: aleksis/core/templates/core/person/full.html:133
 msgid "Contact details"
 msgstr ""
 
-#: aleksis/core/templates/core/person/full.html:130
+#: aleksis/core/templates/core/person/full.html:224
+msgid "This person didn't upload a personal photo."
+msgstr ""
+
+#: aleksis/core/templates/core/person/full.html:232
 msgid "Children"
 msgstr ""
 
@@ -2044,77 +2180,138 @@ msgstr ""
 msgid "Save preferences"
 msgstr ""
 
-#: aleksis/core/templates/oauth2_provider/application_confirm_delete.html:5
-#: aleksis/core/templates/oauth2_provider/application_confirm_delete.html:6
-msgid "Delete application"
+#: aleksis/core/templates/invitations/disabled.html:5
+msgid "The invite feature is disabled"
+msgstr ""
+
+#: aleksis/core/templates/invitations/disabled.html:13
+msgid "The invite feature is disabled."
+msgstr ""
+
+#: aleksis/core/templates/invitations/disabled.html:15
+msgid "To enable it, switch on the corresponding checkbox in the authentication section of the "
+msgstr ""
+
+#: aleksis/core/templates/invitations/disabled.html:16
+msgid "site preferences page"
+msgstr ""
+
+#: aleksis/core/templates/invitations/enter.html:21
+msgid "Accept your invitation"
 msgstr ""
 
-#: aleksis/core/templates/oauth2_provider/application_confirm_delete.html:12
+#: aleksis/core/templates/invitations/enter.html:25
+msgid ""
+"\n"
+"                Please enter your invitation code to register\n"
+"                your new user account:\n"
+"              "
+msgstr ""
+
+#: aleksis/core/templates/invitations/enter.html:37
+msgid "Accept invite"
+msgstr ""
+
+#: aleksis/core/templates/invitations/forms/_invite.html:9
+#: aleksis/core/templates/invitations/forms/_invite.html:10
+#: aleksis/core/templates/invitations/forms/_invite.html:21
+msgid "Invite"
+msgstr ""
+
+#: aleksis/core/templates/invitations/forms/_invite.html:17
+msgid "Invite by email address"
+msgstr ""
+
+#: aleksis/core/templates/invitations/forms/_invite.html:26
+msgid "Generate invitation code"
+msgstr ""
+
+#: aleksis/core/templates/invitations/forms/_invite.html:29
+msgid "Generate code"
+msgstr ""
+
+#: aleksis/core/templates/invitations/forms/_invite.html:33
+msgid "Invitations"
+msgstr ""
+
+#: aleksis/core/templates/invitations/messages/invite_accepted.txt:3
 #, python-format
-msgid "Are you sure to delete the application %(application_name)s?"
+msgid "The invitation for %(email)s has been accepted."
+msgstr ""
+
+#: aleksis/core/templates/oauth2_provider/application/create.html:5
+#: aleksis/core/templates/oauth2_provider/application/create.html:6
+msgid "Register OAuth2 Application"
 msgstr ""
 
-#: aleksis/core/templates/oauth2_provider/application_confirm_delete.html:24
-#: aleksis/core/templates/oauth2_provider/application_form.html:18
+#: aleksis/core/templates/oauth2_provider/application/create.html:14
+#: aleksis/core/templates/oauth2_provider/application/edit.html:14
 #: aleksis/core/templates/oauth2_provider/authorized-token-delete.html:24
 #: aleksis/core/templates/two_factor/_wizard_actions.html:6
 msgid "Cancel"
 msgstr ""
 
-#: aleksis/core/templates/oauth2_provider/application_detail.html:27
+#: aleksis/core/templates/oauth2_provider/application/detail.html:5
+msgid "OAuth2 Application"
+msgstr ""
+
+#: aleksis/core/templates/oauth2_provider/application/detail.html:39
 msgid "Client id"
 msgstr ""
 
-#: aleksis/core/templates/oauth2_provider/application_detail.html:35
+#: aleksis/core/templates/oauth2_provider/application/detail.html:47
 msgid "Client secret"
 msgstr ""
 
-#: aleksis/core/templates/oauth2_provider/application_detail.html:43
+#: aleksis/core/templates/oauth2_provider/application/detail.html:55
 msgid "Client type"
 msgstr ""
 
-#: aleksis/core/templates/oauth2_provider/application_detail.html:51
-msgid "Authorization Grant Type"
+#: aleksis/core/templates/oauth2_provider/application/detail.html:63
+msgid "Allowed scopes"
 msgstr ""
 
-#: aleksis/core/templates/oauth2_provider/application_detail.html:59
+#: aleksis/core/templates/oauth2_provider/application/detail.html:71
 msgid "Redirect URIs"
 msgstr ""
 
-#: aleksis/core/templates/oauth2_provider/application_form.html:5
-msgid "Create OAuth2 Application"
+#: aleksis/core/templates/oauth2_provider/application/detail.html:79
+msgid "Skip Authorisation"
 msgstr ""
 
-#: aleksis/core/templates/oauth2_provider/application_form.html:10
-msgid "Edit application"
+#: aleksis/core/templates/oauth2_provider/application/edit.html:5
+#: aleksis/core/templates/oauth2_provider/application/edit.html:6
+msgid "Edit OAuth2 Application"
 msgstr ""
 
-#: aleksis/core/templates/oauth2_provider/application_list.html:8
-msgid "OAuth2 applications"
+#: aleksis/core/templates/oauth2_provider/application/list.html:11
+msgid "Register new application"
 msgstr ""
 
-#: aleksis/core/templates/oauth2_provider/application_list.html:12
-msgid "Register new applications"
-msgstr ""
-
-#: aleksis/core/templates/oauth2_provider/application_list.html:23
+#: aleksis/core/templates/oauth2_provider/application/list.html:25
 msgid "No applications defined."
 msgstr ""
 
 #: aleksis/core/templates/oauth2_provider/authorize.html:5
-#: aleksis/core/templates/oauth2_provider/authorize.html:16
+#: aleksis/core/templates/socialaccount/login.html:5
+#: aleksis/core/templates/socialaccount/login.html:6
 msgid "Authorize"
 msgstr ""
 
-#: aleksis/core/templates/oauth2_provider/authorize.html:18
+#: aleksis/core/templates/oauth2_provider/authorize.html:23
+#, python-format
+msgid "Authorize %(name)s"
+msgstr ""
+
+#: aleksis/core/templates/oauth2_provider/authorize.html:25
 msgid "The application requests access to the following scopes:"
 msgstr ""
 
-#: aleksis/core/templates/oauth2_provider/authorize.html:33
+#: aleksis/core/templates/oauth2_provider/authorize.html:40
 msgid "Allow"
 msgstr ""
 
-#: aleksis/core/templates/oauth2_provider/authorize.html:36
+#: aleksis/core/templates/oauth2_provider/authorize.html:43
 msgid "Disallow"
 msgstr ""
 
@@ -2217,6 +2414,20 @@ msgstr ""
 msgid "Add a Third-party Account"
 msgstr ""
 
+#: aleksis/core/templates/socialaccount/login.html:12
+#, python-format
+msgid "You are about to connect a new third party account from %(provider)s."
+msgstr ""
+
+#: aleksis/core/templates/socialaccount/login.html:23
+#, python-format
+msgid "You are about to sign in using a third party account from %(provider)s."
+msgstr ""
+
+#: aleksis/core/templates/socialaccount/login.html:28
+msgid "Continue"
+msgstr ""
+
 #: aleksis/core/templates/socialaccount/login_cancelled.html:5
 #: aleksis/core/templates/socialaccount/login_cancelled.html:6
 #: aleksis/core/templates/socialaccount/login_cancelled.html:13
@@ -2261,26 +2472,60 @@ msgid ""
 "        "
 msgstr ""
 
-#: aleksis/core/templates/templated_email/data_checks.email:4
-msgid "The system detected some new problems with your data."
+#: aleksis/core/templates/templated_email/base.email:5
+#: aleksis/core/templates/templated_email/base.email:16
+msgid "Hello"
 msgstr ""
 
-#: aleksis/core/templates/templated_email/data_checks.email:8
-#: aleksis/core/templates/templated_email/data_checks.email:24
-#: aleksis/core/templates/templated_email/person_changed.email:8
-#: aleksis/core/templates/templated_email/person_changed.email:20
-msgid "Hello,"
+#: aleksis/core/templates/templated_email/celery_failure.email:4
+#, python-format
+msgid "Celery task %(task_name)s failed!"
 msgstr ""
 
-#: aleksis/core/templates/templated_email/data_checks.email:10
+#: aleksis/core/templates/templated_email/celery_failure.email:7
+#, python-format
+msgid "the celery task %(task_name)s failed with following information:"
+msgstr ""
+
+#: aleksis/core/templates/templated_email/celery_failure.email:10
+#: aleksis/core/templates/templated_email/celery_failure.email:29
+msgid "Task ID"
+msgstr ""
+
+#: aleksis/core/templates/templated_email/celery_failure.email:11
+#: aleksis/core/templates/templated_email/celery_failure.email:30
+msgid "Raised exception"
+msgstr ""
+
+#: aleksis/core/templates/templated_email/celery_failure.email:12
+#: aleksis/core/templates/templated_email/celery_failure.email:31
+msgid "Positional arguments"
+msgstr ""
+
+#: aleksis/core/templates/templated_email/celery_failure.email:15
+#: aleksis/core/templates/templated_email/celery_failure.email:38
+msgid "Keyword arguments"
+msgstr ""
+
+#: aleksis/core/templates/templated_email/celery_failure.email:22
+#, python-format
 msgid ""
 "\n"
-"  the system detected some new problems with your data.\n"
-"  Please take some time to inspect them and solve the issues or mark them as ignored.\n"
-" "
+"      the celery task %(task_name)s failed with following information:\n"
+"    "
 msgstr ""
 
-#: aleksis/core/templates/templated_email/data_checks.email:26
+#: aleksis/core/templates/templated_email/data_checks.email:3
+msgid "The system detected some new problems with your data."
+msgstr ""
+
+#: aleksis/core/templates/templated_email/data_checks.email:6
+msgid ""
+"the system detected some new problems with your data.\n"
+"Please take some time to inspect them and solve the issues or mark them as ignored."
+msgstr ""
+
+#: aleksis/core/templates/templated_email/data_checks.email:15
 msgid ""
 "\n"
 "   the system detected some new problems with your data.\n"
@@ -2288,43 +2533,40 @@ msgid ""
 "  "
 msgstr ""
 
-#: aleksis/core/templates/templated_email/data_checks.email:34
+#: aleksis/core/templates/templated_email/data_checks.email:23
 msgid "Problem description"
 msgstr ""
 
-#: aleksis/core/templates/templated_email/data_checks.email:35
+#: aleksis/core/templates/templated_email/data_checks.email:24
 msgid "Count of objects with new problems"
 msgstr ""
 
-#: aleksis/core/templates/templated_email/notification.email:3
+#: aleksis/core/templates/templated_email/notification.email:4
 msgid "New notification for"
 msgstr ""
 
 #: aleksis/core/templates/templated_email/notification.email:6
-#: aleksis/core/templates/templated_email/notification.email:27
+#: aleksis/core/templates/templated_email/notification.email:20
 #, python-format
-msgid "Dear %(notification_user)s,"
+msgid "Hello %(notification_user)s,"
 msgstr ""
 
-#: aleksis/core/templates/templated_email/notification.email:8
-#: aleksis/core/templates/templated_email/notification.email:29
+#: aleksis/core/templates/templated_email/notification.email:9
+#: aleksis/core/templates/templated_email/notification.email:23
 msgid "we got a new notification for you:"
 msgstr ""
 
 #: aleksis/core/templates/templated_email/notification.email:15
-#: aleksis/core/templates/templated_email/notification.email:35
+#: aleksis/core/templates/templated_email/notification.email:29
 msgid "More information"
 msgstr ""
 
-#: aleksis/core/templates/templated_email/notification.email:18
+#: aleksis/core/templates/templated_email/notification.email:17
 #, python-format
-msgid ""
-"\n"
-"        Sent by %(trans_sender)s at %(trans_created_at)s\n"
-"    "
+msgid "Sent by %(trans_sender)s at %(trans_created_at)s"
 msgstr ""
 
-#: aleksis/core/templates/templated_email/notification.email:40
+#: aleksis/core/templates/templated_email/notification.email:34
 #, python-format
 msgid ""
 "\n"
@@ -2337,15 +2579,12 @@ msgstr ""
 msgid "%(person)s changed their data!"
 msgstr ""
 
-#: aleksis/core/templates/templated_email/person_changed.email:10
+#: aleksis/core/templates/templated_email/person_changed.email:7
 #, python-format
-msgid ""
-"\n"
-"   the person %(person)s recently changed the following fields:\n"
-" "
+msgid "the person %(person)s recently changed the following fields:"
 msgstr ""
 
-#: aleksis/core/templates/templated_email/person_changed.email:22
+#: aleksis/core/templates/templated_email/person_changed.email:15
 #, python-format
 msgid ""
 "\n"
@@ -2396,21 +2635,30 @@ msgstr ""
 msgid "Generate Tokens"
 msgstr ""
 
-#: aleksis/core/templates/two_factor/core/login.html:20
+#: aleksis/core/templates/two_factor/core/login.html:27
+#, python-format
+msgid "Login for %(name)s"
+msgstr ""
+
+#: aleksis/core/templates/two_factor/core/login.html:30
 msgid "Login with username and password"
 msgstr ""
 
-#: aleksis/core/templates/two_factor/core/login.html:28
+#: aleksis/core/templates/two_factor/core/login.html:38
 msgid ""
 "You have no permission to view this page. Please login with an other\n"
 "                    account."
 msgstr ""
 
-#: aleksis/core/templates/two_factor/core/login.html:36
+#: aleksis/core/templates/two_factor/core/login.html:47
+msgid "Please login with your account to use the external application."
+msgstr ""
+
+#: aleksis/core/templates/two_factor/core/login.html:54
 msgid "Please login to see this page."
 msgstr ""
 
-#: aleksis/core/templates/two_factor/core/login.html:46
+#: aleksis/core/templates/two_factor/core/login.html:65
 msgid ""
 "\n"
 "                        We are calling your phone right now, please enter the\n"
@@ -2418,7 +2666,7 @@ msgid ""
 "                      "
 msgstr ""
 
-#: aleksis/core/templates/two_factor/core/login.html:51
+#: aleksis/core/templates/two_factor/core/login.html:70
 msgid ""
 "\n"
 "                        We sent you a text message, please enter the tokens we\n"
@@ -2426,7 +2674,7 @@ msgid ""
 "                      "
 msgstr ""
 
-#: aleksis/core/templates/two_factor/core/login.html:56
+#: aleksis/core/templates/two_factor/core/login.html:75
 msgid ""
 "\n"
 "                        Please enter the tokens generated by your token\n"
@@ -2434,7 +2682,7 @@ msgid ""
 "                      "
 msgstr ""
 
-#: aleksis/core/templates/two_factor/core/login.html:62
+#: aleksis/core/templates/two_factor/core/login.html:81
 msgid ""
 "\n"
 "                      Use this form for entering backup tokens for logging in.\n"
@@ -2443,23 +2691,23 @@ msgid ""
 "                    "
 msgstr ""
 
-#: aleksis/core/templates/two_factor/core/login.html:90
+#: aleksis/core/templates/two_factor/core/login.html:109
 msgid "Device currently not available?"
 msgstr ""
 
-#: aleksis/core/templates/two_factor/core/login.html:92
+#: aleksis/core/templates/two_factor/core/login.html:111
 msgid "Or, alternatively, use one of your backup phones:"
 msgstr ""
 
-#: aleksis/core/templates/two_factor/core/login.html:102
+#: aleksis/core/templates/two_factor/core/login.html:121
 msgid "As a last resort, you can use a backup token:"
 msgstr ""
 
-#: aleksis/core/templates/two_factor/core/login.html:105
+#: aleksis/core/templates/two_factor/core/login.html:124
 msgid "Use Backup Token"
 msgstr ""
 
-#: aleksis/core/templates/two_factor/core/login.html:116
+#: aleksis/core/templates/two_factor/core/login.html:135
 msgid "Use alternative login options"
 msgstr ""
 
@@ -2696,135 +2944,184 @@ msgid ""
 "      "
 msgstr ""
 
-#: aleksis/core/util/notifications.py:65
+#: aleksis/core/util/notifications.py:63
 msgid "E-Mail"
 msgstr ""
 
-#: aleksis/core/util/notifications.py:66
+#: aleksis/core/util/notifications.py:64
 msgid "SMS"
 msgstr ""
 
-#: aleksis/core/util/pdf.py:105
+#: aleksis/core/util/pdf.py:118
 msgid "Progress: Generate PDF file"
 msgstr ""
 
-#: aleksis/core/util/pdf.py:106
+#: aleksis/core/util/pdf.py:119
 msgid "Generating PDF file …"
 msgstr ""
 
-#: aleksis/core/util/pdf.py:107
+#: aleksis/core/util/pdf.py:120
 msgid "The PDF file has been generated successfully."
 msgstr ""
 
-#: aleksis/core/util/pdf.py:108
+#: aleksis/core/util/pdf.py:121
 msgid "There was a problem while generating the PDF file."
 msgstr ""
 
-#: aleksis/core/util/pdf.py:111
+#: aleksis/core/util/pdf.py:124
 msgid "Download PDF"
 msgstr ""
 
-#: aleksis/core/views.py:251
+#: aleksis/core/views.py:285
 msgid "The school term has been created."
 msgstr ""
 
-#: aleksis/core/views.py:263
+#: aleksis/core/views.py:297
 msgid "The school term has been saved."
 msgstr ""
 
-#: aleksis/core/views.py:387
+#: aleksis/core/views.py:417
 msgid "The child groups were successfully saved."
 msgstr ""
 
-#: aleksis/core/views.py:406 aleksis/core/views.py:416
+#: aleksis/core/views.py:436 aleksis/core/views.py:446
 msgid "The person has been saved."
 msgstr ""
 
-#: aleksis/core/views.py:466
+#: aleksis/core/views.py:496
 msgid "The group has been saved."
 msgstr ""
 
-#: aleksis/core/views.py:563
+#: aleksis/core/views.py:593
 msgid "The announcement has been saved."
 msgstr ""
 
-#: aleksis/core/views.py:579
+#: aleksis/core/views.py:609
 msgid "The announcement has been deleted."
 msgstr ""
 
-#: aleksis/core/views.py:663
+#: aleksis/core/views.py:677
+msgid "The requested preference registry does not exist"
+msgstr ""
+
+#: aleksis/core/views.py:696
 msgid "The preferences have been saved successfully."
 msgstr ""
 
-#: aleksis/core/views.py:687
+#: aleksis/core/views.py:720
 msgid "The person has been deleted."
 msgstr ""
 
-#: aleksis/core/views.py:701
+#: aleksis/core/views.py:734
 msgid "The group has been deleted."
 msgstr ""
 
-#: aleksis/core/views.py:733
+#: aleksis/core/views.py:766
 msgid "The additional_field has been saved."
 msgstr ""
 
-#: aleksis/core/views.py:767
+#: aleksis/core/views.py:800
 msgid "The additional field has been deleted."
 msgstr ""
 
-#: aleksis/core/views.py:792
+#: aleksis/core/views.py:825
 msgid "The group type has been saved."
 msgstr ""
 
-#: aleksis/core/views.py:822
+#: aleksis/core/views.py:855
 msgid "The group type has been deleted."
 msgstr ""
 
-#: aleksis/core/views.py:855
+#: aleksis/core/views.py:888
 msgid "Progress: Run data checks"
 msgstr ""
 
-#: aleksis/core/views.py:856
+#: aleksis/core/views.py:889
 msgid "Run data checks …"
 msgstr ""
 
-#: aleksis/core/views.py:857
+#: aleksis/core/views.py:890
 msgid "The data checks were run successfully."
 msgstr ""
 
-#: aleksis/core/views.py:858
+#: aleksis/core/views.py:891
 msgid "There was a problem while running data checks."
 msgstr ""
 
-#: aleksis/core/views.py:874
+#: aleksis/core/views.py:907
 #, python-brace-format
 msgid "The solve option '{solve_option_obj.verbose_name}' "
 msgstr ""
 
-#: aleksis/core/views.py:916
+#: aleksis/core/views.py:917
+msgid "The requested solve option does not exist"
+msgstr ""
+
+#: aleksis/core/views.py:949
 msgid "The dashboard widget has been saved."
 msgstr ""
 
-#: aleksis/core/views.py:946
+#: aleksis/core/views.py:979
 msgid "The dashboard widget has been created."
 msgstr ""
 
-#: aleksis/core/views.py:956
+#: aleksis/core/views.py:989
 msgid "The dashboard widget has been deleted."
 msgstr ""
 
-#: aleksis/core/views.py:1023
+#: aleksis/core/views.py:1060
 msgid "Your dashboard configuration has been saved successfully."
 msgstr ""
 
-#: aleksis/core/views.py:1025
+#: aleksis/core/views.py:1062
 msgid "The configuration of the default dashboard has been saved successfully."
 msgstr ""
 
-#: aleksis/core/views.py:1153
+#: aleksis/core/views.py:1138
+#, python-brace-format
+msgid "The invitation was successfully created. The invitation code is {code}"
+msgstr ""
+
+#: aleksis/core/views.py:1229
+msgid "We have successfully assigned the permissions."
+msgstr ""
+
+#: aleksis/core/views.py:1239
+msgid "The global user permission has been deleted."
+msgstr ""
+
+#: aleksis/core/views.py:1249
+msgid "The global group permission has been deleted."
+msgstr ""
+
+#: aleksis/core/views.py:1259
+msgid "The object user permission has been deleted."
+msgstr ""
+
+#: aleksis/core/views.py:1269
+msgid "The object group permission has been deleted."
+msgstr ""
+
+#: aleksis/core/views.py:1337
+msgid "The requested PDF file does not exist"
+msgstr ""
+
+#: aleksis/core/views.py:1346 aleksis/core/views.py:1350
+msgid "The requested task does not exist or is not accessible"
+msgstr ""
+
+#: aleksis/core/views.py:1388
 msgid "The third-party account could not be disconnected because it is the only login method available."
 msgstr ""
 
-#: aleksis/core/views.py:1160
+#: aleksis/core/views.py:1395
 msgid "The third-party account has been successfully disconnected."
 msgstr ""
+
+#: aleksis/core/views.py:1466
+msgid "Person was invited successfully and an email with further instructions has been send to them."
+msgstr ""
+
+#: aleksis/core/views.py:1477
+msgid "Person was already invited."
+msgstr ""
diff --git a/aleksis/core/locale/tr_TR/LC_MESSAGES/djangojs.po b/aleksis/core/locale/tr_TR/LC_MESSAGES/djangojs.po
index dc6d028e5442a3666f35a7cb46e39e5c01cdd3f9..dfec73c5c894d3d686092593be9c21a811cd0535 100644
--- a/aleksis/core/locale/tr_TR/LC_MESSAGES/djangojs.po
+++ b/aleksis/core/locale/tr_TR/LC_MESSAGES/djangojs.po
@@ -8,7 +8,7 @@ msgid ""
 msgstr ""
 "Project-Id-Version: PACKAGE VERSION\n"
 "Report-Msgid-Bugs-To: \n"
-"POT-Creation-Date: 2021-10-28 16:18+0200\n"
+"POT-Creation-Date: 2022-02-08 23:16+0000\n"
 "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
 "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
 "Language-Team: LANGUAGE <LL@li.org>\n"
@@ -17,18 +17,18 @@ msgstr ""
 "Content-Type: text/plain; charset=UTF-8\n"
 "Content-Transfer-Encoding: 8bit\n"
 
-#: aleksis/core/static/js/main.js:15
+#: aleksis/core/static/js/main.js:66
 msgid "Today"
 msgstr ""
 
-#: aleksis/core/static/js/main.js:16
+#: aleksis/core/static/js/main.js:67
 msgid "Cancel"
 msgstr ""
 
-#: aleksis/core/static/js/main.js:17
+#: aleksis/core/static/js/main.js:68
 msgid "OK"
 msgstr ""
 
-#: aleksis/core/static/js/main.js:127
+#: aleksis/core/static/js/main.js:191
 msgid "This page may contain outdated information since there is no internet connection."
 msgstr ""
diff --git a/aleksis/core/managers.py b/aleksis/core/managers.py
index aed4cab72d4b0cd53d3544b20f0180a7087dfd2c..7a549775f1730d57f83e6c3486644fd2c3bdda65 100644
--- a/aleksis/core/managers.py
+++ b/aleksis/core/managers.py
@@ -7,6 +7,7 @@ from django.db.models import QuerySet
 from django.db.models.manager import Manager
 
 from calendarweek import CalendarWeek
+from django_cte import CTEManager, CTEQuerySet
 from polymorphic.managers import PolymorphicManager
 
 
@@ -84,15 +85,15 @@ class SchoolTermRelatedQuerySet(QuerySet):
             return None
 
 
-class GroupManager(CurrentSiteManagerWithoutMigrations):
+class GroupManager(CurrentSiteManagerWithoutMigrations, CTEManager):
     """Manager adding specific methods to groups."""
 
     def get_queryset(self):
         """Ensure all related data is loaded as well."""
-        return super().get_queryset().select_related("school_term")
+        return Manager.get_queryset(self).select_related("school_term")
 
 
-class GroupQuerySet(SchoolTermRelatedQuerySet):
+class GroupQuerySet(SchoolTermRelatedQuerySet, CTEQuerySet):
     pass
 
 
diff --git a/aleksis/core/menus.py b/aleksis/core/menus.py
index d9fd84b0f78a1ce141537725d233f57b43a4fbea..da2d102535b5daa91c8484475ec843c16e4d66aa 100644
--- a/aleksis/core/menus.py
+++ b/aleksis/core/menus.py
@@ -20,6 +20,15 @@ MENUS = {
                 ("aleksis.core.util.predicates.permission_validator", "core.can_register"),
             ],
         },
+        {
+            "name": _("Accept invitation"),
+            "url": "enter_invitation_code",
+            "icon": "vpn_key",
+            "validators": [
+                "menu_generator.validators.is_anonymous",
+                ("aleksis.core.util.predicates.permission_validator", "core.invite_enabled"),
+            ],
+        },
         {
             "name": _("Dashboard"),
             "url": "index",
@@ -34,85 +43,10 @@ MENUS = {
             "icon": "notifications",
             "badge": unread_notifications_badge,
             "validators": [
-                ("aleksis.core.util.predicates.permission_validator", "core.view_notifications",),
-            ],
-        },
-        {
-            "name": _("Account"),
-            "url": "#",
-            "icon": "person",
-            "root": True,
-            "validators": ["menu_generator.validators.is_authenticated"],
-            "submenu": [
-                {
-                    "name": _("Stop impersonation"),
-                    "url": "impersonate-stop",
-                    "icon": "stop",
-                    "validators": [
-                        "menu_generator.validators.is_authenticated",
-                        "aleksis.core.util.core_helpers.is_impersonate",
-                    ],
-                },
-                {
-                    "name": _("Logout"),
-                    "url": "logout",
-                    "icon": "exit_to_app",
-                    "validators": ["menu_generator.validators.is_authenticated"],
-                },
-                {
-                    "name": _("2FA"),
-                    "url": "two_factor:profile",
-                    "icon": "phonelink_lock",
-                    "validators": ["menu_generator.validators.is_authenticated",],
-                },
-                {
-                    "name": _("Change password"),
-                    "url": "account_change_password",
-                    "icon": "lock",
-                    "validators": [
-                        "menu_generator.validators.is_authenticated",
-                        (
-                            "aleksis.core.util.predicates.permission_validator",
-                            "core.can_change_password",
-                        ),
-                    ],
-                },
-                {
-                    "name": _("Me"),
-                    "url": "person",
-                    "icon": "insert_emoticon",
-                    "validators": [
-                        "menu_generator.validators.is_authenticated",
-                        "aleksis.core.util.core_helpers.has_person",
-                    ],
-                },
-                {
-                    "name": _("Preferences"),
-                    "url": "preferences_person",
-                    "icon": "settings",
-                    "validators": [
-                        "menu_generator.validators.is_authenticated",
-                        "aleksis.core.util.core_helpers.has_person",
-                    ],
-                },
-                {
-                    "name": _("Third-party accounts"),
-                    "url": "socialaccount_connections",
-                    "icon": "public",
-                    "validators": [
-                        "menu_generator.validators.is_authenticated",
-                        "aleksis.core.util.core_helpers.has_person",
-                    ],
-                },
-                {
-                    "name": _("Authorized applications"),
-                    "url": "oauth2_provider:authorized-token-list",
-                    "icon": "touch_app",
-                    "validators": [
-                        "menu_generator.validators.is_authenticated",
-                        "aleksis.core.util.core_helpers.has_person",
-                    ],
-                },
+                (
+                    "aleksis.core.util.predicates.permission_validator",
+                    "core.view_notifications",
+                ),
             ],
         },
         {
@@ -178,17 +112,6 @@ MENUS = {
                         ),
                     ],
                 },
-                {
-                    "name": _("Impersonation"),
-                    "url": "impersonate-list",
-                    "icon": "people",
-                    "validators": [
-                        (
-                            "aleksis.core.util.predicates.permission_validator",
-                            "core.impersonate_rule",
-                        ),
-                    ],
-                },
                 {
                     "name": _("Configuration"),
                     "url": "preferences_site",
@@ -206,15 +129,28 @@ MENUS = {
                     "icon": "done_all",
                     "validators": ["menu_generator.validators.is_superuser"],
                 },
+                {
+                    "name": _("Manage permissions"),
+                    "url": "manage_user_global_permissions",
+                    "icon": "shield",
+                    "validators": [
+                        (
+                            "aleksis.core.util.predicates.permission_validator",
+                            "core.manage_permissions",
+                        ),
+                    ],
+                },
                 {
                     "name": _("Backend Admin"),
                     "url": "admin:index",
                     "icon": "settings",
-                    "validators": ["menu_generator.validators.is_superuser",],
+                    "validators": [
+                        "menu_generator.validators.is_superuser",
+                    ],
                 },
                 {
                     "name": _("OAuth2 Applications"),
-                    "url": "oauth_list",
+                    "url": "oauth2_applications",
                     "icon": "touch_app",
                     "validators": [
                         (
@@ -289,6 +225,15 @@ MENUS = {
                         )
                     ],
                 },
+                {
+                    "name": _("Invite person"),
+                    "url": "invite_person",
+                    "icon": "card_giftcard",
+                    "validators": [
+                        "menu_generator.validators.is_authenticated",
+                        ("aleksis.core.util.predicates.permission_validator", "core.can_invite"),
+                    ],
+                },
             ],
         },
     ],
@@ -304,4 +249,78 @@ MENUS = {
             ],
         },
     ],
+    "NAVBAR_ACCOUNT_MENU": [
+        {
+            "name": _("Stop impersonation"),
+            "url": "impersonate-stop",
+            "icon": "stop",
+            "validators": [
+                "menu_generator.validators.is_authenticated",
+                "aleksis.core.util.core_helpers.is_impersonate",
+            ],
+        },
+        {
+            "name": _("Account"),
+            "url": "person",
+            "icon": "person",
+            "validators": [
+                "menu_generator.validators.is_authenticated",
+                "aleksis.core.util.core_helpers.has_person",
+            ],
+        },
+        {
+            "name": _("Preferences"),
+            "url": "preferences_person",
+            "icon": "settings",
+            "validators": [
+                "menu_generator.validators.is_authenticated",
+                "aleksis.core.util.core_helpers.has_person",
+            ],
+        },
+        {
+            "name": _("2FA"),
+            "url": "two_factor:profile",
+            "icon": "phonelink_lock",
+            "validators": [
+                "menu_generator.validators.is_authenticated",
+            ],
+        },
+        {
+            "name": _("Change password"),
+            "url": "account_change_password",
+            "icon": "lock",
+            "validators": [
+                "menu_generator.validators.is_authenticated",
+                (
+                    "aleksis.core.util.predicates.permission_validator",
+                    "core.can_change_password",
+                ),
+            ],
+        },
+        {
+            "name": _("Third-party accounts"),
+            "url": "socialaccount_connections",
+            "icon": "public",
+            "validators": [
+                "menu_generator.validators.is_authenticated",
+                "aleksis.core.util.core_helpers.has_person",
+            ],
+        },
+        {
+            "name": _("Authorized applications"),
+            "url": "oauth2_provider:authorized-token-list",
+            "icon": "touch_app",
+            "validators": [
+                "menu_generator.validators.is_authenticated",
+                "aleksis.core.util.core_helpers.has_person",
+            ],
+        },
+        {
+            "divider": True,
+            "name": _("Logout"),
+            "url": "logout",
+            "icon": "exit_to_app",
+            "validators": ["menu_generator.validators.is_authenticated"],
+        },
+    ],
 }
diff --git a/aleksis/core/migrations/0026_oauthapplication_allowed_scopes.py b/aleksis/core/migrations/0026_oauthapplication_allowed_scopes.py
new file mode 100644
index 0000000000000000000000000000000000000000..23e1b40b56a2ce2fbe1082b8728adea642f3e82b
--- /dev/null
+++ b/aleksis/core/migrations/0026_oauthapplication_allowed_scopes.py
@@ -0,0 +1,19 @@
+# Generated by Django 3.2.8 on 2021-11-05 10:37
+
+import django.contrib.postgres.fields
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('core', '0025_oauth_align_user_fk'),
+    ]
+
+    operations = [
+        migrations.AddField(
+            model_name='oauthapplication',
+            name='allowed_scopes',
+            field=django.contrib.postgres.fields.ArrayField(base_field=models.CharField(max_length=32), blank=True, null=True, size=None, verbose_name='Allowed scopes that clients can request'),
+        ),
+    ]
diff --git a/aleksis/core/migrations/0027_person_place_of_birth.py b/aleksis/core/migrations/0027_person_place_of_birth.py
new file mode 100644
index 0000000000000000000000000000000000000000..53c5bf169a3e852f6a83d9681bf6a9dd7666bb1d
--- /dev/null
+++ b/aleksis/core/migrations/0027_person_place_of_birth.py
@@ -0,0 +1,18 @@
+# Generated by Django 3.2.8 on 2021-11-02 20:32
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('core', '0026_oauthapplication_allowed_scopes'),
+    ]
+
+    operations = [
+        migrations.AddField(
+            model_name='person',
+            name='place_of_birth',
+            field=models.CharField(blank=True, max_length=255, null=True, verbose_name='Place of birth'),
+        ),
+    ]
diff --git a/aleksis/core/migrations/0028_char_field_not_null.py b/aleksis/core/migrations/0028_char_field_not_null.py
new file mode 100644
index 0000000000000000000000000000000000000000..76c715e03f35d94d92ed26a67b7796f6901b8f10
--- /dev/null
+++ b/aleksis/core/migrations/0028_char_field_not_null.py
@@ -0,0 +1,25 @@
+# Generated by Django 3.2.10 on 2021-12-15 21:10
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('core', '0027_person_place_of_birth'),
+    ]
+
+    operations = [
+        migrations.AlterField(
+            model_name='oauthapplication',
+            name='authorization_grant_type',
+            field=models.CharField(blank=True, choices=[('authorization-code', 'Authorization code'), ('implicit', 'Implicit'), ('password', 'Resource owner password-based'), ('client-credentials', 'Client credentials'), ('openid-hybrid', 'OpenID connect hybrid')], default='', max_length=32),
+            preserve_default=False,
+        ),
+        migrations.AlterField(
+            model_name='person',
+            name='place_of_birth',
+            field=models.CharField(blank=True, default='', max_length=255, verbose_name='Place of birth'),
+            preserve_default=False,
+        ),
+    ]
diff --git a/aleksis/core/migrations/0029_invitations.py b/aleksis/core/migrations/0029_invitations.py
new file mode 100644
index 0000000000000000000000000000000000000000..0c4410267bf7064dbb0b8c9b57ff7f15a9645e93
--- /dev/null
+++ b/aleksis/core/migrations/0029_invitations.py
@@ -0,0 +1,34 @@
+# Generated by Django 3.1.7 on 2021-03-31 16:55
+
+import aleksis.core.mixins
+from django.conf import settings
+from django.db import migrations, models
+import django.db.models.deletion
+import django.utils.timezone
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        migrations.swappable_dependency(settings.AUTH_USER_MODEL),
+        ('core', '0028_char_field_not_null'),
+    ]
+
+    operations = [
+        migrations.CreateModel(
+            name='PersonInvitation',
+            fields=[
+                ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+                ('accepted', models.BooleanField(default=False, verbose_name='accepted')),
+                ('key', models.CharField(max_length=64, unique=True, verbose_name='key')),
+                ('sent', models.DateTimeField(null=True, verbose_name='sent')),
+                ('email', models.EmailField(blank=True, max_length=254, verbose_name='E-Mail address')),
+                ('inviter', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
+                ('person', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='invitation', to='core.person'))
+            ],
+            options={
+                'abstract': False,
+            },
+            bases=(models.Model, aleksis.core.mixins.PureDjangoModel),
+        ),
+    ]
diff --git a/aleksis/core/migrations/0030_user_attributes.py b/aleksis/core/migrations/0030_user_attributes.py
new file mode 100644
index 0000000000000000000000000000000000000000..38434842aae54f5f4ad004094c33c524dd0a69ba
--- /dev/null
+++ b/aleksis/core/migrations/0030_user_attributes.py
@@ -0,0 +1,46 @@
+# Generated by Django 3.2.10 on 2021-12-25 10:59
+
+import aleksis.core.mixins
+from django.conf import settings
+from django.contrib.auth import get_user_model
+from django.db import migrations, models
+import django.db.models.deletion
+
+
+def assume_ldap_authenticated_true(apps, schema_editor):
+    """Set ldap_authenticated user attribute to True to protect existing sites."""
+    if not hasattr(settings, "AUTH_LDAP_SERVER_URI"):
+        # Skip if LDAP is not used on site
+        return
+
+    User = get_user_model()
+    UserAdditionalAttributes = apps.get_model("core", "UserAdditionalAttributes")
+
+    db_alias = schema_editor.connection.alias
+
+    attributes = [
+        UserAdditionalAttributes(user_id=user.pk, attributes={"ldap_authenticated": True})
+        for user in User.objects.using(db_alias).all()
+    ]
+    UserAdditionalAttributes.objects.using(db_alias).bulk_create(attributes)
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        migrations.swappable_dependency(settings.AUTH_USER_MODEL),
+        ('core', '0029_invitations'),
+    ]
+
+    operations = [
+        migrations.CreateModel(
+            name='UserAdditionalAttributes',
+            fields=[
+                ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+                ('attributes', models.JSONField(default=dict, verbose_name='Additional attributes')),
+                ('user', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='additional_attributes', to=settings.AUTH_USER_MODEL, verbose_name='Linked user')),
+            ],
+            bases=(models.Model, aleksis.core.mixins.PureDjangoModel),
+        ),
+        migrations.RunPython(assume_ldap_authenticated_true, lambda a, s: None),
+    ]
diff --git a/aleksis/core/migrations/0031_oauthapplication_icon.py b/aleksis/core/migrations/0031_oauthapplication_icon.py
new file mode 100644
index 0000000000000000000000000000000000000000..26f2d6575e768578e653088c582fd3dadd147dc5
--- /dev/null
+++ b/aleksis/core/migrations/0031_oauthapplication_icon.py
@@ -0,0 +1,18 @@
+# Generated by Django 3.2.11 on 2022-01-08 13:59
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('core', '0030_user_attributes'),
+    ]
+
+    operations = [
+        migrations.AddField(
+            model_name='oauthapplication',
+            name='icon',
+            field=models.ImageField(blank=True, help_text='This image will be shown as icon in the authorization flow. It should be squared.', null=True, upload_to='', verbose_name='Icon'),
+        ),
+    ]
diff --git a/aleksis/core/migrations/0032_remove_person_is_active.py b/aleksis/core/migrations/0032_remove_person_is_active.py
new file mode 100644
index 0000000000000000000000000000000000000000..927913f3febb7ec5e791aab26bf56cd7b8aab2df
--- /dev/null
+++ b/aleksis/core/migrations/0032_remove_person_is_active.py
@@ -0,0 +1,17 @@
+# Generated by Django 3.2.11 on 2022-01-08 10:30
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('core', '0031_oauthapplication_icon'),
+    ]
+
+    operations = [
+        migrations.RemoveField(
+            model_name='person',
+            name='is_active',
+        ),
+    ]
diff --git a/aleksis/core/migrations/0033_update_photo_avatar.py b/aleksis/core/migrations/0033_update_photo_avatar.py
new file mode 100644
index 0000000000000000000000000000000000000000..e20c6ee6bff00581fa7f586b2e9e9888ce1dfd1d
--- /dev/null
+++ b/aleksis/core/migrations/0033_update_photo_avatar.py
@@ -0,0 +1,41 @@
+# Generated by Django 3.2.9 on 2021-12-09 14:22
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('core', '0032_remove_person_is_active'),
+    ]
+
+    operations = [
+        migrations.AddField(
+            model_name='group',
+            name='avatar',
+            field=models.ImageField(blank=True, help_text='This is a picture or an avatar for public display.', null=True, upload_to='', verbose_name='Display picture / Avatar'),
+        ),
+        migrations.AddField(
+            model_name='group',
+            name='photo',
+            field=models.ImageField(blank=True, help_text='This is an official photo, used for official documents and for internal use cases.', null=True, upload_to='', verbose_name='Photo'),
+        ),
+        migrations.AddField(
+            model_name='person',
+            name='avatar',
+            field=models.ImageField(blank=True, help_text='This is a picture or an avatar for public display.', null=True, upload_to='', verbose_name='Display picture / Avatar'),
+        ),
+        migrations.AlterField(
+            model_name='person',
+            name='photo',
+            field=models.ImageField(blank=True, help_text='This is an official photo, used for official documents and for internal use cases.', null=True, upload_to='', verbose_name='Photo'),
+        ),
+        migrations.AlterModelOptions(
+            name='globalpermissions',
+            options={'default_permissions': (), 'managed': False, 'permissions': (('view_system_status', 'Can view system status'), ('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.AlterModelOptions(
+            name='person',
+            options={'ordering': ['last_name', 'first_name'], 'permissions': (('view_address', 'Can view address'), ('view_contact_details', 'Can view contact details'), ('view_photo', 'Can view photo'), ('view_avatar', 'Can view avatar image'), ('view_person_groups', 'Can view persons groups'), ('view_personal_details', 'Can view personal details')), 'verbose_name': 'Person', 'verbose_name_plural': 'Persons'},
+        ),
+    ]
diff --git a/aleksis/core/migrations/0034_invite_permission.py b/aleksis/core/migrations/0034_invite_permission.py
new file mode 100644
index 0000000000000000000000000000000000000000..02a2d24d791a1bb69597c872aee5e3d5e1e52dea
--- /dev/null
+++ b/aleksis/core/migrations/0034_invite_permission.py
@@ -0,0 +1,17 @@
+# Generated by Django 3.2.11 on 2022-01-22 20:18
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('core', '0033_update_photo_avatar'),
+    ]
+
+    operations = [
+        migrations.AlterModelOptions(
+            name='globalpermissions',
+            options={'default_permissions': (), 'managed': False, 'permissions': (('view_system_status', 'Can view system status'), ('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'), ('invite', 'Can invite persons'))},
+        ),
+    ]
diff --git a/aleksis/core/migrations/0035_preference_model_unique.py b/aleksis/core/migrations/0035_preference_model_unique.py
new file mode 100644
index 0000000000000000000000000000000000000000..b5227060eaf9ba46ec8a07261ec6a56cdeb256a7
--- /dev/null
+++ b/aleksis/core/migrations/0035_preference_model_unique.py
@@ -0,0 +1,51 @@
+# Generated by Django 3.2.11 on 2022-01-27 18:52
+
+from email.headerregistry import Group
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('sites', '0002_alter_domain_unique'),
+        ('core', '0034_invite_permission'),
+    ]
+
+    def _migrate_preference_models_to_unique(apps, schema_editor):
+        from aleksis.core.models import GroupPreferenceModel, PersonPreferenceModel, SitePreferenceModel
+
+        models = [GroupPreferenceModel, PersonPreferenceModel, SitePreferenceModel]
+
+        for model in models:
+            duplicates = {}
+            db_alias = schema_editor.connection.alias
+            for obj in model.objects.using(db_alias).all():
+                key = f"{obj.instance.pk}__{obj.section}__{obj.name}"
+                duplicates.setdefault(key, [])
+                duplicates[key].append(obj)
+
+            for key, objs in duplicates.items():
+                if len(objs) > 1:
+                    found = False
+                    for obj in objs:
+                        if obj.value == obj.preference.default or found:
+                            obj.delete()
+                        else:
+                            found = True
+
+
+    operations = [
+        migrations.RunPython(_migrate_preference_models_to_unique),
+        migrations.AlterUniqueTogether(
+            name='grouppreferencemodel',
+            unique_together={('instance', 'section', 'name')},
+        ),
+        migrations.AlterUniqueTogether(
+            name='personpreferencemodel',
+            unique_together={('instance', 'section', 'name')},
+        ),
+        migrations.AlterUniqueTogether(
+            name='sitepreferencemodel',
+            unique_together={('instance', 'section', 'name')},
+        ),
+    ]
diff --git a/aleksis/core/migrations/0036_additionalfields_helptext_required.py b/aleksis/core/migrations/0036_additionalfields_helptext_required.py
new file mode 100644
index 0000000000000000000000000000000000000000..9486105645a238f437b0a25eced23e56d7d805c2
--- /dev/null
+++ b/aleksis/core/migrations/0036_additionalfields_helptext_required.py
@@ -0,0 +1,23 @@
+# Generated by Django 3.2.12 on 2022-02-18 21:48
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('core', '0035_preference_model_unique'),
+    ]
+
+    operations = [
+        migrations.AddField(
+            model_name='additionalfield',
+            name='help_text',
+            field=models.TextField(blank=True, verbose_name='Help text / description'),
+        ),
+        migrations.AddField(
+            model_name='additionalfield',
+            name='required',
+            field=models.BooleanField(default=False, verbose_name='Required'),
+        ),
+    ]
diff --git a/aleksis/core/migrations/0037_add_static_content_widget.py b/aleksis/core/migrations/0037_add_static_content_widget.py
new file mode 100644
index 0000000000000000000000000000000000000000..bb4e810a614c1357243bdda4c5ac42b7f844058a
--- /dev/null
+++ b/aleksis/core/migrations/0037_add_static_content_widget.py
@@ -0,0 +1,27 @@
+# Generated by Django 3.2.12 on 2022-02-23 18:03
+
+import ckeditor.fields
+from django.db import migrations, models
+import django.db.models.deletion
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('core', '0036_additionalfields_helptext_required'),
+    ]
+
+    operations = [
+        migrations.CreateModel(
+            name='StaticContentWidget',
+            fields=[
+                ('dashboardwidget_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='core.dashboardwidget')),
+                ('content', ckeditor.fields.RichTextField(verbose_name='Content')),
+            ],
+            options={
+                'verbose_name': 'Static content widget',
+                'verbose_name_plural': 'Static content widgets',
+            },
+            bases=('core.dashboardwidget',),
+        ),
+    ]
diff --git a/aleksis/core/migrations/0038_notification_send_at.py b/aleksis/core/migrations/0038_notification_send_at.py
new file mode 100644
index 0000000000000000000000000000000000000000..edb96f1e87f143d60477a1578aea5d90226389a8
--- /dev/null
+++ b/aleksis/core/migrations/0038_notification_send_at.py
@@ -0,0 +1,20 @@
+# Generated by Django 3.2.12 on 2022-02-23 19:33
+
+from django.db import migrations, models
+import django.utils.timezone
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('core', '0037_add_static_content_widget'),
+    ]
+
+    operations = [
+        migrations.AddField(
+            model_name='notification',
+            name='send_at',
+            field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='Send notification at'),
+            preserve_default=False,
+        ),
+    ]
diff --git a/aleksis/core/mixins.py b/aleksis/core/mixins.py
index 1e4fd42168f66d4ca986dca670f277ead0cbf3e7..c6cee9a8c6bbefed3439eef156b685f5484e4a13 100644
--- a/aleksis/core/mixins.py
+++ b/aleksis/core/mixins.py
@@ -1,7 +1,8 @@
 # flake8: noqa: DJ12
 
+import os
 from datetime import datetime
-from typing import Any, Callable, Optional, Union
+from typing import Any, Callable, List, Optional, Union
 
 from django.conf import settings
 from django.contrib import messages
@@ -11,6 +12,7 @@ from django.contrib.sites.managers import CurrentSiteManager
 from django.contrib.sites.models import Site
 from django.db import models
 from django.db.models import JSONField, QuerySet
+from django.db.models.fields import CharField, TextField
 from django.forms.forms import BaseForm
 from django.forms.models import ModelForm, ModelFormMetaclass
 from django.http import HttpResponse
@@ -20,6 +22,8 @@ from django.views.generic import CreateView, UpdateView
 from django.views.generic.edit import DeleteView, ModelFormMixin
 
 import reversion
+from dynamic_preferences.settings import preferences_settings
+from dynamic_preferences.types import FilePreference
 from guardian.admin import GuardedModelAdmin
 from guardian.core import ObjectPermissionChecker
 from jsonstore.fields import IntegerField, JSONFieldMixin
@@ -276,6 +280,15 @@ class ExtensibleModel(models.Model, metaclass=_ExtensibleModelBase):
 
             to.property_(_virtual_related, related_name)
 
+    @classmethod
+    def get_filter_fields(cls) -> List[str]:
+        """Get names of all text-searchable fields of this model."""
+        fields = []
+        for field in cls.syncable_fields():
+            if isinstance(field, (CharField, TextField)):
+                fields.append(field.name)
+        return fields
+
     @classmethod
     def syncable_fields(
         cls, recursive: bool = True, exclude_remotes: list = []
@@ -519,3 +532,14 @@ class SchoolTermRelatedExtensibleForm(ExtensibleForm):
             kwargs["initial"] = {"school_term": SchoolTerm.current}
 
         super().__init__(*args, **kwargs)
+
+
+class PublicFilePreferenceMixin(FilePreference):
+    """Uploads a file to the public namespace."""
+
+    upload_path = "public"
+
+    def get_upload_path(self):
+        return os.path.join(
+            self.upload_path, preferences_settings.FILE_PREFERENCE_UPLOAD_DIR, self.identifier()
+        )
diff --git a/aleksis/core/models.py b/aleksis/core/models.py
index 279a6fc798b5829fecac5dd09e2287c50ebc816a..7c555b409c0aac4a8631df21e6aabadbadd26242 100644
--- a/aleksis/core/models.py
+++ b/aleksis/core/models.py
@@ -1,7 +1,8 @@
 # flake8: noqa: DJ01
+import base64
 import hmac
 from datetime import date, datetime, timedelta
-from typing import Iterable, List, Optional, Sequence, Union
+from typing import Any, Iterable, List, Optional, Sequence, Union
 from urllib.parse import urlparse
 
 from django.conf import settings
@@ -9,11 +10,12 @@ from django.contrib.auth import get_user_model
 from django.contrib.auth.models import Group as DjangoGroup
 from django.contrib.contenttypes.fields import GenericForeignKey
 from django.contrib.contenttypes.models import ContentType
+from django.contrib.postgres.fields import ArrayField
 from django.contrib.sites.models import Site
 from django.core.exceptions import ValidationError
 from django.core.validators import MaxValueValidator
 from django.db import models, transaction
-from django.db.models import QuerySet
+from django.db.models import Q, QuerySet
 from django.db.models.signals import m2m_changed
 from django.dispatch import receiver
 from django.forms.widgets import Media
@@ -23,11 +25,19 @@ from django.utils.functional import classproperty
 from django.utils.text import slugify
 from django.utils.translation import gettext_lazy as _
 
+import customidenticon
 import jsonstore
 from cachalot.api import cachalot_disabled
 from cache_memoize import cache_memoize
+from celery.result import AsyncResult
+from ckeditor.fields import RichTextField
 from django_celery_results.models import TaskResult
+from django_cte import CTEQuerySet, With
 from dynamic_preferences.models import PerInstancePreferenceModel
+from invitations import signals
+from invitations.adapters import get_invitations_adapter
+from invitations.base_invitation import AbstractBaseInvitation
+from invitations.models import Invitation
 from model_utils import FieldTracker
 from model_utils.models import TimeStampedModel
 from oauth2_provider.models import (
@@ -39,7 +49,6 @@ from oauth2_provider.models import (
 )
 from phonenumber_field.modelfields import PhoneNumberField
 from polymorphic.models import PolymorphicModel
-from templated_email import send_templated_mail
 
 from aleksis.core.data_checks import BrokenDashboardWidgetDataCheck, DataCheck, DataCheckRegistry
 
@@ -58,7 +67,8 @@ from .mixins import (
     SchoolTermRelatedExtensibleModel,
 )
 from .tasks import send_notification
-from .util.core_helpers import get_site_preferences, now_tomorrow
+from .util.core_helpers import generate_random_code, get_site_preferences, now_tomorrow
+from .util.email import send_email
 from .util.model_helpers import ICONS
 
 FIELD_CHOICES = (
@@ -149,6 +159,7 @@ class Person(ExtensibleModel):
             ("view_address", _("Can view address")),
             ("view_contact_details", _("Can view contact details")),
             ("view_photo", _("Can view photo")),
+            ("view_avatar", _("Can view avatar image")),
             ("view_person_groups", _("Can view persons groups")),
             ("view_personal_details", _("Can view personal details")),
         )
@@ -170,7 +181,6 @@ class Person(ExtensibleModel):
         related_name="person",
         verbose_name=_("Linked user"),
     )
-    is_active = models.BooleanField(verbose_name=_("Is person active?"), default=True)
 
     first_name = models.CharField(verbose_name=_("First name"), max_length=255)
     last_name = models.CharField(verbose_name=_("Last name"), max_length=255)
@@ -193,9 +203,24 @@ class Person(ExtensibleModel):
     email = models.EmailField(verbose_name=_("E-mail address"), blank=True)
 
     date_of_birth = models.DateField(verbose_name=_("Date of birth"), blank=True, null=True)
+    place_of_birth = models.CharField(verbose_name=_("Place of birth"), max_length=255, blank=True)
     sex = models.CharField(verbose_name=_("Sex"), max_length=1, choices=SEX_CHOICES, blank=True)
 
-    photo = models.ImageField(verbose_name=_("Photo"), blank=True, null=True)
+    photo = models.ImageField(
+        verbose_name=_("Photo"),
+        blank=True,
+        null=True,
+        help_text=_(
+            "This is an official photo, used for official documents and for internal use cases."
+        ),
+    )
+
+    avatar = models.ImageField(
+        verbose_name=_("Display picture / Avatar"),
+        blank=True,
+        null=True,
+        help_text=_("This is a picture or an avatar for public display."),
+    )
 
     guardians = models.ManyToManyField(
         "self",
@@ -291,20 +316,72 @@ class Person(ExtensibleModel):
         """Return the count of unread notifications for this person."""
         return self.unread_notifications.count()
 
-    user_info_tracker = FieldTracker(fields=("first_name", "last_name", "email"))
+    @property
+    def initials(self):
+        initials = ""
+        if self.first_name:
+            initials += self.first_name[0]
+        if self.last_name:
+            initials += self.last_name[0]
+        return initials.upper() or "?"
+
+    user_info_tracker = FieldTracker(fields=("first_name", "last_name", "email", "user_id"))
+
+    @property
+    def member_of_recursive(self) -> QuerySet:
+        """Get all groups this person is a member of, recursively."""
+        q = self.member_of
+        for group in q.all():
+            q = q.union(group.parent_groups_recursive)
+        return q
+
+    @property
+    def owner_of_recursive(self) -> QuerySet:
+        """Get all groups this person is a member of, recursively."""
+        q = self.owner_of
+        for group in q.all():
+            q = q.union(group.child_groups_recursive)
+        return q
+
+    @property
+    @cache_memoize(60 * 60)
+    def identicon_url(self):
+        identicon = customidenticon.create(self.full_name, border=35)
+        base64_data = base64.b64encode(identicon).decode("ascii")
+        return f"data:image/png;base64,{base64_data}"
+
+    @property
+    def avatar_url(self):
+        if self.avatar:
+            return self.avatar.url
+        else:
+            return self.identicon_url
 
     def save(self, *args, **kwargs):
         # Determine all fields that were changed since last load
-        dirty = self.pk is None or bool(self.user_info_tracker.changed())
+        changed = self.user_info_tracker.changed()
 
         super().save(*args, **kwargs)
 
-        if self.user and dirty:
-            # Synchronise user fields to linked User object to keep it up to date
-            self.user.first_name = self.first_name
-            self.user.last_name = self.last_name
-            self.user.email = self.email
-            self.user.save()
+        if self.pk is None or bool(changed):
+            if "user_id" in changed:
+                # Clear groups of previous Django user
+                previous_user = changed["user_id"]
+                if previous_user is not None:
+                    get_user_model().objects.get(pk=previous_user).groups.clear()
+
+            if self.user:
+                if "first_name" in changed or "last_name" in changed or "email" in changed:
+                    # Synchronise user fields to linked User object to keep it up to date
+                    self.user.first_name = self.first_name
+                    self.user.last_name = self.last_name
+                    self.user.email = self.email
+                    self.user.save()
+
+                if "user_id" in changed:
+                    # Synchronise groups to Django groups
+                    for group in self.member_of.union(self.owner_of.all()).all():
+                        group.save(force=True)
 
         # Select a primary group if none is set
         self.auto_select_primary_group()
@@ -345,10 +422,13 @@ class Person(ExtensibleModel):
         recipients = recipients or [
             get_site_preferences()["account__person_change_notification_contact"]
         ]
-        send_templated_mail(
+        send_email(
             template_name="person_changed",
             from_email=self.mail_sender_via,
-            headers={"Reply-To": self.mail_sender, "Sender": self.mail_sender,},
+            headers={
+                "Reply-To": self.mail_sender,
+                "Sender": self.mail_sender,
+            },
             recipient_list=recipients,
             context=context,
         )
@@ -378,6 +458,8 @@ class AdditionalField(ExtensibleModel):
     field_type = models.CharField(
         verbose_name=_("Type of field"), choices=FIELD_CHOICES, max_length=50
     )
+    required = models.BooleanField(verbose_name=_("Required"), default=False)
+    help_text = models.TextField(verbose_name=_("Help text / description"), blank=True)
 
     def __str__(self) -> str:
         return self.title
@@ -453,6 +535,21 @@ class Group(SchoolTermRelatedExtensibleModel):
         AdditionalField, verbose_name=_("Additional fields"), blank=True
     )
 
+    photo = models.ImageField(
+        verbose_name=_("Photo"),
+        blank=True,
+        null=True,
+        help_text=_(
+            "This is an official photo, used for official documents and for internal use cases."
+        ),
+    )
+    avatar = models.ImageField(
+        verbose_name=_("Display picture / Avatar"),
+        blank=True,
+        null=True,
+        help_text=_("This is a picture or an avatar for public display."),
+    )
+
     def get_absolute_url(self) -> str:
         return reverse("group_by_id", args=[self.id])
 
@@ -463,7 +560,7 @@ class Group(SchoolTermRelatedExtensibleModel):
 
     @property
     def get_group_stats(self) -> dict:
-        """ Get stats about a given group """
+        """Get stats about a given group"""
         stats = {}
 
         stats["members"] = len(self.members.all())
@@ -477,6 +574,50 @@ class Group(SchoolTermRelatedExtensibleModel):
 
         return stats
 
+    @property
+    def parent_groups_recursive(self) -> CTEQuerySet:
+        """Get all parent groups recursively."""
+
+        def _make_cte(cte):
+            Through = self.parent_groups.through
+            return (
+                Through.objects.values("to_group_id")
+                .filter(from_group=self)
+                .union(cte.join(Through, from_group=cte.col.to_group_id), all=True)
+            )
+
+        cte = With.recursive(_make_cte)
+        return cte.join(Group, id=cte.col.to_group_id).with_cte(cte)
+
+    @property
+    def child_groups_recursive(self) -> CTEQuerySet:
+        """Get all child groups recursively."""
+
+        def _make_cte(cte):
+            Through = self.child_groups.through
+            return (
+                Through.objects.values("from_group_id")
+                .filter(to_group=self)
+                .union(cte.join(Through, to_group=cte.col.from_group_id), all=True)
+            )
+
+        cte = With.recursive(_make_cte)
+        return cte.join(Group, id=cte.col.from_group_id).with_cte(cte)
+
+    @property
+    def members_recursive(self) -> QuerySet:
+        """Get all members of this group and its child groups."""
+        return Person.objects.filter(
+            Q(member_of=self) | Q(member_of__in=self.child_groups_recursive)
+        )
+
+    @property
+    def owners_recursive(self) -> QuerySet:
+        """Get all ownerss of this group and its parent groups."""
+        return Person.objects.filter(
+            Q(owner_of=self) | Q(owner_of__in=self.parent_groups_recursive)
+        )
+
     def __str__(self) -> str:
         if self.school_term:
             return f"{self.name} ({self.short_name}) ({self.school_term})"
@@ -503,6 +644,12 @@ class Group(SchoolTermRelatedExtensibleModel):
             )
             dj_group.save()
 
+    @property
+    def django_group(self):
+        """Get Django group for this group."""
+        dj_group, _ = DjangoGroup.objects.get_or_create(name=self.name)
+        return dj_group
+
 
 class PersonGroupThrough(ExtensibleModel):
     """Through table for many-to-many relationship of group members.
@@ -594,6 +741,8 @@ class Notification(ExtensibleModel, TimeStampedModel):
     description = models.TextField(max_length=500, verbose_name=_("Description"))
     link = models.URLField(blank=True, verbose_name=_("Link"))
 
+    send_at = models.DateTimeField(default=timezone.now, verbose_name=_("Send notification at"))
+
     read = models.BooleanField(default=False, verbose_name=_("Read"))
     sent = models.BooleanField(default=False, verbose_name=_("Sent"))
 
@@ -602,10 +751,14 @@ class Notification(ExtensibleModel, TimeStampedModel):
 
     def save(self, **kwargs):
         super().save(**kwargs)
-        if not self.sent:
-            send_notification(self.pk, resend=True)
-        self.sent = True
-        super().save(**kwargs)
+        if not self.sent and self.send_at <= timezone.now():
+            self.send()
+            super().save(**kwargs)
+
+    def send(self, resend: bool = False) -> Optional[AsyncResult]:
+        """Send the notification to the recipient."""
+        if not self.sent or resend:
+            return send_notification.delay(self.pk, resend=True)
 
     class Meta:
         verbose_name = _("Notification")
@@ -683,7 +836,8 @@ class Announcement(ExtensibleModel):
         verbose_name=_("Date and time from when to show"), default=timezone.now
     )
     valid_until = models.DateTimeField(
-        verbose_name=_("Date and time until when to show"), default=now_tomorrow,
+        verbose_name=_("Date and time until when to show"),
+        default=now_tomorrow,
     )
 
     @property
@@ -750,40 +904,7 @@ class AnnouncementRecipient(ExtensibleModel):
 
 
 class DashboardWidget(PolymorphicModel, PureDjangoModel):
-    """Base class for dashboard widgets on the index page.
-
-    To implement a widget, add a model that subclasses DashboardWidget, sets the template
-    and implements the get_context method to return a dictionary to be passed as context
-    to the template.
-
-    If your widget does not add any database fields, you should mark it as a proxy model.
-
-    You can provide a Media meta class with custom JS and CSS files which
-    will be added to html head.  For further information on media definition
-    see https://docs.djangoproject.com/en/3.0/topics/forms/media/
-
-    Example::
-
-      from django.forms.widgets import Media
-
-      from aleksis.core.models import DashboardWidget
-
-      class MyWidget(DashboardWidget):
-          template = "myapp/widget.html"
-
-          def get_context(self, request):
-              context = {"some_content": "foo"}
-              return context
-
-          class Meta:
-              proxy = True
-
-          media = Media(css={
-                  'all': ('pretty.css',)
-              },
-              js=('animations.js', 'actions.js')
-          )
-    """
+    """Base class for dashboard widgets on the index page."""
 
     objects = UninstallRenitentPolymorphicManager()
 
@@ -875,6 +996,19 @@ class ExternalLinkWidget(DashboardWidget):
         verbose_name_plural = _("External link widgets")
 
 
+class StaticContentWidget(DashboardWidget):
+    template = "core/dashboard_widget/static_content_widget.html"
+
+    content = RichTextField(verbose_name=_("Content"))
+
+    def get_context(self, request):
+        return {"title": self.title, "content": self.content}
+
+    class Meta:
+        verbose_name = _("Static content widget")
+        verbose_name_plural = _("Static content widgets")
+
+
 class DashboardWidgetOrder(ExtensibleModel):
     widget = models.ForeignKey(
         DashboardWidget, on_delete=models.CASCADE, verbose_name=_("Dashboard widget")
@@ -982,12 +1116,8 @@ class GlobalPermissions(GlobalPermissionModel):
             ("change_site_preferences", _("Can change site preferences")),
             ("change_person_preferences", _("Can change person preferences")),
             ("change_group_preferences", _("Can change group preferences")),
-            ("add_oauth_applications", _("Can add oauth applications")),
-            ("list_oauth_applications", _("Can list oauth applications")),
-            ("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")),
+            ("invite", _("Can invite persons")),
         )
 
 
@@ -996,7 +1126,7 @@ class SitePreferenceModel(PerInstancePreferenceModel, PureDjangoModel):
 
     instance = models.ForeignKey(Site, on_delete=models.CASCADE)
 
-    class Meta:
+    class Meta(PerInstancePreferenceModel.Meta):
         app_label = "core"
 
 
@@ -1005,7 +1135,7 @@ class PersonPreferenceModel(PerInstancePreferenceModel, PureDjangoModel):
 
     instance = models.ForeignKey(Person, on_delete=models.CASCADE)
 
-    class Meta:
+    class Meta(PerInstancePreferenceModel.Meta):
         app_label = "core"
 
 
@@ -1014,7 +1144,7 @@ class GroupPreferenceModel(PerInstancePreferenceModel, PureDjangoModel):
 
     instance = models.ForeignKey(Group, on_delete=models.CASCADE)
 
-    class Meta:
+    class Meta(PerInstancePreferenceModel.Meta):
         app_label = "core"
 
 
@@ -1053,6 +1183,30 @@ class DataCheckResult(ExtensibleModel):
         )
 
 
+class PersonInvitation(AbstractBaseInvitation, PureDjangoModel):
+    """Custom model for invitations to allow to generate invitations codes without email address."""
+
+    email = models.EmailField(verbose_name=_("E-Mail address"), blank=True)
+    person = models.ForeignKey(
+        Person, on_delete=models.CASCADE, blank=True, related_name="invitation", null=True
+    )
+
+    def __str__(self) -> str:
+        return f"{self.email} ({self.inviter})"
+
+    @classmethod
+    def create(cls, email, inviter=None, **kwargs):
+        length = get_site_preferences()["auth__invite_code_length"]
+        packet_size = get_site_preferences()["auth__invite_code_packet_size"]
+        code = generate_random_code(length, packet_size)
+
+        instance = cls.objects.create(email=email, inviter=inviter, key=code, **kwargs)
+        return instance
+
+    key_expired = Invitation.key_expired
+    send_invitation = Invitation.send_invitation
+
+
 class PDFFile(ExtensibleModel):
     """Link to a rendered PDF file."""
 
@@ -1104,18 +1258,75 @@ class TaskUserAssignment(ExtensibleModel):
         verbose_name_plural = _("Task user assignments")
 
 
+class UserAdditionalAttributes(models.Model, PureDjangoModel):
+    """Additional attributes for Django user accounts.
+
+    These attributes are explicitly linked to a User, not to a Person.
+    """
+
+    user = models.OneToOneField(
+        get_user_model(),
+        on_delete=models.CASCADE,
+        related_name="additional_attributes",
+        verbose_name=_("Linked user"),
+    )
+
+    attributes = models.JSONField(verbose_name=_("Additional attributes"), default=dict)
+
+    @classmethod
+    def get_user_attribute(
+        cls, username: str, attribute: str, default: Optional[Any] = None
+    ) -> Any:
+        """Get a user attribute for a user by name."""
+        try:
+            attributes = cls.objects.get(user__username=username)
+        except cls.DoesNotExist:
+            return default
+
+        return attributes.attributes.get(attribute, default)
+
+    @classmethod
+    def set_user_attribute(cls, username: str, attribute: str, value: Any):
+        """Set a user attribute for a user by name.
+
+        Raises DoesNotExist if a username for a non-existing Django user is passed.
+        """
+        user = get_user_model().objects.get(username=username)
+        attributes, __ = cls.objects.update_or_create(user=user)
+
+        attributes.attributes[attribute] = value
+        attributes.save()
+
+
 class OAuthApplication(AbstractApplication):
     """Modified OAuth application class that supports Grant Flows configured in preferences."""
 
     # Override grant types to make field optional
     authorization_grant_type = models.CharField(
-        max_length=32, choices=AbstractApplication.GRANT_TYPES, blank=True, null=True
+        max_length=32, choices=AbstractApplication.GRANT_TYPES, blank=True
+    )
+
+    # Optional list of alloewd scopes
+    allowed_scopes = ArrayField(
+        models.CharField(max_length=32),
+        verbose_name=_("Allowed scopes that clients can request"),
+        null=True,
+        blank=True,
+    )
+
+    icon = models.ImageField(
+        verbose_name=_("Icon"),
+        blank=True,
+        null=True,
+        help_text=_(
+            "This image will be shown as icon in the authorization flow. It should be squared."
+        ),
     )
 
     def allows_grant_type(self, *grant_types: set[str]) -> bool:
         allowed_grants = get_site_preferences()["auth__oauth_allowed_grants"]
 
-        return bool(set(allowed_grants) & grant_types)
+        return bool(set(allowed_grants) & set(grant_types))
 
 
 class OAuthGrant(AbstractGrant):
diff --git a/aleksis/core/preferences.py b/aleksis/core/preferences.py
index d0de22fe03cc044fa5722982dfcc7faf86a4503c..78b1874356a3a11eab121a56963c9aaf3595d872 100644
--- a/aleksis/core/preferences.py
+++ b/aleksis/core/preferences.py
@@ -3,6 +3,7 @@ from django.forms import EmailField, ImageField, URLField
 from django.forms.widgets import SelectMultiple
 from django.utils.translation import gettext_lazy as _
 
+import pycountry
 from colorfield.widgets import ColorWidget
 from dynamic_preferences.preferences import Section
 from dynamic_preferences.types import (
@@ -16,6 +17,7 @@ from dynamic_preferences.types import (
 )
 from oauth2_provider.models import AbstractApplication
 
+from .mixins import PublicFilePreferenceMixin
 from .models import Group, Person
 from .registries import person_preferences_registry, site_preferences_registry
 from .util.notifications import get_notification_choices_lazy
@@ -38,8 +40,8 @@ class SiteTitle(StringPreference):
     section = general
     name = "title"
     default = "AlekSIS"
-    required = False
     verbose_name = _("Site title")
+    required = True
 
 
 @site_preferences_registry.register
@@ -60,9 +62,9 @@ class ColourPrimary(StringPreference):
     section = theme
     name = "primary"
     default = "#0d5eaf"
-    required = False
     verbose_name = _("Primary colour")
     widget = ColorWidget
+    required = True
 
 
 @site_preferences_registry.register
@@ -72,39 +74,53 @@ class ColourSecondary(StringPreference):
     section = theme
     name = "secondary"
     default = "#0d5eaf"
-    required = False
     verbose_name = _("Secondary colour")
     widget = ColorWidget
+    required = True
 
 
 @site_preferences_registry.register
-class Logo(FilePreference):
+class Logo(PublicFilePreferenceMixin, FilePreference):
     """Logo of your AlekSIS instance."""
 
     section = theme
     field_class = ImageField
     name = "logo"
     verbose_name = _("Logo")
+    required = False
 
 
 @site_preferences_registry.register
-class Favicon(FilePreference):
+class Favicon(PublicFilePreferenceMixin, FilePreference):
     """Favicon of your AlekSIS instance."""
 
     section = theme
     field_class = ImageField
     name = "favicon"
     verbose_name = _("Favicon")
+    required = False
 
 
 @site_preferences_registry.register
-class PWAIcon(FilePreference):
+class PWAIcon(PublicFilePreferenceMixin, FilePreference):
     """PWA-Icon of your AlekSIS instance."""
 
     section = theme
     field_class = ImageField
     name = "pwa_icon"
     verbose_name = _("PWA-Icon")
+    required = False
+
+
+@site_preferences_registry.register
+class PWAIconMaskable(BooleanPreference):
+    """PWA icon is maskable."""
+
+    section = theme
+    name = "pwa_icon_maskable"
+    verbose_name = _("PWA-Icon is maskable")
+    default = True
+    required = False
 
 
 @site_preferences_registry.register
@@ -114,8 +130,8 @@ class MailOutName(StringPreference):
     section = mail
     name = "name"
     default = "AlekSIS"
-    required = False
     verbose_name = _("Mail out name")
+    required = True
 
 
 @site_preferences_registry.register
@@ -125,9 +141,9 @@ class MailOut(StringPreference):
     section = mail
     name = "address"
     default = settings.DEFAULT_FROM_EMAIL
-    required = False
     verbose_name = _("Mail out address")
     field_class = EmailField
+    required = True
 
 
 @site_preferences_registry.register
@@ -161,12 +177,12 @@ class AdressingNameFormat(ChoicePreference):
     section = notification
     name = "addressing_name_format"
     default = "first_last"
-    required = False
     verbose_name = _("Name format for addressing")
     choices = (
         ("first_last", "John Doe"),
         ("last_fist", "Doe, John"),
     )
+    required = True
 
 
 @person_preferences_registry.register
@@ -255,6 +271,14 @@ class AllowPasswordChange(BooleanPreference):
     verbose_name = _("Allow users to change their passwords")
 
 
+@site_preferences_registry.register
+class AllowPasswordReset(BooleanPreference):
+    section = auth
+    name = "allow_password_reset"
+    default = True
+    verbose_name = _("Allow users to reset their passwords")
+
+
 @site_preferences_registry.register
 class SignupEnabled(BooleanPreference):
     section = auth
@@ -263,6 +287,38 @@ class SignupEnabled(BooleanPreference):
     verbose_name = _("Enable signup")
 
 
+@site_preferences_registry.register
+class AllowedUsernameRegex(StringPreference):
+    section = auth
+    name = "allowed_username_regex"
+    default = ".+"
+    verbose_name = _("Regular expression for allowed usernames")
+
+
+@site_preferences_registry.register
+class InviteEnabled(BooleanPreference):
+    section = auth
+    name = "invite_enabled"
+    default = False
+    verbose_name = _("Enable invitations")
+
+
+@site_preferences_registry.register
+class InviteCodeLength(IntegerPreference):
+    section = auth
+    name = "invite_code_length"
+    default = 3
+    verbose_name = _("Length of invite code. (Default 3: abcde-acbde-abcde)")
+
+
+@site_preferences_registry.register
+class InviteCodePacketSize(IntegerPreference):
+    section = auth
+    name = "invite_code_packet_size"
+    default = 5
+    verbose_name = _("Size of packets. (Default 5: abcde)")
+
+
 @site_preferences_registry.register
 class OAuthAllowedGrants(MultipleChoicePreference):
     """Grant Flows allowed for OAuth applications."""
@@ -274,6 +330,7 @@ class OAuthAllowedGrants(MultipleChoicePreference):
     verbose_name = _("Allowed Grant Flows for OAuth applications")
     field_attribute = {"initial": []}
     choices = AbstractApplication.GRANT_TYPES
+    required = False
 
 
 @site_preferences_registry.register
@@ -287,6 +344,7 @@ class AvailableLanguages(MultipleChoicePreference):
     verbose_name = _("Available languages")
     field_attribute = {"initial": []}
     choices = settings.LANGUAGES
+    required = True
 
 
 @site_preferences_registry.register
@@ -350,6 +408,7 @@ class EditableFieldsPerson(MultipleChoicePreference):
     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()]
+    required = False
 
 
 @site_preferences_registry.register
@@ -365,6 +424,7 @@ class SendNotificationOnPersonChange(MultipleChoicePreference):
     )
     field_attribute = {"initial": []}
     choices = [(field.name, field.name) for field in Person.syncable_fields()]
+    required = False
 
 
 @site_preferences_registry.register
@@ -375,6 +435,17 @@ class PersonChangeNotificationContact(StringPreference):
     name = "person_change_notification_contact"
     default = ""
     verbose_name = _("Contact for notification if a person changes their data")
+    required = False
+
+
+@site_preferences_registry.register
+class PersonPreferPhoto(BooleanPreference):
+    """Preference, whether personal photos should be displayed instead of avatars."""
+
+    section = account
+    name = "person_prefer_photo"
+    default = False
+    verbose_name = _("Prefer personal photos over avatars")
 
 
 @site_preferences_registry.register
@@ -406,3 +477,13 @@ class AutoUpdatingDashboardSite(BooleanPreference):
     name = "automatically_update_dashboard_site"
     default = True
     verbose_name = _("Automatically update the dashboard and its widgets sitewide")
+
+
+@site_preferences_registry.register
+class PhoneNumberCountry(ChoicePreference):
+    section = internationalisation
+    name = "phone_number_country"
+    required = True
+    default = "GB"
+    choices = [(x.alpha_2, x.alpha_2) for x in pycountry.countries]
+    verbose_name = _("Country for phone number parsing")
diff --git a/aleksis/core/rules.py b/aleksis/core/rules.py
index e8e5443e15396554befa1f14c2536c66231c76cf..022fa11a00671a3acb0775c2229a378bd3981342 100644
--- a/aleksis/core/rules.py
+++ b/aleksis/core/rules.py
@@ -1,4 +1,5 @@
 import rules
+from rules import is_superuser
 
 from .models import AdditionalField, Announcement, Group, GroupType, Person
 from .util.predicates import (
@@ -57,6 +58,12 @@ view_photo_predicate = has_person & (
 )
 rules.add_perm("core.view_photo_rule", view_photo_predicate)
 
+# View person avatar image
+view_avatar_predicate = has_person & (
+    has_global_perm("core.view_avatar") | has_object_perm("core.view_avatar") | is_current_person
+)
+rules.add_perm("core.view_avatar_rule", view_avatar_predicate)
+
 # View persons groups
 view_groups_predicate = has_person & (
     has_global_perm("core.view_person_groups")
@@ -196,7 +203,7 @@ rules.add_perm("core.change_additionalfield_rule", change_additional_field_predi
 
 # Edit additional field
 create_additional_field_predicate = has_person & (
-    has_global_perm("core.create_additionalfield") | has_object_perm("core.create_additionalfield")
+    has_global_perm("core.add_additionalfield") | has_object_perm("core.add_additionalfield")
 )
 rules.add_perm("core.create_additionalfield_rule", create_additional_field_predicate)
 
@@ -222,7 +229,7 @@ rules.add_perm("core.edit_grouptype_rule", change_group_type_predicate)
 
 # Create group type
 create_group_type_predicate = has_person & (
-    has_global_perm("core.create_grouptype") | has_object_perm("core.change_grouptype")
+    has_global_perm("core.add_grouptype") | has_object_perm("core.add_grouptype")
 )
 rules.add_perm("core.create_grouptype_rule", create_group_type_predicate)
 
@@ -241,13 +248,13 @@ rules.add_perm("core.view_grouptypes_rule", view_group_types_predicate)
 
 # Create person
 create_person_predicate = has_person & (
-    has_global_perm("core.create_person") | has_object_perm("core.create_person")
+    has_global_perm("core.add_person") | has_object_perm("core.add_person")
 )
 rules.add_perm("core.create_person_rule", create_person_predicate)
 
 # Create group
 create_group_predicate = has_person & (
-    has_global_perm("core.create_group") | has_object_perm("core.create_group")
+    has_global_perm("core.add_group") | has_object_perm("core.add_group")
 )
 rules.add_perm("core.create_group_rule", create_group_predicate)
 
@@ -318,18 +325,25 @@ rules.add_perm("core.can_register", can_register_predicate)
 can_change_password_predicate = is_site_preference_set(section="auth", pref="allow_password_change")
 rules.add_perm("core.can_change_password", can_change_password_predicate)
 
+# django-invitations
+invite_enabled_predicate = is_site_preference_set(section="auth", pref="invite_enabled")
+rules.add_perm("core.invite_enabled", invite_enabled_predicate)
+
+can_invite_predicate = invite_enabled_predicate & has_person & has_global_perm("core.invite")
+rules.add_perm("core.can_invite", can_invite_predicate)
+
 # OAuth2 permissions
-add_oauth_applications_predicate = has_person & has_global_perm("core.add_oauth_applications")
-rules.add_perm("core.add_oauth_applications_rule", add_oauth_applications_predicate)
+create_oauthapplication_predicate = has_person & has_global_perm("core.add_oauthapplication")
+rules.add_perm("core.create_oauthapplication_rule", create_oauthapplication_predicate)
 
-list_oauth_applications_predicate = has_person & has_global_perm("core.list_oauth_applications")
-rules.add_perm("core.list_oauth_applications_rule", list_oauth_applications_predicate)
+view_oauth_applications_predicate = has_person & has_global_perm("core.view_oauthapplication")
+rules.add_perm("core.view_oauthapplications_rule", view_oauth_applications_predicate)
 
-view_oauth_applications_predicate = has_person & has_global_perm("core.view_oauth_applications")
-rules.add_perm("core.view_oauth_applications_rule", view_oauth_applications_predicate)
+view_oauth_application_predicate = has_person & has_global_perm("core.view_oauthapplication")
+rules.add_perm("core.view_oauthapplication_rule", view_oauth_application_predicate)
 
-update_oauth_applications_predicate = has_person & has_global_perm("core.update_oauth_applications")
-rules.add_perm("core.update_oauth_applications_rule", update_oauth_applications_predicate)
+edit_oauth_application_predicate = has_person & has_global_perm("core.change_oauthapplication")
+rules.add_perm("core.edit_oauthapplication_rule", edit_oauth_application_predicate)
 
 delete_oauth_applications_predicate = has_person & has_global_perm("core.delete_oauth_applications")
 rules.add_perm("core.delete_oauth_applications_rule", delete_oauth_applications_predicate)
@@ -338,5 +352,8 @@ rules.add_perm("core.delete_oauth_applications_rule", delete_oauth_applications_
 upload_files_ckeditor_predicate = has_person & has_global_perm("core.upload_files_ckeditor")
 rules.add_perm("core.upload_files_ckeditor_rule", upload_files_ckeditor_predicate)
 
+manage_person_permissions_predicate = has_person & is_superuser
+rules.add_perm("core.manage_permissions", manage_person_permissions_predicate)
+
 test_pdf_generation_predicate = has_person & has_global_perm("core.test_pdf")
 rules.add_perm("core.test_pdf_rule", test_pdf_generation_predicate)
diff --git a/aleksis/core/settings.py b/aleksis/core/settings.py
index df38522f9fe0d5b557cf9f9c378197f64907aebc..df2258fc304f473c5c7be3e8c83783e10cf94353 100644
--- a/aleksis/core/settings.py
+++ b/aleksis/core/settings.py
@@ -1,4 +1,5 @@
 import os
+import warnings
 from glob import glob
 from socket import getfqdn
 
@@ -11,9 +12,11 @@ from .util.core_helpers import get_app_packages, merge_app_settings, monkey_patc
 monkey_patch()
 
 IN_PYTEST = "PYTEST_CURRENT_TEST" in os.environ or "TOX_ENV_DIR" in os.environ
+PYTEST_SETUP_DATABASES = [("default", "default_oot")]
 
 ENVVAR_PREFIX_FOR_DYNACONF = "ALEKSIS"
 DIRS_FOR_DYNACONF = ["/etc/aleksis"]
+MERGE_ENABLED_FOR_DYNACONF = True
 
 SETTINGS_FILE_FOR_DYNACONF = []
 for directory in DIRS_FOR_DYNACONF:
@@ -21,10 +24,15 @@ for directory in DIRS_FOR_DYNACONF:
     SETTINGS_FILE_FOR_DYNACONF += glob(os.path.join(directory, "*.ini"))
     SETTINGS_FILE_FOR_DYNACONF += glob(os.path.join(directory, "*.yaml"))
     SETTINGS_FILE_FOR_DYNACONF += glob(os.path.join(directory, "*.toml"))
+    SETTINGS_FILE_FOR_DYNACONF += glob(os.path.join(directory, "*/*.json"))
+    SETTINGS_FILE_FOR_DYNACONF += glob(os.path.join(directory, "*/*.ini"))
+    SETTINGS_FILE_FOR_DYNACONF += glob(os.path.join(directory, "*/*.yaml"))
+    SETTINGS_FILE_FOR_DYNACONF += glob(os.path.join(directory, "*/*.toml"))
 
 _settings = LazySettings(
     ENVVAR_PREFIX_FOR_DYNACONF=ENVVAR_PREFIX_FOR_DYNACONF,
     SETTINGS_FILE_FOR_DYNACONF=SETTINGS_FILE_FOR_DYNACONF,
+    MERGE_ENABLED_FOR_DYNACONF=MERGE_ENABLED_FOR_DYNACONF,
 )
 
 # Build paths inside the project like this: os.path.join(BASE_DIR, ...)
@@ -89,6 +97,7 @@ INSTALLED_APPS = [
     "rules.apps.AutodiscoverRulesConfig",
     "haystack",
     "polymorphic",
+    "dj_cleavejs.apps.DjCleaveJSConfig",
     "dbbackup",
     "django_celery_beat",
     "django_celery_results",
@@ -117,6 +126,7 @@ INSTALLED_APPS = [
     "allauth",
     "allauth.account",
     "allauth.socialaccount",
+    "invitations",
     "health_check",
     "health_check.db",
     "health_check.cache",
@@ -192,6 +202,14 @@ TEMPLATES = [
     },
 ]
 
+# Attention: The following context processors must accept None
+# as first argument (in addition to a HttpRequest object)
+NON_REQUEST_CONTEXT_PROCESSORS = [
+    "django.template.context_processors.i18n",
+    "django.template.context_processors.tz",
+    "aleksis.core.util.core_helpers.custom_information_processor",
+]
+
 WSGI_APPLICATION = "aleksis.core.wsgi.application"
 
 # Database
@@ -206,9 +224,17 @@ DATABASES = {
         "HOST": _settings.get("database.host", "127.0.0.1"),
         "PORT": _settings.get("database.port", "5432"),
         "CONN_MAX_AGE": _settings.get("database.conn_max_age", None),
+        "OPTIONS": _settings.get("database.options", {}),
     }
 }
 
+# Duplicate default database for out-of-transaction updates
+DATABASES["default_oot"] = DATABASES["default"].copy()
+DATABASE_ROUTERS = [
+    "aleksis.core.util.core_helpers.OOTRouter",
+]
+DATABASE_OOT_LABELS = ["django_celery_results"]
+
 merge_app_settings("DATABASES", DATABASES, False)
 
 REDIS_HOST = _settings.get("redis.host", "localhost")
@@ -227,7 +253,9 @@ if _settings.get("caching.redis.enabled", not IN_PYTEST):
         "default": {
             "BACKEND": "django_redis.cache.RedisCache",
             "LOCATION": _settings.get("caching.redis.address", REDIS_URL),
-            "OPTIONS": {"CLIENT_CLASS": "django_redis.client.DefaultClient",},
+            "OPTIONS": {
+                "CLIENT_CLASS": "django_redis.client.DefaultClient",
+            },
         }
     }
     if REDIS_PASSWORD:
@@ -243,8 +271,9 @@ else:
 INSTALLED_APPS.append("cachalot")
 DEBUG_TOOLBAR_PANELS.append("cachalot.panels.CachalotPanel")
 CACHALOT_TIMEOUT = _settings.get("caching.cachalot.timeout", None)
-CACHALOT_DATABASES = set(["default"])
+CACHALOT_DATABASES = set(["default", "default_oot"])
 SILENCED_SYSTEM_CHECKS += ["cachalot.W001"]
+CACHALOT_ENABLED = _settings.get("caching.query_caching", True)
 
 SESSION_ENGINE = "django.contrib.sessions.backends.cache"
 SESSION_CACHE_ALIAS = "default"
@@ -253,10 +282,18 @@ SESSION_CACHE_ALIAS = "default"
 # https://docs.djangoproject.com/en/2.1/ref/settings/#auth-password-validators
 
 AUTH_PASSWORD_VALIDATORS = [
-    {"NAME": "django.contrib.auth.password_validation.UserAttributeSimilarityValidator",},
-    {"NAME": "django.contrib.auth.password_validation.MinimumLengthValidator",},
-    {"NAME": "django.contrib.auth.password_validation.CommonPasswordValidator",},
-    {"NAME": "django.contrib.auth.password_validation.NumericPasswordValidator",},
+    {
+        "NAME": "django.contrib.auth.password_validation.UserAttributeSimilarityValidator",
+    },
+    {
+        "NAME": "django.contrib.auth.password_validation.MinimumLengthValidator",
+    },
+    {
+        "NAME": "django.contrib.auth.password_validation.CommonPasswordValidator",
+    },
+    {
+        "NAME": "django.contrib.auth.password_validation.NumericPasswordValidator",
+    },
 ]
 
 AUTH_INITIAL_SUPERUSER = {
@@ -303,8 +340,11 @@ ACCOUNT_AUTHENTICATION_METHOD = _settings.get("auth.registration.method", "usern
 ACCOUNT_EMAIL_REQUIRED = _settings.get("auth.registration.email_required", True)
 SOCIALACCOUNT_EMAIL_REQUIRED = False
 
-# Require email verification after sigm up
-ACCOUNT_EMAIL_VERIFICATION = _settings.get("auth.registration.email_verification", "mandatory")
+# Cooldown for verification mails
+ACCOUNT_EMAIL_CONFIRMATION_COOLDOWN = _settings.get("auth.registration.verification_cooldown", 180)
+
+# Require email verification after sign up
+ACCOUNT_EMAIL_VERIFICATION = _settings.get("auth.registration.email_verification", "optional")
 SOCIALACCOUNT_EMAIL_VERIFICATION = False
 
 # Email subject prefix for verification mails
@@ -322,26 +362,52 @@ ACCOUNT_SIGNUP_EMAIL_ENTER_TWICE = True
 # Enforce uniqueness of email addresses
 ACCOUNT_UNIQUE_EMAIL = _settings.get("auth.login.registration.unique_email", True)
 
+# Configurable username validators
+ACCOUNT_USERNAME_VALIDATORS = "aleksis.core.util.auth_helpers.custom_username_validators"
+
+# Configuration for django-invitations
+
+# Expire invitations are configured amout of days
+INVITATIONS_INVITATION_EXPIRY = _settings.get("auth.invitation.expiry", 3)
+# Use email prefix configured for django-allauth
+INVITATIONS_EMAIL_SUBJECT_PREFIX = ACCOUNT_EMAIL_SUBJECT_PREFIX
+# Use custom invitation model
+INVITATIONS_INVITATION_MODEL = "core.PersonInvitation"
+# Use custom invitation form
+INVITATIONS_INVITE_FORM = "aleksis.core.forms.PersonCreateInviteForm"
+# Display error message if invitation code is invalid
+INVITATIONS_GONE_ON_ACCEPT_ERROR = False
+# Mark invitation as accepted after signup
+INVITATIONS_ACCEPT_INVITE_AFTER_SIGNUP = True
+
 # Configuration for OAuth2 provider
-OAUTH2_PROVIDER = {"SCOPES_BACKEND_CLASS": "aleksis.core.util.auth_helpers.AppScopes"}
+OAUTH2_PROVIDER = {
+    "SCOPES_BACKEND_CLASS": "aleksis.core.util.auth_helpers.AppScopes",
+    "OAUTH2_VALIDATOR_CLASS": "aleksis.core.util.auth_helpers.CustomOAuth2Validator",
+    "OIDC_ENABLED": True,
+}
 OAUTH2_PROVIDER_APPLICATION_MODEL = "core.OAuthApplication"
 OAUTH2_PROVIDER_GRANT_MODEL = "core.OAuthGrant"
 OAUTH2_PROVIDER_ACCESS_TOKEN_MODEL = "core.OAuthAccessToken"  # noqa: S105
 OAUTH2_PROVIDER_ID_TOKEN_MODEL = "core.OAuthIDToken"  # noqa: S105
 OAUTH2_PROVIDER_REFRESH_TOKEN_MODEL = "core.OAuthRefreshToken"  # noqa: S105
 
-if _settings.get("oauth2.oidc.enabled", False):
-    with open(_settings.get("oauth2.oidc.rsa_key", "/etc/aleksis/oidc.pem"), "r") as f:
-        oid_rsa_key = f.read()
-
-    OAUTH2_PROVIDER.update(
-        {
-            "OAUTH2_VALIDATOR_CLASS": "aleksis.core.util.auth_helpers.CustomOAuth2Validator",
-            "OIDC_ENABLED": True,
-            "OIDC_RSA_PRIVATE_KEY": oid_rsa_key,
-            #        "OIDC_ISS_ENDPOINT": _settings.get("oauth2.oidc.issuer_name", "example.com"),
-        }
+_OIDC_RSA_KEY_DEFAULT = "/etc/aleksis/oidc.pem"
+_OIDC_RSA_KEY = _settings.get("oauth2.oidc.rsa_key", "/etc/aleksis/oidc.pem")
+if "BEGIN RSA PRIVATE KEY" in _OIDC_RSA_KEY:
+    OAUTH2_PROVIDER["OIDC_RSA_PRIVATE_KEY"] = _OIDC_RSA_KEY
+elif _OIDC_RSA_KEY == _OIDC_RSA_KEY_DEFAULT and not os.path.exists(_OIDC_RSA_KEY):
+    warnings.warn(
+        (
+            f"The default OIDC RSA key in {_OIDC_RSA_KEY} does not exist. "
+            f"RSA will be disabled for now, but creating and configuring a "
+            f"key is recommended. To silence this warning, set oauth2.oidc.rsa_key "
+            f"to the empty string in a configuration file."
+        )
     )
+elif _OIDC_RSA_KEY:
+    with open(_OIDC_RSA_KEY, "r") as f:
+        OAUTH2_PROVIDER["OIDC_RSA_PRIVATE_KEY"] = f.read()
 
 # Configuration for REST framework
 REST_FRAMEWORK = {
@@ -362,6 +428,10 @@ if _settings.get("ldap.uri", None):
         PosixGroupType,
     )
 
+    AUTH_LDAP_GLOBAL_OPTIONS = {
+        ldap.OPT_NETWORK_TIMEOUT: _settings.get("ldap.network_timeout", 3),
+    }
+
     # Enable Django's integration to LDAP
     AUTHENTICATION_BACKENDS.append("aleksis.core.util.ldap.LDAPBackend")
 
@@ -372,7 +442,7 @@ if _settings.get("ldap.uri", None):
         AUTH_LDAP_BIND_DN = _settings.get("ldap.bind.dn")
         AUTH_LDAP_BIND_PASSWORD = _settings.get("ldap.bind.password")
 
-    # Keep local password for users to be required to proveide their old password on change
+    # Keep local password for users to be required to provide their old password on change
     AUTH_LDAP_SET_USABLE_PASSWORD = _settings.get("ldap.handle_passwords", True)
 
     # Keep bound as the authenticating user
@@ -387,7 +457,11 @@ if _settings.get("ldap.uri", None):
     # Search attributes to find users by username
     AUTH_LDAP_USER_SEARCH = LDAPSearchUnion(
         *[
-            LDAPSearch(entry["base"], ldap.SCOPE_SUBTREE, entry.get("filter", "(uid=%(user)s)"),)
+            LDAPSearch(
+                entry["base"],
+                ldap.SCOPE_SUBTREE,
+                entry.get("filter", "(uid=%(user)s)"),
+            )
             for entry in _AUTH_LDAP_USER_SETTINGS
         ]
     )
@@ -442,7 +516,7 @@ if _settings.get("ldap.uri", None):
                 "is_superuser"
             ]
 
-# Add ModelBckend last so all other backends get a chance
+# Add ModelBackend last so all other backends get a chance
 # to verify passwords first
 AUTHENTICATION_BACKENDS.append("django.contrib.auth.backends.ModelBackend")
 
@@ -477,9 +551,10 @@ MEDIA_ROOT = _settings.get("media.root", os.path.join(BASE_DIR, "media"))
 NODE_MODULES_ROOT = _settings.get("node_modules.root", os.path.join(BASE_DIR, "node_modules"))
 
 YARN_INSTALLED_APPS = [
+    "cleave.js",
     "@fontsource/roboto",
     "jquery",
-    "materialize-css",
+    "@materializecss/materialize",
     "material-design-icons-iconfont",
     "select2",
     "select2-materialize",
@@ -487,6 +562,7 @@ YARN_INSTALLED_APPS = [
     "jquery-sortablejs",
     "sortablejs",
     "@sentry/tracing",
+    "luxon",
 ]
 
 merge_app_settings("YARN_INSTALLED_APPS", YARN_INSTALLED_APPS, True)
@@ -499,7 +575,7 @@ SELECT2_JS = JS_URL + "/select2/dist/js/select2.min.js"
 SELECT2_I18N_PATH = JS_URL + "/select2/dist/js/i18n"
 
 ANY_JS = {
-    "materialize": {"js_url": JS_URL + "/materialize-css/dist/js/materialize.min.js"},
+    "materialize": {"js_url": JS_URL + "/@materializecss/materialize/dist/js/materialize.min.js"},
     "jQuery": {"js_url": JS_URL + "/jquery/dist/jquery.min.js"},
     "material-design-icons": {
         "css_url": JS_URL + "/material-design-icons-iconfont/dist/material-design-icons.css"
@@ -518,10 +594,14 @@ ANY_JS = {
     "Roboto700": {"css_url": JS_URL + "/@fontsource/roboto/700.css"},
     "Roboto900": {"css_url": JS_URL + "/@fontsource/roboto/900.css"},
     "Sentry": {"js_url": JS_URL + "/@sentry/tracing/build/bundle.tracing.js"},
+    "cleavejs": {"js_url": JS_URL + "/cleave.js/dist/cleave.min.js"},
+    "luxon": {"js_url": JS_URL + "/luxon/build/global/luxon.min.js"},
 }
 
 merge_app_settings("ANY_JS", ANY_JS, True)
 
+CLEAVE_JS = ANY_JS["cleavejs"]["js_url"]
+
 SASS_PROCESSOR_ENABLED = True
 SASS_PROCESSOR_AUTO_INCLUDE = False
 SASS_PROCESSOR_CUSTOM_FUNCTIONS = {
@@ -529,13 +609,17 @@ SASS_PROCESSOR_CUSTOM_FUNCTIONS = {
     "get-preference": "aleksis.core.util.sass_helpers.get_preference",
 }
 SASS_PROCESSOR_INCLUDE_DIRS = [
-    _settings.get("materialize.sass_path", os.path.join(JS_ROOT, "materialize-css", "sass")),
+    _settings.get(
+        "materialize.sass_path", os.path.join(JS_ROOT, "@materializecss", "materialize", "sass")
+    ),
     os.path.join(STATIC_ROOT, "public"),
 ]
 
-ADMINS = _settings.get("contact.admins", [AUTH_INITIAL_SUPERUSER["email"]])
-SERVER_EMAIL = _settings.get("contact.from", ADMINS[0])
-DEFAULT_FROM_EMAIL = _settings.get("contact.from", ADMINS[0])
+ADMINS = _settings.get(
+    "contact.admins", [(AUTH_INITIAL_SUPERUSER["username"], AUTH_INITIAL_SUPERUSER["email"])]
+)
+SERVER_EMAIL = _settings.get("contact.from", ADMINS[0][1])
+DEFAULT_FROM_EMAIL = _settings.get("contact.from", ADMINS[0][1])
 MANAGERS = _settings.get("contact.admins", ADMINS)
 
 if _settings.get("mail.server.host", None):
@@ -632,8 +716,8 @@ if _settings.get("dev.uwsgi.celery", DEBUG):
     UWSGI["attach-daemon"].append("celery -A aleksis.core beat")
 
 DEFAULT_FAVICON_PATHS = {
-    "pwa_icon": os.path.join(STATIC_ROOT, "img/aleksis-icon.png"),
-    "favicon": os.path.join(STATIC_ROOT, "img/aleksis-icon.png"),
+    "pwa_icon": os.path.join(STATIC_ROOT, "img/aleksis-icon-maskable.png"),
+    "favicon": os.path.join(STATIC_ROOT, "img/aleksis-favicon.png"),
 }
 PWA_ICONS_CONFIG = {
     "android": [192, 512],
@@ -723,7 +807,13 @@ CKEDITOR_CONFIGS = {
             {"name": "colors", "items": ["TextColor", "BGColor"]},
             {"name": "tools", "items": ["Maximize", "ShowBlocks"]},
             {"name": "about", "items": ["About"]},
-            {"name": "customtools", "items": ["Preview", "Maximize",]},
+            {
+                "name": "customtools",
+                "items": [
+                    "Preview",
+                    "Maximize",
+                ],
+            },
         ],
         "toolbar": "Full",
         "tabSpaces": 4,
@@ -778,6 +868,10 @@ LOGGING = {
     "formatters": {"verbose": {"format": "%(levelname)s %(asctime)s %(module)s: %(message)s"}},
     "root": {"handlers": ["console"], "level": _settings.get("logging.level", "WARNING"),},
     "filters": {"ignorable_500": {"()": "aleksis.core.util.core_helpers.Ignorable500URLsFilter"},},
+    "root": {
+        "handlers": ["console"],
+        "level": _settings.get("logging.level", "WARNING"),
+    },
     "loggers": {},
 }
 
@@ -798,7 +892,10 @@ SILENCED_SYSTEM_CHECKS.append("guardian.W001")
 AUTHENTICATION_BACKENDS.append("rules.permissions.ObjectPermissionBackend")
 
 HAYSTACK_CONNECTIONS = {
-    "default": {"ENGINE": "haystack_redis.RedisEngine", "PATH": REDIS_URL,},
+    "default": {
+        "ENGINE": "haystack_redis.RedisEngine",
+        "PATH": REDIS_URL,
+    },
 }
 
 HAYSTACK_SIGNAL_PROCESSOR = "celery_haystack.signals.CelerySignalProcessor"
@@ -822,10 +919,16 @@ PROMETHEUS_METRICS_EXPORT_ADDRESS = _settings.get("prometheus.metrucs.address",
 
 SECURE_PROXY_SSL_HEADER = ("REQUEST_SCHEME", "https")
 
+FILE_UPLOAD_HANDLERS = [
+    "django.core.files.uploadhandler.MemoryFileUploadHandler",
+    "django.core.files.uploadhandler.TemporaryFileUploadHandler",
+]
+
 if _settings.get("storage.type", "").lower() == "s3":
     INSTALLED_APPS.append("storages")
 
     DEFAULT_FILE_STORAGE = "storages.backends.s3boto3.S3Boto3Storage"
+    FILE_UPLOAD_HANDLERS.remove("django.core.files.uploadhandler.MemoryFileUploadHandler")
 
     if _settings.get("storage.s3.static.enabled", False):
         STATICFILES_STORAGE = "storages.backends.s3boto3.S3StaticStorage"
@@ -889,5 +992,13 @@ if SENTRY_ENABLED:
         **SENTRY_SETTINGS,
     )
 
+SHELL_PLUS_MODEL_IMPORTS_RESOLVER = "django_extensions.collision_resolvers.AppLabelPrefixCR"
+SHELL_PLUS_APP_PREFIXES = {
+    "auth": "auth",
+}
+SHELL_PLUS_DONT_LOAD = []
+merge_app_settings("SHELL_PLUS_APP_PREFIXES", SHELL_PLUS_APP_PREFIXES)
+merge_app_settings("SHELL_PLUS_DONT_LOAD", SHELL_PLUS_DONT_LOAD)
+
 # 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/icons/android_192.png b/aleksis/core/static/icons/android_192.png
deleted file mode 100644
index b5f7fec68184883830f68dabc606f7a95c45e8bb..0000000000000000000000000000000000000000
Binary files a/aleksis/core/static/icons/android_192.png and /dev/null differ
diff --git a/aleksis/core/static/icons/android_512.png b/aleksis/core/static/icons/android_512.png
deleted file mode 100644
index 5e042b4f98ef6dd8056ce643ad16ba085658684e..0000000000000000000000000000000000000000
Binary files a/aleksis/core/static/icons/android_512.png and /dev/null differ
diff --git a/aleksis/core/static/icons/apple_114.png b/aleksis/core/static/icons/apple_114.png
deleted file mode 100644
index eb8db13ad567105a819dab802b4f7ddd360e34be..0000000000000000000000000000000000000000
Binary files a/aleksis/core/static/icons/apple_114.png and /dev/null differ
diff --git a/aleksis/core/static/icons/apple_152.png b/aleksis/core/static/icons/apple_152.png
deleted file mode 100644
index 51eaa8d28988a16629e2b884d8de4df58fbadccc..0000000000000000000000000000000000000000
Binary files a/aleksis/core/static/icons/apple_152.png and /dev/null differ
diff --git a/aleksis/core/static/icons/apple_180.png b/aleksis/core/static/icons/apple_180.png
deleted file mode 100644
index 5c6b70846586ef0bf4bd44d177f6f79b49d1cddd..0000000000000000000000000000000000000000
Binary files a/aleksis/core/static/icons/apple_180.png and /dev/null differ
diff --git a/aleksis/core/static/icons/apple_76.png b/aleksis/core/static/icons/apple_76.png
deleted file mode 100644
index 3751b84654d01943148fc6f1b90125a4195d4af9..0000000000000000000000000000000000000000
Binary files a/aleksis/core/static/icons/apple_76.png and /dev/null differ
diff --git a/aleksis/core/static/icons/favicon_16.png b/aleksis/core/static/icons/favicon_16.png
deleted file mode 100644
index f1d02a7a8a34825726d20eeb1f6a63922d1a848e..0000000000000000000000000000000000000000
Binary files a/aleksis/core/static/icons/favicon_16.png and /dev/null differ
diff --git a/aleksis/core/static/icons/favicon_32.png b/aleksis/core/static/icons/favicon_32.png
deleted file mode 100644
index edc72d4556cce0fde923a0ac3db4d0f94bb74412..0000000000000000000000000000000000000000
Binary files a/aleksis/core/static/icons/favicon_32.png and /dev/null differ
diff --git a/aleksis/core/static/icons/favicon_48.png b/aleksis/core/static/img/aleksis-favicon.png
similarity index 100%
rename from aleksis/core/static/icons/favicon_48.png
rename to aleksis/core/static/img/aleksis-favicon.png
diff --git a/aleksis/core/static/img/aleksis-icon-maskable.png b/aleksis/core/static/img/aleksis-icon-maskable.png
new file mode 100644
index 0000000000000000000000000000000000000000..6ec3bb2c04392e646c7d00c5ba3a316ebf99486e
Binary files /dev/null and b/aleksis/core/static/img/aleksis-icon-maskable.png differ
diff --git a/aleksis/core/static/img/aleksis-icon-maskable.svg b/aleksis/core/static/img/aleksis-icon-maskable.svg
new file mode 100644
index 0000000000000000000000000000000000000000..e37f3c3606d748039e4dc03cc4b0f27f5de491e1
--- /dev/null
+++ b/aleksis/core/static/img/aleksis-icon-maskable.svg
@@ -0,0 +1,134 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg
+   id="svg8"
+   width="256"
+   height="256"
+   version="1.1"
+   viewBox="0 0 67.73 67.73"
+   xmlns:xlink="http://www.w3.org/1999/xlink"
+   xmlns="http://www.w3.org/2000/svg"
+   xmlns:svg="http://www.w3.org/2000/svg"
+   xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+   xmlns:cc="http://creativecommons.org/ns#"
+   xmlns:dc="http://purl.org/dc/elements/1.1/">
+  <title
+     id="title32">AlekSIS icon</title>
+  <defs
+     id="defs2">
+    <linearGradient
+       id="shadow-gradient"
+       x1="-19.53"
+       x2="165.4"
+       y1="-19.53"
+       y2="165.4"
+       gradientUnits="userSpaceOnUse"
+       gradientTransform="matrix(0.2646,0,0,0.2646,109.82466,-4.9393086)">
+      <stop
+         id="stop9444"
+         offset="0" />
+      <stop
+         id="stop9446"
+         stop-opacity="0"
+         offset="1" />
+    </linearGradient>
+  </defs>
+  <metadata
+     id="metadata5">
+    <rdf:RDF>
+      <cc:Work
+         rdf:about="">
+        <dc:format>image/svg+xml</dc:format>
+        <dc:type
+           rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
+        <dc:title>AlekSIS icon</dc:title>
+        <cc:license
+           rdf:resource="http://creativecommons.org/licenses/by-sa/4.0/" />
+        <dc:date>2020-02-29</dc:date>
+        <dc:creator>
+          <cc:Agent>
+            <dc:title>Dominik George</dc:title>
+          </cc:Agent>
+        </dc:creator>
+        <dc:contributor>
+          <cc:Agent>
+            <dc:title>Julian Leucker</dc:title>
+          </cc:Agent>
+        </dc:contributor>
+      </cc:Work>
+      <cc:License
+         rdf:about="http://creativecommons.org/licenses/by-sa/4.0/">
+        <cc:permits
+           rdf:resource="http://creativecommons.org/ns#Reproduction" />
+        <cc:permits
+           rdf:resource="http://creativecommons.org/ns#Distribution" />
+        <cc:requires
+           rdf:resource="http://creativecommons.org/ns#Notice" />
+        <cc:requires
+           rdf:resource="http://creativecommons.org/ns#Attribution" />
+        <cc:permits
+           rdf:resource="http://creativecommons.org/ns#DerivativeWorks" />
+        <cc:requires
+           rdf:resource="http://creativecommons.org/ns#ShareAlike" />
+      </cc:License>
+    </rdf:RDF>
+  </metadata>
+  <g
+     id="background-with-glow">
+    <rect
+       id="background"
+       y="-3.357e-6"
+       width="67.73"
+       height="67.73"
+       rx="3.307"
+       ry="3.307"
+       fill="#0d5eaf"
+       stroke-width=".4104" />
+    <path
+       id="glow"
+       transform="scale(.2646)"
+       d="m9.959 0.2578c-5.698 1.168-9.959 6.189-9.959 12.24v52.31a234.1 86.8 0 0 0 188.9 35.71 234.1 86.8 0 0 0 67.09-3.715v-84.3c0-4.319-2.17-8.112-5.482-10.36-0.01554-0.01049-0.03129-0.02083-0.04688-0.03125-0.2971-0.1993-0.6034-0.3849-0.918-0.5586-0.05211-0.02867-0.1037-0.05799-0.1562-0.08594-0.2939-0.1568-0.5968-0.3001-0.9043-0.4336-0.05654-0.02442-0.111-0.05257-0.168-0.07617-0.3308-0.1378-0.6709-0.2577-1.016-0.3672-0.03502-0.01105-0.06836-0.02636-0.1035-0.03711-0.3806-0.1172-0.7687-0.2158-1.164-0.2969h-236.1z"
+       fill="#fff"
+       opacity=".2"
+       stroke-width="1.551" />
+  </g>
+  <g
+     id="favicon-bag"
+     transform="translate(-46.809258,-7.6499461)"
+     display="none">
+    <use
+       xlink:href="#schoolbag"
+       transform="matrix(2.25,0,0,2.25,-30.62,0)"
+       fill="#ffffff"
+       x="0"
+       y="0"
+       width="100%"
+       height="100%"
+       id="use18" />
+  </g>
+  <g
+     id="widgets-with-shadow"
+     transform="matrix(0.62859773,0,0,0.63035889,12.536993,12.511931)"
+     style="stroke-width:1.58861">
+    <g
+       id="widgets"
+       fill="#ffffff"
+       style="stroke-width:2.18867">
+      <path
+         id="schoolbag"
+         d="m 44.52,19.7 h 10.45 a 0.4353,0.4353 0 0 1 0.4353,0.4353 v 3.917 A 0.4353,0.4353 0 0 1 54.97,24.4876 H 44.52 a 0.4353,0.4353 0 0 1 -0.4353,-0.4353 v -3.917 A 0.4353,0.4353 0 0 1 44.52,19.7 Z M 55.4,18.394 v -0.8705 a 0.4353,0.4353 0 0 0 -0.4353,-0.4353 h -10.45 a 0.4353,0.4353 0 0 0 -0.4353,0.4353 v 0.8705 a 0.4353,0.4353 0 0 0 0.4353,0.4353 h 10.45 A 0.4353,0.4353 0 0 0 55.4,18.394 Z m -14.8,10.01 v 0.8705 a 2.176,2.176 0 0 0 2.176,2.176 h 13.93 a 2.176,2.176 0 0 0 2.176,-2.176 V 28.404 a 0.4353,0.4353 0 0 0 -0.4353,-0.4353 h -17.41 a 0.4353,0.4353 0 0 0 -0.4353,0.4353 z m -0.8705,-7.4 v -1.741 a 0.4353,0.4353 0 0 0 -0.4353,-0.4353 h -0.8705 a 1.306,1.306 0 0 0 -1.306,1.306 v 0.8706 a 0.4353,0.4353 0 0 0 0.4353,0.4353 h 1.741 a 0.4353,0.4353 0 0 0 0.4353,-0.4353 z m -2.612,1.741 v 4.788 a 1.306,1.306 0 0 0 1.306,1.306 h 0.8705 a 0.4353,0.4353 0 0 0 0.4353,-0.4353 v -5.659 A 0.4353,0.4353 0 0 0 39.294,22.3094 h -1.741 a 0.4353,0.4353 0 0 0 -0.4353,0.4353 z m 22.63,0 v 5.659 a 0.4353,0.4353 0 0 0 0.4353,0.4353 h 0.8705 a 1.306,1.306 0 0 0 1.306,-1.306 v -4.788 A 0.4353,0.4353 0 0 0 61.924,22.31 h -1.741 a 0.4353,0.4353 0 0 0 -0.4353,0.4353 z m 2.612,-1.741 v -0.8706 a 1.306,1.306 0 0 0 -1.306,-1.306 H 60.183 a 0.4353,0.4353 0 0 0 -0.4353,0.4353 v 1.741 a 0.4353,0.4353 0 0 0 0.4353,0.4353 h 1.741 a 0.4353,0.4353 0 0 0 0.4353,-0.4353 z m -3.482,-6.203 v 11.86 a 0.4353,0.4353 0 0 1 -0.4353,0.4353 h -17.41 A 0.4353,0.4353 0 0 1 40.5969,26.661 v -11.86 a 6.42,6.42 0 0 1 6.42,-6.42 h 0.3809 A 0.1632,0.1632 0 0 0 47.561,8.2178 V 7.5105 a 2.179,2.179 0 0 1 2.229,-2.176 c 1.188,0.0282 2.124,1.028 2.124,2.217 v 0.6664 a 0.1632,0.1632 0 0 0 0.1632,0.1632 h 0.3809 a 6.42,6.42 0 0 1 6.42,6.42 z m -10.45,-6.583 a 0.1632,0.1632 0 0 0 0.1632,0.1632 h 2.285 A 0.1632,0.1632 0 0 0 51.0389,8.218 V 7.5404 c 0,-0.7306 -0.5978,-1.348 -1.328,-1.336 a 1.307,1.307 0 0 0 -1.283,1.306 z m -0.4353,4.081 a 1.741,1.741 0 1 0 1.741,-1.741 1.741,1.741 0 0 0 -1.741,1.741 z m 8.27,5.223 a 1.307,1.307 0 0 0 -1.306,-1.306 h -10.45 a 1.307,1.307 0 0 0 -1.306,1.306 v 6.529 a 1.307,1.307 0 0 0 1.306,1.306 h 10.45 a 1.307,1.307 0 0 0 1.306,-1.306 z"
+         stroke-width="0.0864368" />
+      <path
+         id="puzzle-ll"
+         d="m 8.484,37.13 c -1.759,0 -3.175,1.416 -3.175,3.175 v 18.89 c 0,1.759 1.416,3.175 3.175,3.175 h 18.93 c 1.759,0 3.175,-1.416 3.175,-3.175 v -18.89 c 0,-1.759 -1.416,-3.175 -3.175,-3.175 h -5.842 v 2.798 h -0.01188 c 0.0052,0.0528 0.01281,0.1048 0.01447,0.1586 0,2.002 -1.623,3.625 -3.625,3.625 -2.002,0 -3.625,-1.623 -3.625,-3.625 0.01013,-0.0551 0.02494,-0.1053 0.03669,-0.1586 H 14.32614 V 37.13 Z m 28.69,12.62 c 3e-6,2.002 -1.623,3.625 -3.625,3.625 -4.515,-0.8302 -3.163,-7.154 0,-7.251 2.002,0 3.625,1.623 3.625,3.625 z m -6.582,-3.623 h 2.798 v 7.247 h -2.798 z"
+         style="stroke-width:1.58861" />
+      <path
+         id="puzzle-lr"
+         d="m 37.31,59.24 c 0,1.759 1.416,3.175 3.175,3.175 h 18.89 c 1.759,0 3.175,-1.416 3.175,-3.175 V 40.31 c 0,-1.759 -1.416,-3.175 -3.175,-3.175 h -18.89 c -1.759,0 -3.175,1.416 -3.175,3.175 v 5.842 h 2.798 v 0.0119 c 0.0528,-0.005 0.1048,-0.0128 0.1586,-0.0145 2.002,0 3.625,1.623 3.625,3.625 0,2.002 -1.623,3.625 -3.625,3.625 -0.0551,-0.0101 -0.1053,-0.0249 -0.1586,-0.0367 v 0.0351 H 37.31 Z"
+         style="stroke-width:1.58861" />
+      <path
+         id="puzzle-ul"
+         d="m 31.48,20.23 c 0.9818,-0.9818 0.9818,-2.563 0,-3.544 L 20.94,6.146 c -0.9818,-0.9819 -2.563,-0.9819 -3.545,0 l -10.57,10.57 c -0.9818,0.9818 -0.9818,2.563 6e-6,3.544 l 10.54,10.54 c 0.9818,0.9819 2.563,0.9819 3.545,0 l 3.261,-3.261 -1.562,-1.562 0.0064,-0.0069 c -0.03229,-0.02696 -0.06563,-0.05145 -0.09668,-0.08057 -1.118,-1.118 -1.118,-2.93 0,-4.048 1.118,-1.118 2.93,-1.118 4.048,0 0.02514,0.0363 0.04485,0.07241 0.0681,0.109 l 0.0196,-0.01948 1.562,1.562 z M 8.42,29.199 c -1.118,-1.118 -1.118,-2.93 -6.1e-6,-4.048 2.9840001,-2.057 5.7590001,2.228 4.0480001,4.048 -1.118,1.118 -2.9300001,1.118 -4.0480001,0 z m 5.697,-1.652 -1.562,1.562 -4.045,-4.045 1.562,-1.562 z"
+         style="stroke-width:1.58861" />
+    </g>
+  </g>
+</svg>
diff --git a/aleksis/core/static/img/hero.svg b/aleksis/core/static/img/hero.svg
new file mode 100644
index 0000000000000000000000000000000000000000..e11b51689d359a713f0bf5482a91c388a8632b60
--- /dev/null
+++ b/aleksis/core/static/img/hero.svg
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" height="1080px" width="1920px">
+  <defs>
+    <pattern id="doodad" width="92.38" height="80" viewBox="0 0 34.64101615137755 30" patternUnits="userSpaceOnUse" patternTransform="rotate(134)">
+      <path d="M-20-20h200v200h-200M33.77 25.5L25.98 21L18.19 25.5L18.19 34.5L25.98 39L33.77 34.5zM16.45 25.5L8.66 21L0.87 25.5L0.87 34.5L8.66 39L16.45 34.5zM7.79 10.5L0 6L-7.79 10.5L-7.79 19.5L0 24L7.79 19.5zM16.45-4.5L8.66-9L0.87-4.5L0.87 4.5L8.66 9L16.45 4.5zM33.77-4.5L25.98-9L18.19-4.5L18.19 4.5L25.98 9L33.77 4.5zM42.43 10.5L34.64 6L26.85 10.5L26.85 19.5L34.64 24L42.43 19.5zM25.11 10.5L17.32 6L9.53 10.5L9.53 19.5L17.32 24L25.11 19.5z" fill="#2222"/>
+      <path d="M-20-20h200v200h-200M24.21 25.25L15.98 20.5L7.75 25.25L7.75 34.75L15.98 39.5L24.21 34.75zM6.89 25.25L-1.34 20.5L-9.57 25.25L-9.57 34.75L-1.34 39.5L6.89 34.75zM-1.77 10.25L-10 5.5L-18.23 10.25L-18.23 19.75L-10 24.5L-1.77 19.75zM6.89-4.75L-1.34-9.5L-9.57-4.75L-9.57 4.75L-1.34 9.5L6.89 4.75zM24.21-4.75L15.98-9.5L7.75-4.75L7.75 4.75L15.98 9.5L24.21 4.75zM32.87 10.25L24.64 5.5L16.41 10.25L16.41 19.75L24.64 24.5L32.87 19.75zM41.53 25.25L33.3 20.5L25.07 25.25L25.07 34.75L33.3 39.5L41.53 34.75zM15.55 40.25L7.32 35.5L-0.91 40.25L-0.91 49.75L7.32 54.5L15.55 49.75zM-10.43 25.25L-18.66 20.5L-26.89 25.25L-26.89 34.75L-18.66 39.5L-10.43 34.75zM-10.43-4.75L-18.66-9.5L-26.89-4.75L-26.89 4.75L-18.66 9.5L-10.43 4.75zM15.55-19.75L7.32-24.5L-0.91-19.75L-0.91-10.25L7.32-5.5L15.55-10.25zM41.53-4.75L33.3-9.5L25.07-4.75L25.07 4.75L33.3 9.5L41.53 4.75zM32.87 40.25L24.64 35.5L16.41 40.25L16.41 49.75L24.64 54.5L32.87 49.75zM-1.77 40.25L-10 35.5L-18.23 40.25L-18.23 49.75L-10 54.5L-1.77 49.75zM-19.09 10.25L-27.32 5.5L-35.55 10.25L-35.55 19.75L-27.32 24.5L-19.09 19.75zM-1.77-19.75L-10-24.5L-18.23-19.75L-18.23-10.25L-10-5.5L-1.77-10.25zM32.87-19.75L24.64-24.5L16.41-19.75L16.41-10.25L24.64-5.5L32.87-10.25zM50.19 10.25L41.96 5.5L33.73 10.25L33.73 19.75L41.96 24.5L50.19 19.75zM15.55 10.25L7.32 5.5L-0.91 10.25L-0.91 19.75L7.32 24.5L15.55 19.75z" fill="#7777"/>
+    </pattern>
+  </defs>
+  <rect fill="url(#doodad)" height="200%" width="200%"/>
+</svg>
diff --git a/aleksis/core/static/js/main.js b/aleksis/core/static/js/main.js
index 1d2a2fe28e3c4304e72d8abfc398f0fda3908586..afc7b50fd530574753bc71f3c81cdd53c769e0a4 100644
--- a/aleksis/core/static/js/main.js
+++ b/aleksis/core/static/js/main.js
@@ -1,8 +1,59 @@
+// Define maps between Python's strftime and Luxon's and Materialize's proprietary formats
+const pythonToMomentJs = {
+    "%a": "EEE",
+    "%A": "EEEE",
+    "%w": "E",
+    "%d": "dd",
+    "%b": "MMM",
+    "%B": "MMMM",
+    "%m": "MM",
+    "%y": "yy",
+    "%Y": "yyyy",
+    "%H": "HH",
+    "%I": "hh",
+    "%p": "a",
+    "%M": "mm",
+    "%s": "ss",
+    "%f": "SSSSSS",
+    "%z": "ZZZ",
+    "%Z": "z",
+    "%U": "WW",
+    "%j": "ooo",
+    "%W": "WW",
+    "%u": "E",
+    "%G": "kkkk",
+    "%V": "WW",
+};
+
+const pythonToMaterialize = {
+    "%d": "dd",
+    "%a": "ddd",
+    "%A": "dddd",
+    "%m": "mm",
+    "%b": "mmm",
+    "%B": "mmmm",
+    "%y": "yy",
+    "%Y": "yyyy",
+}
+
+function buildDateFormat(formatString, map) {
+    // Convert a Python strftime format string to another format string
+    for (const key in map) {
+        formatString = formatString.replace(key, map[key]);
+    }
+    return formatString;
+}
+
 function initDatePicker(sel) {
     // Initialize datepicker [MAT]
-    const format = get_format('SHORT_DATE_FORMAT').toLowerCase().replace('d', 'dd').replace('m', 'mm').replace('y', 'yyyy');
+
+    // Get the date format from Django
+    const dateInputFormat = get_format('DATE_INPUT_FORMATS')[0]
+    const inputFormat = buildDateFormat(dateInputFormat, pythonToMomentJs);
+    const outputFormat = buildDateFormat(dateInputFormat, pythonToMaterialize);
+
     const el = $(sel).datepicker({
-        format: format,
+        format: outputFormat,
         // Pull translations from Django helpers
         i18n: {
             months: calendarweek_i18n.month_names,
@@ -19,9 +70,19 @@ function initDatePicker(sel) {
 
         // Set monday as first day of week
         firstDay: get_format('FIRST_DAY_OF_WEEK'),
-        autoClose: true
+        autoClose: true,
+        yearRange: [new Date().getFullYear() - 100, new Date().getFullYear() + 100],
+    });
+
+    // Set initial values of datepickers
+    $(sel).each(function () {
+        const currentValue = $(this).val();
+        if (currentValue) {
+            const currentDate = luxon.DateTime.fromFormat(currentValue, inputFormat).toJSDate();
+            $(this).datepicker('setDate', currentDate);
+        }
     });
-    el.datepicker("setDate", $(sel).val());
+
     return el;
 }
 
@@ -60,6 +121,10 @@ $(document).ready(function () {
 
     // Initialize dropdown [MAT]
     $('.dropdown-trigger').dropdown();
+    $('.navbar-dropdown-trigger').dropdown({
+        "coverTrigger": false,
+        "constrainWidth": false,
+    });
 
     // If JS is activated, the language form will be auto-submitted
     $('.language-field select').change(function () {
@@ -83,6 +148,9 @@ $(document).ready(function () {
     // Initialize Modals [MAT]
     $('.modal').modal();
 
+    // Initialize image boxes [Materialize]
+    $('.materialboxed').materialbox();
+
     // Intialize Tabs [Materialize]
     $('.tabs').tabs();
 
diff --git a/aleksis/core/static/js/progress.js b/aleksis/core/static/js/progress.js
index 9be718b7e67f30e8cc77a436755e50f08b540fd3..0b2509006029421273159617c9ff2646bb12800b 100644
--- a/aleksis/core/static/js/progress.js
+++ b/aleksis/core/static/js/progress.js
@@ -47,6 +47,7 @@ function customSuccess(progressBarElement, progressBarMessageElement) {
     $("#result-icon").text("check_circle");
     $("#result-text").text(OPTIONS.success);
     $("#result-box").show();
+    $("#result-button").show();
     const redirect = "redirect_on_success" in OPTIONS && OPTIONS.redirect_on_success;
     if (redirect) {
         window.location.replace(OPTIONS.redirect_on_success);
diff --git a/aleksis/core/static/public/style.scss b/aleksis/core/static/public/style.scss
index c2aa516e8cfcf88190aa13f43cc8f6aad5d747de..888dad2b0bdaa3b6832c41c039c21bc084954696 100644
--- a/aleksis/core/static/public/style.scss
+++ b/aleksis/core/static/public/style.scss
@@ -64,6 +64,10 @@ header, main, footer {
   margin-left: 300px;
 }
 
+.without-menu header, .without-menu main, .without-menu footer {
+  margin-left: 0;
+}
+
 @media only screen and (max-width: 992px) {
   header, main, footer {
     margin-left: 0;
@@ -73,6 +77,9 @@ header, main, footer {
 .materialize-circle {
   @extend .circle;
 }
+.collection .collection-item.avatar > .materialize-circle > .materialize-circle {
+  left: 0;
+}
 
 /**********/
 /* HEADER */
@@ -92,8 +99,11 @@ header, main, footer {
 /* MAIN */
 /********/
 
+$main-padding-lr: 20px;
+$main-padding-tb: 10px;
+
 main {
-  padding: 10px 20px;
+  padding: $main-padding-tb $main-padding-lr;
   flex: 1 0 auto;
 }
 
@@ -131,7 +141,7 @@ ul.sidenav li.logo > a:hover {
 }
 
 .sidenav-badge {
-  min-width: 2rem!important;
+  min-width: 2rem !important;
 }
 
 .sidenav li.search {
@@ -204,25 +214,6 @@ div#search-results {
 }
 
 
-// Sidenav trigger
-
-header a.sidenav-trigger {
-  position: absolute;
-  left: 7.5%;
-  top: 0;
-
-  height: 64px;
-  font-size: 38px;
-
-  float: none;
-
-  text-align: center;
-  color: white;
-
-  z-index: 2;
-}
-
-
 // Footer
 
 .footer-icon {
@@ -290,6 +281,11 @@ h2 {
   font-size: 3.0rem;
 }
 
+p, h1, h2, h3, h4, h5, h6, .card-title {
+  overflow-wrap: break-word;
+  hyphens: auto;
+}
+
 /* Collections */
 
 ul.collection .collection-item .title {
@@ -377,6 +373,12 @@ span.badge .material-icons {
   margin-left: -2px;
 }
 
+.chip {
+  padding: 8px 12px;
+  height: auto;
+  line-height: 16px;
+}
+
 /*+++++++++*/
 /* Buttons */
 /*+++++++++*/
@@ -412,6 +414,10 @@ span.badge .material-icons {
 
 /* Table*/
 
+.table-container {
+  overflow-x: auto;
+}
+
 table.striped > tbody > tr:nth-child(odd), table tr.striped, table tbody.striped tr {
   background-color: rgba(208, 208, 208, 0.5);
 }
@@ -536,11 +542,11 @@ th.orderable.desc > a {
   font-weight: 700;
 }
 
-figure.alert > :not(.material-icons.left){
+figure.alert > :not(.material-icons.left) {
   margin-left: 39px;
 }
 
-.alert figcaption  {
+.alert figcaption {
   font-weight: 700;
   font-size: 17px;
   margin: auto;
@@ -548,7 +554,7 @@ figure.alert > :not(.material-icons.left){
 
 div.alert > p,
 div.alert > div,
-figure.alert{
+figure.alert {
   margin: 10px;
   padding: 10px;
   border-left: 5px solid;
@@ -556,7 +562,7 @@ figure.alert{
 
 div.alert.success > p,
 div.alert.success > div,
-figure.alert.success{
+figure.alert.success {
   @extend .success;
   border-color: color("green", "base");
 }
@@ -595,23 +601,23 @@ main figure.alert {
 // Text collapsible
 
 .text-collapsible > .material-icons {
-    font-size: 20px;
-    line-height: 15px;
-    margin-bottom: 2px;
-    margin-left: -1px;
-    vertical-align: middle;
-    cursor: pointer;
-    color: #6d6d6d;
+  font-size: 20px;
+  line-height: 15px;
+  margin-bottom: 2px;
+  margin-left: -1px;
+  vertical-align: middle;
+  cursor: pointer;
+  color: #6d6d6d;
 }
 
 .text-collapsible.opened .a,
 .text-collapsible.opened > .material-icons.open-icon {
-    display: none;
+  display: none;
 }
 
 .text-collapsible.closed .b,
 .text-collapsible.closed > .material-icons.close-icon {
-    display: none;
+  display: none;
 }
 
 // Helpers
@@ -755,10 +761,10 @@ main figure.alert {
 }
 
 .card .card-action-light {
-	background-color: inherit;
-	border-top: 1px solid rgba(160, 160, 160, 0.2);
-	position: relative;
-	padding: 16px 24px;
+  background-color: inherit;
+  border-top: 1px solid rgba(160, 160, 160, 0.2);
+  position: relative;
+  padding: 16px 24px;
 }
 
 .margin-top {
@@ -768,3 +774,219 @@ main figure.alert {
 .margin-bottom {
   margin-bottom: 0.7rem !important;
 }
+
+/* Tabs with icons */
+.tabs-icons, .tabs-icons .tab, .tabs-icons a {
+  height: 72px;
+}
+
+.tabs-icons .tab {
+  display: inline-flex;
+  flex-direction: column;
+
+}
+
+.tabs-icons .tab a {
+  padding-top: 12px;
+  line-height: 14px;
+  font-size: 12px;
+  font-weight: 500;
+  letter-spacing: 0.08em;
+}
+
+.tabs-icons .tab i {
+  font-size: 24px;
+  line-height: 1;
+  height: 24px;
+  margin-bottom: 8px;
+}
+
+/* Person overview */
+$person-logo-size: 20vh;
+
+.clip-circle:not(.active) {
+  width: $person-logo-size;
+  height: $person-logo-size;
+  background: #f9f9f9;
+  border-radius: 50%;
+
+  & img {
+    border-radius: 50%;
+    width: 100%;
+    height: 100%;
+    object-fit: cover;
+  }
+}
+
+.clip-circle.no-image, .clip-circle.no-image > i.material-icons {
+  font-size: calc(#{$person-logo-size} * 0.5);
+  color: #6f6f6f;
+  background: #f2f2f2;
+  line-height: $person-logo-size;
+  width: $person-logo-size;
+  text-align: center;
+  user-select: none;
+  cursor: default;
+  border-radius: 50%;
+  height: unset;
+}
+
+.nav-wrapper {
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+  padding: 0 1rem;
+  > a {
+    position: static!important;
+    transform: none!important;
+  }
+  & .nav-spacer {
+    width: 60px;
+  }
+  & ul.account-nav {
+    display: flex;
+    margin-inline: 7.5px;
+    & > li > a {
+      padding: 0 7.5px;
+    }
+  }
+}
+
+.nav-wrapper .navbar-dropdown-trigger {
+  cursor: pointer;
+  height: 100%;
+  display: grid;
+}
+
+.navbar-dropdown-trigger .clip-circle {
+  margin: auto;
+  width: $navbar-height*0.75;
+  height: $navbar-height*0.75;
+  cursor: pointer;
+
+  &.no-image, &.no-image > i.material-icons {
+    font-size: calc(#{$navbar-height} * 0.75 * 0.5);
+    color: #6f6f6f;
+    background: #f2f2f2;
+    line-height: $navbar-height*0.75;
+    width: $navbar-height*0.75;
+    cursor: pointer;
+  }
+}
+
+i.material-icons.new-notification {
+  position: relative;
+  &:after {
+    content: "";
+    position: absolute;
+    width: 12px;
+    height: 12px;
+    bottom: 27%;
+    right: -4%;
+    background-color: $secondary-color;
+    border-radius: 50%;
+  }
+}
+
+#hero-bg {
+  position: absolute;
+  width: calc(100% + #{$main-padding-lr});
+  height: 30vh;
+  left: -$main-padding-lr;
+  top: 0;
+  overflow: hidden;
+  background-color: lighten($primary-color, 30%);
+  z-index: -1;
+  box-shadow: 0 4px 5px 0 rgba(0, 0, 0, 0.14) inset,
+  0 1px 10px 0 rgba(0, 0, 0, 0.12) inset,
+  0 2px 4px -1px rgba(0, 0, 0, 0.3) inset;
+}
+
+.person-buttons {
+  display: flex;
+  flex-direction: column;
+  gap: 6px;
+  width: max-content;
+  position: absolute;
+  right: $main-padding-lr;
+  margin: 0;
+  align-items: end;
+
+  & a {
+    -webkit-transition: width 0.5s 0s ease;
+    -moz-transition: width 0.5s 0s ease;
+    -o-transition: width 0.5s 0s ease;
+    transition: width 0.5s 0s ease;
+
+    &:hover {
+      width: max-content;
+    }
+  }
+}
+
+@media (pointer: fine) {
+  .person-buttons a {
+    width: 50px;
+  }
+}
+
+.person-container {
+  margin: calc(30vh - #{$main-padding-tb} - #{$navbar-height} - 10vh) 0 0 0;
+  display: flex;
+  align-items: center;
+  flex-wrap: wrap;
+  justify-content: space-around;
+  flex-direction: column;
+}
+
+.person-collection .collection-item.avatar {
+  display: flex;
+  align-items: center;
+
+  & img.circle {
+    object-fit: cover;
+  }
+}
+
+.materialboxed {
+  &:not(.active) {
+    opacity: 100% !important;
+
+    & > img:hover {
+      opacity: 80%;
+    }
+  }
+
+  &.active {
+    box-shadow: none;
+
+    & > img {
+      width: 100%;
+      height: 100%;
+      object-fit: contain;
+    }
+  }
+}
+
+.application-circle {
+  border-radius: 50%;
+  width: 20vh;
+  height: 20vh;
+}
+
+
+.application-circle img {
+  @extend .application-circle;
+  object-fit: cover;
+}
+
+// Login Page
+.login-card-action {
+  display: grid;
+  grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
+  row-gap: 0.7rem;
+  & *:last-child {
+    grid-column: -2;
+    text-align: center;
+  }
+}
diff --git a/aleksis/core/tables.py b/aleksis/core/tables.py
index 54d3f3e5c75e4c5ac0dca4b8ccfe868f70b7efb2..f08460bd8654907cb56f655e01014342f8e59ab5 100644
--- a/aleksis/core/tables.py
+++ b/aleksis/core/tables.py
@@ -1,14 +1,19 @@
+from textwrap import wrap
+
 from django.utils.translation import gettext_lazy as _
 
 import django_tables2 as tables
 from django_tables2.utils import A
 
+from .models import Person
+from .util.core_helpers import get_site_preferences
+
 
 class SchoolTermTable(tables.Table):
     """Table to list persons."""
 
     class Meta:
-        attrs = {"class": "responsive-table highlight"}
+        attrs = {"class": "highlight"}
 
     name = tables.LinkColumn("edit_school_term", args=[A("id")])
     date_start = tables.Column()
@@ -26,7 +31,7 @@ class PersonsTable(tables.Table):
     """Table to list persons."""
 
     class Meta:
-        attrs = {"class": "responsive-table highlight"}
+        attrs = {"class": "highlight"}
 
     first_name = tables.LinkColumn("person_by_id", args=[A("id")])
     last_name = tables.LinkColumn("person_by_id", args=[A("id")])
@@ -36,7 +41,7 @@ class GroupsTable(tables.Table):
     """Table to list groups."""
 
     class Meta:
-        attrs = {"class": "responsive-table highlight"}
+        attrs = {"class": "highlight"}
 
     name = tables.LinkColumn("group_by_id", args=[A("id")])
     short_name = tables.LinkColumn("group_by_id", args=[A("id")])
@@ -47,7 +52,7 @@ class AdditionalFieldsTable(tables.Table):
     """Table to list group types."""
 
     class Meta:
-        attrs = {"class": "responsive-table hightlight"}
+        attrs = {"class": "hightlight"}
 
     title = tables.LinkColumn("edit_additional_field_by_id", args=[A("id")])
     delete = tables.LinkColumn(
@@ -63,7 +68,7 @@ class GroupTypesTable(tables.Table):
     """Table to list group types."""
 
     class Meta:
-        attrs = {"class": "responsive-table highlight"}
+        attrs = {"class": "highlight"}
 
     name = tables.LinkColumn("edit_group_type_by_id", args=[A("id")])
     description = tables.LinkColumn("edit_group_type_by_id", args=[A("id")])
@@ -76,7 +81,7 @@ class DashboardWidgetTable(tables.Table):
     """Table to list dashboard widgets."""
 
     class Meta:
-        attrs = {"class": "responsive-table highlight"}
+        attrs = {"class": "highlight"}
 
     widget_name = tables.Column(accessor="pk")
     title = tables.LinkColumn("edit_dashboard_widget", args=[A("id")])
@@ -91,3 +96,94 @@ class DashboardWidgetTable(tables.Table):
 
     def render_widget_name(self, value, record):
         return record._meta.verbose_name
+
+
+class PersonColumn(tables.Column):
+    """Returns person object from given id."""
+
+    def render(self, value):
+        return Person.objects.get(user__id=value)
+
+
+class InvitationCodeColumn(tables.Column):
+    """Returns invitation code in a more readable format."""
+
+    def render(self, value):
+        packet_size = get_site_preferences()["auth__invite_code_packet_size"]
+        return "-".join(wrap(value, packet_size))
+
+
+class InvitationsTable(tables.Table):
+    """Table to list persons."""
+
+    person_id = PersonColumn()
+    email = tables.EmailColumn()
+    sent = tables.DateColumn()
+    inviter_id = PersonColumn()
+    key = InvitationCodeColumn()
+    accepted = tables.BooleanColumn(
+        yesno="check,cancel", attrs={"span": {"class": "material-icons"}}
+    )
+
+
+class PermissionDeleteColumn(tables.LinkColumn):
+    """Link column with label 'Delete'."""
+
+    def __init__(self, url, **kwargs):
+        super().__init__(
+            url,
+            args=[A("pk")],
+            text=_("Delete"),
+            attrs={"a": {"class": "btn-flat waves-effect waves-red red-text"}},
+            verbose_name=_("Actions"),
+            **kwargs
+        )
+
+
+class PermissionTable(tables.Table):
+    """Table to list permissions."""
+
+    class Meta:
+        attrs = {"class": "responsive-table highlight"}
+
+    permission = tables.Column()
+
+
+class ObjectPermissionTable(PermissionTable):
+    """Table to list object permissions."""
+
+    content_object = tables.Column()
+
+
+class GlobalPermissionTable(PermissionTable):
+    """Table to list global permissions."""
+
+    pass
+
+
+class GroupObjectPermissionTable(ObjectPermissionTable):
+    """Table to list assigned group object permissions."""
+
+    group = tables.Column()
+    delete = PermissionDeleteColumn("delete_group_object_permission")
+
+
+class UserObjectPermissionTable(ObjectPermissionTable):
+    """Table to list assigned user object permissions."""
+
+    user = tables.Column()
+    delete = PermissionDeleteColumn("delete_user_object_permission")
+
+
+class GroupGlobalPermissionTable(GlobalPermissionTable):
+    """Table to list assigned global user permissions."""
+
+    group = tables.Column()
+    delete = PermissionDeleteColumn("delete_group_global_permission")
+
+
+class UserGlobalPermissionTable(GlobalPermissionTable):
+    """Table to list assigned global group permissions."""
+
+    user = tables.Column()
+    delete = PermissionDeleteColumn("delete_user_global_permission")
diff --git a/aleksis/core/tasks.py b/aleksis/core/tasks.py
index 731c4356c20bba915bd5ad0811307c3877810631..7b7e529b3a1fa21c5a6620170296d9ee1eb777ec 100644
--- a/aleksis/core/tasks.py
+++ b/aleksis/core/tasks.py
@@ -1,7 +1,10 @@
+from datetime import timedelta
+
 from django.conf import settings
 from django.core import management
 
 from .celery import app
+from .util.notifications import _send_due_notifications as _send_due_notifications
 from .util.notifications import send_notification as _send_notification
 
 
@@ -15,7 +18,7 @@ def send_notification(notification: int, resend: bool = False) -> None:
     _send_notification(notification, resend)
 
 
-@app.task
+@app.task(run_every=timedelta(days=1))
 def backup_data() -> None:
     """Backup database and media using django-dbbackup."""
     # Assemble command-line options for dbbackup management command
@@ -38,3 +41,17 @@ def backup_data() -> None:
     # Hand off to dbbackup's management commands
     management.call_command("dbbackup", *db_options)
     management.call_command("mediabackup", *media_options)
+
+
+@app.task(run_every=timedelta(days=1))
+def clear_oauth_tokens():
+    """Clear expired OAuth2 tokens."""
+    from oauth2_provider.models import clear_expired  # noqa
+
+    return clear_expired()
+
+
+@app.task(run_every=timedelta(minutes=5))
+def send_notifications():
+    """Send due notifications to users."""
+    _send_due_notifications()
diff --git a/aleksis/core/templates/404.html b/aleksis/core/templates/404.html
index 33c311fcaf4c106d44c53788b44426584d014c33..cf68c12bf63624ecf40107ee6f3994b288548c3c 100644
--- a/aleksis/core/templates/404.html
+++ b/aleksis/core/templates/404.html
@@ -7,8 +7,7 @@
     <div class="card red">
       <div class="card-content white-text">
         <i class="material-icons small left">error_outline</i>
-        <span class="card-title">{% trans "Error" %} (404): {% blocktrans %}The requested page or object was not
-          found.{% endblocktrans %}</span>
+        <span class="card-title">{{ exception }}</span>
         <p>
           {% blocktrans %}
             If you were redirected by a link on an external page,
diff --git a/aleksis/core/templates/500.html b/aleksis/core/templates/500.html
index 2abf1445494da40885904252ecbd0aa5bffed077..bf56e08db5b6fd6d6052585ce216bd50cbea3796 100644
--- a/aleksis/core/templates/500.html
+++ b/aleksis/core/templates/500.html
@@ -16,6 +16,10 @@
           {% endblocktrans %}
         </p>
         {% include "core/partials/admins_list.html" %}
+        <a href="javascript:window.location.reload()" class="btn green waves-effect waves-light">
+          <i class="material-icons left">refresh</i>
+          {% trans "Retry" %}
+        </a>
       </div>
     </div>
   </div>
diff --git a/aleksis/core/templates/account/account_inactive.html b/aleksis/core/templates/account/account_inactive.html
index 62b03a3db859699aabb816ef1d05455777965bd9..5c328abcfbca48c6499f71b4b11fac5f1f91ccad 100644
--- a/aleksis/core/templates/account/account_inactive.html
+++ b/aleksis/core/templates/account/account_inactive.html
@@ -9,8 +9,10 @@
   <div class="container">
     <div class="card red">
       <div class="card-content white-text">
-        <div class="material-icons small left">error_outline</div>
-        <span class="card-title">{% blocktrans %}Account inactive.{% endblocktrans %}</span>
+        <div class="card-title">
+          <i class="material-icons small left">error_outline</i>
+          {% blocktrans %}Account inactive.{% endblocktrans %}
+        </div>
         <p>
           {% blocktrans %}
             This account is currently inactive. If you think this is an
diff --git a/aleksis/core/templates/account/signup.html b/aleksis/core/templates/account/signup.html
index b4a74179908dafc208d34c5b2c472f3bd73c3de4..8885e3902af13c609a69a595df3d125b66dc79ce 100644
--- a/aleksis/core/templates/account/signup.html
+++ b/aleksis/core/templates/account/signup.html
@@ -16,7 +16,9 @@
   <form method="post" action="{% url 'account_signup' %}">
     {% csrf_token %}
     {% form form=form %}{% endform %}
-    <input type="hidden" name="{{ redirect_field_name }}" value="{{ redirect_field_value }}" />
+    {% if redirect_field_value %}
+      <input type="hidden" name="{{ redirect_field_name }}" value="{{ redirect_field_value }}" />
+    {% endif %}
     {% trans "Sign up" as caption %}
     {% include "core/partials/save_button.html" with caption=caption icon="how_to_reg" %}
   </form>
diff --git a/aleksis/core/templates/account/signup_closed.html b/aleksis/core/templates/account/signup_closed.html
index 33ef749f5598fbd209e3f72dc2b4a79620f17b1a..0d5cbd51095e9c24fceacbc4ba8f6a510a5705a9 100644
--- a/aleksis/core/templates/account/signup_closed.html
+++ b/aleksis/core/templates/account/signup_closed.html
@@ -9,8 +9,10 @@
   <div class="container">
     <div class="card red">
       <div class="card-content white-text">
-        <div class="material-icons small left">error_outline</div>
-        <span class="card-title">{% blocktrans %}Signup closed.{% endblocktrans %}</span>
+        <div class="card-title">
+        <i class="material-icons small left">error_outline</i>
+          {% blocktrans %}Signup closed.{% endblocktrans %}
+        </div>
         <p>
           {% blocktrans %}
             This sign up is currently closed. If you think this is an
diff --git a/aleksis/core/templates/account/verification_sent.html b/aleksis/core/templates/account/verification_sent.html
index dd486aeb2bf212838a014fe868db4cfe6716cf8d..df3ae47c82911a01997ac9f4d7919c9e1b95a36e 100644
--- a/aleksis/core/templates/account/verification_sent.html
+++ b/aleksis/core/templates/account/verification_sent.html
@@ -25,10 +25,6 @@
             contact us if you do not receive it within a few minutes.
           {% endblocktrans %}
         </p>
-        <p>
-          {% url 'account_email' as email_url %}
-          {% blocktrans with email_url=email_url %}<strong>Note:</strong> you can still <a href="{{ email_url }}">change your e-mail address</a>{% endblocktrans %}
-        </p>
         {% include "core/partials/admins_list.html" %}
       </div>
     </div>
diff --git a/aleksis/core/templates/core/base.html b/aleksis/core/templates/core/base.html
index 0dd75f2c6f237dea1ecac47b010abfac9453000f..d3a1ada6436769ab7a449b27051823ee49897c44 100644
--- a/aleksis/core/templates/core/base.html
+++ b/aleksis/core/templates/core/base.html
@@ -58,59 +58,93 @@
 
   {% block extra_head %}{% endblock %}
 </head>
-<body>
+<body {% if no_menu %}class="without-menu"{% endif %}>
 
 <header>
-  <!-- Menu button (sidenav) -->
-  <div class="container">
-    <a href="#" data-target="slide-out" class="top-nav sidenav-trigger hide-on-large-only">
-      <i class="material-icons">menu</i>
-    </a>
-  </div>
-
   <!-- Nav bar (logged in as, logout) -->
-  <nav>
+  <nav class="nav-extended">
     <div class="nav-wrapper">
+      <a href="#" data-target="slide-out" class="top-nav sidenav-trigger hide-on-large-only">
+        <i class="material-icons">menu</i>
+      </a>
+
       <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>
+      {% if user.is_authenticated %}
+        <ul class="account-nav">
+          {% trans "Notifications" as notifications_text %}
           <li>
-            <a href="{% url 'logout' %}">{% trans "Logout" %} <i class="material-icons right">exit_to_app</i></a>
+            <a href="{% url "notifications" %}" class="tooltipped" data-position="bottom"
+               data-tooltip="{{ notifications_text }}" aria-label="{{ notifications_text }}">
+              <i class="material-icons {% if request.user.person.unread_notifications_count > 0 %}new-notification{% endif %}">
+                notifications
+              </i>
+            </a>
           </li>
-        {% endif %}
-      </ul>
+          <li>
+            <a href="#!" class="navbar-dropdown-trigger" data-target="account-dropdown">
+              {{ request.user.person.identicon }}
+              {% include "core/partials/avatar_content.html" with person_or_user=request.user.person %}
+            </a>
+          </li>
+        </ul>
+      {% else %}
+        <span class="nav-spacer"></span>
+      {% endif %}
+    </div>
+    <div class="nav-content">
+      {% block nav_content %}{% endblock %}
     </div>
   </nav>
 
-  <!-- Main nav (sidenav) -->
-  <ul id="slide-out" class="sidenav sidenav-fixed">
-    <li class="logo">
-      {% static "img/aleksis-banner.svg" as aleksis_banner %}
-      <a id="logo-container" href="/" class="brand-logo">
-        <img src="{% firstof request.site.preferences.theme__logo.url aleksis_banner %}"
-             alt="{{ request.site.preferences.general__title }} – Logo">
-      </a>
-    </li>
-    {% has_perm 'core.search_rule' user as search %}
-    {% if search %}
-      <li class="search">
-        <form method="get" action="{% url "haystack_search" %}" id="search-form" class="autocomplete">
-          <div class="search-wrapper">
-            <input id="search" name="q" type="search" enterkeyhint="search" placeholder="{% trans "Search" %}">
-            <button class="btn btn-flat search-button" type="submit" aria-label="{% trans "Search" %}">
-              <i class="material-icons">search</i>
-            </button>
-            <div class="progress" id="search-loader"><div class="indeterminate"></div></div>
-          </div>
-        </form>
+  {% get_menu "NAVBAR_ACCOUNT_MENU" as account_menu %}
+  <ul id="account-dropdown" class="dropdown-content">
+    {% for item in account_menu %}
+      {% if item.divider %}
+        <li class="divider"></li>
+      {% endif %}
+      <li>
+        <a href="{{ item.url }}">
+          {% if item.icon %}
+            <i class="material-icons">{{ item.icon }}</i>
+          {% endif %}
+          {{ item.name }}
+        </a>
       </li>
-    {% endif %}
-    <li class="no-padding">
-      {% include "core/partials/sidenav.html" %}
-    </li>
+    {% endfor %}
   </ul>
+
+  <!-- Main nav (sidenav) -->
+  {% if not no_menu %}
+    <ul id="slide-out" class="sidenav sidenav-fixed">
+      <li class="logo">
+        {% static "img/aleksis-banner.svg" as aleksis_banner %}
+        <a id="logo-container" href="/" class="brand-logo">
+          <img src="{% firstof request.site.preferences.theme__logo.url aleksis_banner %}"
+               alt="{{ request.site.preferences.general__title }} – Logo">
+        </a>
+      </li>
+      {% has_perm 'core.search_rule' user as search %}
+      {% if search %}
+        <li class="search">
+          <form method="get" action="{% url "haystack_search" %}" id="search-form" class="autocomplete">
+            <div class="search-wrapper">
+              <input id="search" name="q" type="search" enterkeyhint="search" placeholder="{% trans "Search" %}">
+              <button class="btn btn-flat search-button" type="submit" aria-label="{% trans "Search" %}">
+                <i class="material-icons">search</i>
+              </button>
+              <div class="progress" id="search-loader">
+                <div class="indeterminate"></div>
+              </div>
+            </div>
+          </form>
+        </li>
+      {% endif %}
+      <li class="no-padding">
+        {% include "core/partials/sidenav.html" %}
+      </li>
+    </ul>
+  {% endif %}
 </header>
 
 
@@ -169,7 +203,7 @@
     <div class="container">
       <div class="left">
         <a class="blue-text text-lighten-4" href="{% url "about_aleksis" %}">
-          {% trans "About AlekSIS — The Free School Information System" %}
+          {% trans "About AlekSIS® — The Free School Information System" %}
         </a>
         © The AlekSIS Team
       </div>
@@ -177,7 +211,7 @@
         <span id="doit"></span>
         {% if request.site.preferences.footer__imprint_url %}
           <a class="blue-text text-lighten-4" href="{{ request.site.preferences.footer__imprint_url }}">
-            {% trans "Impress" %}
+            {% trans "Imprint" %}
           </a>
         {% endif %}
         {% if request.site.preferences.footer__privacy_url and request.site.preferences.footer__imprint_url %}
@@ -194,6 +228,7 @@
 </footer>
 
 
+{% include_js "luxon" %}
 {% include_js "materialize" %}
 {% include_js "sortablejs" %}
 {% include_js "jquery-sortablejs" %}
diff --git a/aleksis/core/templates/core/base_print.html b/aleksis/core/templates/core/base_print.html
index 428d28f2858406c771b635c20ff874bf5c9f9d5a..ed2a0c200c3798311db66980f3391b52ed67600e 100644
--- a/aleksis/core/templates/core/base_print.html
+++ b/aleksis/core/templates/core/base_print.html
@@ -5,13 +5,15 @@
 <!DOCTYPE html>
 <html lang="{{ LANGUAGE_CODE }}">
 <head>
+  <base href="{{ BASE_URL }}" />
+
   {% include "core/partials/meta.html" %}
 
   <title>
     {% block no_browser_title %}
       {% block browser_title %}{% endblock %} —
     {% endblock %}
-    {{ config.SITE_TITLE }}
+    {{ SITE_PREFERENCES.general__title }}
   </title>
 
   {% include_css "material-design-icons" %}
@@ -51,7 +53,7 @@
             <div id="print-header" class="row">
               <div class="col s6 logo">
                 {% static "img/aleksis-banner.svg" as aleksis_banner %}
-                <img src="{% firstof request.site.preferences.theme__logo.url aleksis_banner %}" alt="Logo"
+                <img src="{% firstof SITE_PREFERENCES.theme__logo.url aleksis_banner %}" alt="Logo"
                      id="print-logo"/>
               </div>
               <div class="col s6 right-align">
@@ -65,11 +67,11 @@
 
           <footer>
             <div class="left">
-              {{ request.site.preferences.school__name }}
+              {{ SITE_PREFERENCES.school__name }}
             </div>
 
             <div class="right">
-              {% trans "Powered by AlekSIS" %}
+              {% trans "Powered by AlekSIS®" %}
             </div>
           </footer>
         </div>
diff --git a/aleksis/core/templates/core/dashboard_widget/static_content_widget.html b/aleksis/core/templates/core/dashboard_widget/static_content_widget.html
new file mode 100644
index 0000000000000000000000000000000000000000..96e5549d5890e4cc95dbe6b4a843f1b1612c5d7f
--- /dev/null
+++ b/aleksis/core/templates/core/dashboard_widget/static_content_widget.html
@@ -0,0 +1,10 @@
+{% load html_helpers %}
+
+<div class="card" style="padding: 2em">
+	<div class="card-title">
+		{{ title }}
+	</div>
+        <div class="card-text">
+		{{ content|add_class_to_el:"ul, browser-default"|safe }}
+        </div>
+</div>
diff --git a/aleksis/core/templates/core/data_check/list.html b/aleksis/core/templates/core/data_check/list.html
index 5a510fdd92f550ad1b7ff6b0b81983ea225420ba..36d83d4cd7d3c917db98c104586bd2aa9c82c139 100644
--- a/aleksis/core/templates/core/data_check/list.html
+++ b/aleksis/core/templates/core/data_check/list.html
@@ -50,27 +50,29 @@
           </thead>
           <tbody>
           {% for result in results %}
-            <tr>
-              <td>
-                <code>{{ result.id }}</code>
-              </td>
-              <td>{% verbose_name_object result.related_object %}</td>
-              <td>{{ result.related_object }}</td>
-              <td>{{ result.related_check.problem_name }}</td>
-              <td>
-                <a class="btn-flat waves-effect waves-light" href="{{ result.related_object.get_absolute_url }}">
-                  {% trans "Show object" %}
-                </a>
-              </td>
-              <td>
-                {% for option_name, option in result.related_check.solve_options.items %}
-                  <a class="btn btn-margin waves-effect waves-light"
-                     href="{% url "data_check_solve" result.pk option_name %}">
-                    {{ option.verbose_name }}
+            {% if result.related_object %}
+              <tr>
+                <td>
+                  <code>{{ result.id }}</code>
+                </td>
+                <td>{% verbose_name_object result.related_object %}</td>
+                <td>{{ result.related_object }}</td>
+                <td>{{ result.related_check.problem_name }}</td>
+                <td>
+                  <a class="btn-flat waves-effect waves-light" href="{{ result.related_object.get_absolute_url }}">
+                    {% trans "Show object" %}
                   </a>
-                {% endfor %}
-              </td>
-            </tr>
+                </td>
+                <td>
+                  {% for option_name, option in result.related_check.solve_options.items %}
+                    <a class="btn btn-margin waves-effect waves-light"
+                       href="{% url "data_check_solve" result.pk option_name %}">
+                      {{ option.verbose_name }}
+                    </a>
+                  {% endfor %}
+                </td>
+              </tr>
+            {% endif %}
           {% endfor %}
           </tbody>
         </table>
diff --git a/aleksis/core/templates/core/index.html b/aleksis/core/templates/core/index.html
index 4de8ddcf35ef9153d094798c5cc019aa1bb0f863..7c9c018ca8102172dbd6e46c41048bcbcff4d46f 100644
--- a/aleksis/core/templates/core/index.html
+++ b/aleksis/core/templates/core/index.html
@@ -10,7 +10,7 @@
 
 {% block content %}
   {% has_perm "core.edit_dashboard_rule" user as can_edit_dashboard %}
-  {% if can_edit_dashboard %}
+  {% if can_edit_dashboard and show_edit_dashboard_button %}
     <div class="row no-margin">
       <a class="btn-flat waves-effect waves-light right" href="{% url "edit_dashboard" %}">
         <i class="material-icons left">edit</i>
@@ -85,7 +85,7 @@
           <ul class="collection">
             {% for notification in notifications %}
               <li class="collection-item">
-                <span class="badge new primary-color">{{ notification.app }}</span>
+                <span class="badge new primary-color">{{ notification.sender }}</span>
                 <span class="title">{{ notification.title }}</span>
                 <p>
                   <i class="material-icons left">access_time</i> {{ notification.created }}
@@ -108,7 +108,7 @@
     </div>
   {% endif %}
 
-  {% if user.person.preferences.general__automatically_update_dashboard and request.site.preferences.general__automatically_update_dashboard_site %}
+  {% if user.person.preferences.general__automatically_update_dashboard and SITE_PREFERENCES.general__automatically_update_dashboard_site %}
     <script type="text/javascript" src="{% static "js/include_ajax_live.js" %}"></script>
   {% endif %}
 {% endblock %}
diff --git a/aleksis/core/templates/core/pages/about.html b/aleksis/core/templates/core/pages/about.html
index 787f18f535967058479861c3547fe69098cc2202..6c44d90cbfe51df457f2bd09f600dfb676b7b97d 100644
--- a/aleksis/core/templates/core/pages/about.html
+++ b/aleksis/core/templates/core/pages/about.html
@@ -3,8 +3,8 @@
 {% load i18n %}
 
 
-{% block browser_title %}{% blocktrans %}About AlekSIS{% endblocktrans %}{% endblock %}
-{% block page_title %}{% blocktrans %}AlekSIS – The Free School Information System{% endblocktrans %}{% endblock %}
+{% block browser_title %}{% blocktrans %}About AlekSIS®{% endblocktrans %}{% endblock %}
+{% block page_title %}{% blocktrans %}AlekSIS® – The Free School Information System{% endblocktrans %}{% endblock %}
 
 {% block content %}
 
@@ -15,11 +15,16 @@
           <span class="card-title">{% blocktrans %}About AlekSIS{% endblocktrans %}</span>
           <p>
             {% blocktrans %}
-              This platform is powered by AlekSIS, a web-based school information system (SIS) which can be used
+              This platform is powered by AlekSIS®, a web-based school information system (SIS) which can be used
               to manage and/or publish organisational artifacts of educational institutions. AlekSIS is free software and
               can be used by anyone.
             {% endblocktrans %}
           </p>
+          <p>
+            {% blocktrans %}
+              AlekSIS® is a registered trademark of the AlekSIS open source project, represented by Teckids e.V.
+            {% endblocktrans %}
+          </p>
         </div>
         <div class="card-action">
           <a class="" href="https://aleksis.org/">{% trans "Website of AlekSIS" %}</a>
diff --git a/aleksis/core/templates/core/pages/progress.html b/aleksis/core/templates/core/pages/progress.html
index 799f1f93648ab1de05be3f9432653b8c4e8b1466..fca1ddfc2ebe5980ee132de77bdf1baafce24ae0 100644
--- a/aleksis/core/templates/core/pages/progress.html
+++ b/aleksis/core/templates/core/pages/progress.html
@@ -47,7 +47,7 @@
           {% trans "Go back" %}
         </a>
         {% if additional_button %}
-          <a class="btn waves-effect waves-light" href="{{ additional_button.href }}">
+          <a class="btn waves-effect waves-light" href="{{ additional_button.href }}" id="result-button" style="display: none;">
             <i class="material-icons left">{{ additional_button.icon|default:"" }}</i>
             {{ additional_button.caption }}
           </a>
diff --git a/aleksis/core/templates/core/partials/avatar_content.html b/aleksis/core/templates/core/partials/avatar_content.html
new file mode 100644
index 0000000000000000000000000000000000000000..deef48489f4ed118a4420f05ea226f87d7c0b895
--- /dev/null
+++ b/aleksis/core/templates/core/partials/avatar_content.html
@@ -0,0 +1,29 @@
+{% load rules i18n %}
+{% has_perm 'core.view_avatar_rule' request.user person_or_user as can_view_avatar %}
+{% has_perm 'core.view_photo_rule' request.user person_or_user as can_view_photo %}
+{% if SITE_PREFERENCES.account__person_prefer_photo and person_or_user.photo and can_view_photo %}
+  <div class="{% firstof class "clip-circle" %}">
+    <img class="{% firstof img_class "hundred-percent" %}" src="{{ person_or_user.photo.url }}"
+         alt="{{ person_or_user.full_name }}" {% if title %} title="{{ person_or_user.full_name }}"{% endif %}/>
+  </div>
+{% elif person_or_user.identicon_url %}
+  {# If this is a person #}
+  <div class="{% firstof class "clip-circle" %}">
+    {% if can_view_avatar %}
+      <img class="{% firstof img_class "hundred-percent" %}" src="{{ person_or_user.avatar_url }}"
+           alt="{{ person_or_user.full_name }} ({% trans "Avatar" %})" {% if title %}
+           title="{{ person_or_user.full_name }} ({% trans "Avatar" %})"{% endif %}/>
+    {% else %}
+
+      <img class="{% firstof img_class "hundred-percent" %}" src="{{ person_or_user.identicon_url }}"
+           alt="{{ person_or_user.full_name }} ({% trans "Identicon" %})" {% if title %}
+           title="{{ person_or_user.full_name }} ({% trans "Identicon" %})"{% endif %} />
+    {% endif %}
+  </div>
+
+{% else %}
+  {# There is a user without a person #}
+  <div class="{% firstof class "clip-circle" %} no-image">
+    <i class="material-icons">person</i>
+  </div>
+{% endif %}
diff --git a/aleksis/core/templates/core/partials/hero_background.html b/aleksis/core/templates/core/partials/hero_background.html
new file mode 100644
index 0000000000000000000000000000000000000000..33a4890586a18f5f0b5e486d6fb750707ac805e8
--- /dev/null
+++ b/aleksis/core/templates/core/partials/hero_background.html
@@ -0,0 +1,2 @@
+{% load static %}
+<div id="hero-bg" style="background-image: url('{% static "img/hero.svg" %}'); background-size: 3840px"></div>
diff --git a/aleksis/core/templates/core/partials/meta.html b/aleksis/core/templates/core/partials/meta.html
index 8fdbea9e15dd2c9520d8d6643ab251337da68609..30b2ef099f0914f8ab68df34c047732293d91d5b 100644
--- a/aleksis/core/templates/core/partials/meta.html
+++ b/aleksis/core/templates/core/partials/meta.html
@@ -3,20 +3,20 @@
 <meta charset="utf-8"/>
 <meta http-equiv="X-UA-Compatible" content="IE=edge"/>
 <meta name="viewport" content="width=device-width,initial-scale=1"/>
-<meta name="description" content="{{ request.site.preferences.general__description }}"/>
+<meta name="description" content="{{ SITE_PREFERENCES.general__description }}"/>
 <meta name="generator" content="AlekSIS School Information System"/>
 
-<meta name="theme-color" content="red">
+<meta name="theme-color" content="{{ SITE_PREFERENCES.theme__primary }}">
 <meta name="mobile-web-app-capable" content="yes">
 
-<meta name="apple-mobile-web-app-title" content="{{ request.site.preferences.general__title }}">
+<meta name="apple-mobile-web-app-title" content="{{ SITE_PREFERENCES.general__title }}">
 <meta name="apple-mobile-web-app-capable" content="yes">
 <meta name="apple-mobile-web-app-status-bar-style" content="default">
 
-<meta name="msapplication-navbutton-color" content="{{ request.site.preferences.theme__primary }}">
-<meta name="msapplication-TileColor" content="{{ request.site.preferences.theme__primary }}">
+<meta name="msapplication-navbutton-color" content="{{ SITE_PREFERENCES.theme__primary }}">
+<meta name="msapplication-TileColor" content="{{ SITE_PREFERENCES.theme__primary }}">
 <meta name="msapplication-TileImage" content="{{ PWA_ICONS.microsoft.144.faviconImage.url }}">
-<meta name="application-name" content="{{ request.site.preferences.general__title }}">
+<meta name="application-name" content="{{ SITE_PREFERENCES.general__title }}">
 <meta name="msapplication-starturl" content="/">
 <meta name="msapplication-tap-highlight" content="no">
 <meta name="browsermode" content="application">
diff --git a/aleksis/core/templates/core/partials/sidenav.html b/aleksis/core/templates/core/partials/sidenav.html
index 54df4fe73e4642a693b63bbad76b7941fe142e76..9b3be97059ca834238d39a4dd6f6bfe35205a9d2 100644
--- a/aleksis/core/templates/core/partials/sidenav.html
+++ b/aleksis/core/templates/core/partials/sidenav.html
@@ -9,7 +9,7 @@
   {% for item in core_menu %}
     {% if not item.submenu %}
       <li class="{% if item.selected %} active {% endif %}">
-        <a class="truncate" href="{{ item.url }}">
+        <a class="truncate" {% if item.new_tab %} target="_blank" {% endif %} href="{{ item.url }}">
           {% if item.icon_class %}
             <i class="{{ item.icon_class }}"></i>
           {% elif item.icon %}
@@ -25,7 +25,7 @@
     {% endif %}
     {% if item.submenu %}
       <li class="bold {% if item.selected %} active {% endif %}">
-        <a class="collapsible-header waves-effect waves-primary truncate" href="{{ item.url|default:"#" }}">
+        <a class="collapsible-header waves-effect waves-primary truncate" {% if item.new_tab %} target="_blank" {% endif %} href="{{ item.url|default:"#" }}">
           {% if item.icon_class %}
             <i class="{{ item.icon_class }}"></i>
           {% elif item.icon %}
diff --git a/aleksis/core/templates/core/perms/assign.html b/aleksis/core/templates/core/perms/assign.html
new file mode 100644
index 0000000000000000000000000000000000000000..1833ece952d04f01f2f38440cc0fe8fc1d509bf7
--- /dev/null
+++ b/aleksis/core/templates/core/perms/assign.html
@@ -0,0 +1,32 @@
+{# -*- engine:django -*- #}
+
+{% extends "core/base.html" %}
+
+{% load material_form i18n any_js %}
+
+{% block extra_head %}
+  {{ form.media.css }}
+  {% include_css "select2-materialize" %}
+{% endblock %}
+
+{% block browser_title %}{% blocktrans %}Assign permission{% endblocktrans %}{% endblock %}
+{% block page_title %}{% blocktrans %}Assign permission{% endblocktrans %}{% endblock %}
+
+{% block content %}
+  <p class="flow-text">
+    {% trans "Selected permission" %}: {{ object }}
+  </p>
+
+  <form method="post">
+    {% csrf_token %}
+    <input type="hidden" name="step" value="{{ step }}">
+    {% form form=form %}{% endform %}
+    <button type="submit" class="btn green waves-effect waves-light">
+      <i class="material-icons left">save</i>
+      {% trans "Assign" %}
+    </button>
+  </form>
+
+  {% include_js "select2-materialize" %}
+  {{ form.media.js }}
+{% endblock %}
diff --git a/aleksis/core/templates/core/perms/list.html b/aleksis/core/templates/core/perms/list.html
new file mode 100644
index 0000000000000000000000000000000000000000..73dcfb8730d5e2235b3b398a4ef0f0fd70c8c31b
--- /dev/null
+++ b/aleksis/core/templates/core/perms/list.html
@@ -0,0 +1,73 @@
+{# -*- engine:django -*- #}
+
+{% extends "core/base.html" %}
+
+{% load material_form i18n any_js django_tables2 %}
+
+{% block extra_head %}
+  {{ filter.form.media.css }}
+  {{ assign_form.media.css }}
+  {% include_css "select2-materialize" %}
+{% endblock %}
+
+{% block browser_title %}{% blocktrans %}Manage permissions{% endblocktrans %}{% endblock %}
+{% block page_title %}{% blocktrans %}Manage permissions{% endblocktrans %}{% endblock %}
+
+{% block content %}
+  <div class="card">
+    <div class="card-content">
+      <form action="{% url "select_permission_for_assign" %}?next={% url "manage_"|add:tab|add:"_permissions" %}"
+            method="post">
+        <div class="card-title">{% trans "Assign a new permission" %}</div>
+        {% csrf_token %}
+        {% form form=assign_form %}{% endform %}
+        <button type="submit" class="btn green waves-effect waves-light">
+          {% trans "Select" %}
+        </button>
+      </form>
+    </div>
+  </div>
+
+  <ul class="tabs">
+    <li class="tab">
+      <a target="_self" href="{% url "manage_user_global_permissions" %}"
+         {% if tab == "user_global" %}class="active"{% endif %}>{% trans "Global (user)" %}</a>
+    </li>
+    <li class="tab">
+      <a target="_self" href="{% url "manage_group_global_permissions" %}"
+         {% if tab == "group_global" %}class="active"{% endif %}>{% trans "Global (group)" %}</a>
+    </li>
+    <li class="tab">
+      <a target="_self" href="{% url "manage_user_object_permissions" %}"
+         {% if tab == "user_object" %}class="active"{% endif %}>{% trans "Object (user)" %}</a>
+    </li>
+    <li class="tab">
+      <a target="_self" href="{% url "manage_group_object_permissions" %}"
+         {% if tab == "group_object" %}class="active"{% endif %}>{% trans "Object (group)" %}</a>
+    </li>
+  </ul>
+
+  <div class="card">
+    <div class="card-content">
+      <div class="card-title">{% trans "Filter permissions" %}</div>
+      <form method="get" action="">
+        {% csrf_token %}
+        {% form form=filter.form %}{% endform %}
+        <button type="submit" class="btn waves-effect waves-light">
+          <i class="material-icons left">refresh</i>
+          {% trans "Update" %}
+        </button>
+      </form>
+    </div>
+  </div>
+
+  <div class="card">
+    <div class="card-content">
+      {% render_table table %}
+    </div>
+  </div>
+
+  {% include_js "select2-materialize" %}
+  {{ filter.form.media.js }}
+  {{ assign_form.media.js }}
+{% endblock %}
diff --git a/aleksis/core/templates/core/person/collection.html b/aleksis/core/templates/core/person/collection.html
index c23fa2360c00977485a9d32373ae826d7fe66b6d..c86da61e43ae19ceaa0f601c2f3ec012357bc04d 100644
--- a/aleksis/core/templates/core/person/collection.html
+++ b/aleksis/core/templates/core/person/collection.html
@@ -1,7 +1,9 @@
-<div class="collection">
+{% load rules %}
+
+<div class="collection person-collection">
   {% for person in persons %}
-    <a class="collection-item" href="{% url "person_by_id" person.pk %}">
-      <i class="material-icons left">person</i>
+    <a class="collection-item avatar waves-effect" href="{% url "person_by_id" person.pk %}">
+      {% include "core/partials/avatar_content.html" with person_or_user=person class="materialize-circle" img_class="materialize-circle" %}
       {{ person }}
     </a>
   {% endfor %}
diff --git a/aleksis/core/templates/core/person/full.html b/aleksis/core/templates/core/person/full.html
index 6d48194665b2667df1bfdaf0a3c0d05a706c1d23..3972661fa81828e12b47fa606a3fc59b37ba9e69 100644
--- a/aleksis/core/templates/core/person/full.html
+++ b/aleksis/core/templates/core/person/full.html
@@ -6,102 +6,181 @@
 {% load render_table from django_tables2 %}
 
 {% block browser_title %}{{ person.first_name }} {{ person.last_name }}{% endblock %}
+{% block no_page_title%}{% endblock %}
 
 {% block content %}
-  <h1>{{ person.first_name }} {{ person.last_name }}</h1>
 
   {% has_perm 'core.edit_person_rule' user person as can_change_person %}
   {% has_perm 'core.change_person_preferences_rule' user person as can_change_person_preferences %}
   {% has_perm 'core.delete_person_rule' user person as can_delete_person %}
   {% has_perm "core.impersonate_rule" user person as can_impersonate %}
+  {% has_perm "core.can_invite" user person as can_invite %}
 
-  {% if can_change_person or can_change_person_preferences or can_delete_person or can_impersonate %}
-    <p>
-      {% if can_change_person %}
-        <a href="{% url 'edit_person_by_id' person.id %}" class="btn waves-effect waves-light">
-          <i class="material-icons left">edit</i>
-          {% trans "Edit" %}
-        </a>
-      {% endif %}
+  {% include "core/partials/hero_background.html" %}
 
-      {% if can_delete_person %}
-        <a href="{% url 'delete_person_by_id' person.id %}" class="btn waves-effect waves-light red">
-          <i class="material-icons left">delete</i>
-          {% trans "Delete" %}
-        </a>
-      {% endif %}
+  {% if can_change_person or can_change_person_preferences or can_delete_person or can_impersonate or can_invite %}
+      <p class="person-buttons hide-on-med-and-down">
+        {% if can_change_person %}
+          <a href="{% url 'edit_person_by_id' person.id %}" class="btn waves-effect waves-light">
+            <i class="material-icons left">edit</i>
+            {% trans "Edit" %}
+          </a>
+        {% endif %}
 
-      {% if can_change_person_preferences %}
-        <a href="{% url "preferences_person" person.id %}" class="btn waves-effect waves-light">
-          <i class="material-icons left">settings</i>
-          {% trans "Change preferences" %}
-        </a>
-      {% endif %}
+        {% if can_delete_person %}
+          <a href="{% url 'delete_person_by_id' person.id %}" class="btn waves-effect waves-light red">
+            <i class="material-icons left">delete</i>
+            {% trans "Delete" %}
+          </a>
+        {% endif %}
+
+        {% if can_change_person_preferences %}
+          <a href="{% url 'preferences_person' person.id %}" class="btn waves-effect waves-light">
+            <i class="material-icons left">settings</i>
+            {% trans "Change preferences" %}
+          </a>
+        {% endif %}
 
-    {% if can_impersonate and person.user %}
-        <a href="{% url "impersonate-start" person.user.id %}" class="btn waves-effect waves-light">
-          <i class="material-icons left">portrait</i>
-          {% trans "Impersonate" %}
-        </a>
+        {% if can_impersonate and person.user %}
+          <a href="{% url 'impersonate-start' person.user.id %}" class="btn waves-effect waves-light">
+            <i class="material-icons left">portrait</i>
+            {% trans "Impersonate" %}
+          </a>
+        {% endif %}
+
+        {% if invite_enabled and can_invite and not person.user %}
+          <a href="{% url "invite_person_by_id" person.id %}" class="btn waves-effect waves-light">
+            <i class="material-icons left">card_giftcard</i>
+            {% trans "Invite user" %}
+          </a>
+        {% endif %}
+      </p>
+    {% endif %}
+
+  <header class="person-container">
+    <div class="image-wrapper">
+      {% include "core/partials/avatar_content.html" with class="clip-circle materialboxed z-depth-2" person_or_user=person title=True %}
+    </div>
+    <h1>
+      {{ person.first_name }} {{ person.last_name }}
+      {% if person.user %}
+        <small class="grey-text">{{ person.user.username }}</small>
       {% endif %}
-    </p>
-  {% endif %}
+    </h1>
+  </header>
 
-  <h2>{% blocktrans %}Contact details{% endblocktrans %}</h2>
   <div class="row">
-    <div class="col s12 m4">
-      {% has_perm 'core.view_photo_rule' user person as can_view_photo %}
-      {% if person.photo and can_view_photo %}
-        <img class="person-img" src="{{ person.photo.url }}"
-             alt="{{ person.first_name }} {{ person.last_name }}"/>
-      {% else %}
-        <img class="person-img" src="{% static 'img/fallback.png' %}"
-             alt="{{ person.first_name }} {{ person.last_name }}"/>
-      {% endif %}
-    </div>
-    <div class="col s12 m8">
-      <table class="responsive-table highlight">
-        <tr>
-          <td rowspan="6">
+    {% if person.description %}
+      <div class="col s12">
+        <p class="container center-align">
+          {{ person.description }}
+        </p>
+      </div>
+    {% endif %}
+    {% if can_change_person or can_change_person_preferences or can_delete_person or can_impersonate or can_invite %}
+      <div class="col s12 hide-on-large-only">
+        <div class="collection">
+          {% if can_change_person %}
+            <a href="{% url 'edit_person_by_id' person.id %}" class="collection-item waves-effect waves-dark">
+              <i class="material-icons left">edit</i>
+              {% trans "Edit" %}
+            </a>
+          {% endif %}
 
-          </td>
+          {% if can_delete_person %}
+            <a href="{% url 'delete_person_by_id' person.id %}" class="collection-item waves-effect waves-red red-text">
+              <i class="material-icons left">delete</i>
+              {% trans "Delete" %}
+            </a>
+          {% endif %}
+
+          {% if can_change_person_preferences %}
+            <a href="{% url 'preferences_person' person.id %}" class="collection-item waves-effect waves-dark">
+              <i class="material-icons left">settings</i>
+              {% trans "Change preferences" %}
+            </a>
+          {% endif %}
+
+        {% if can_impersonate and person.user %}
+            <a href="{% url 'impersonate-start' person.user.id %}" class="collection-item waves-effect waves-dark">
+              <i class="material-icons left">portrait</i>
+              {% trans "Impersonate" %}
+            </a>
+          {% endif %}
+
+        {% if can_invite and not person.user %}
+          <a href="{% url "invite_person_by_id" person.id %}" class="collection-item waves-effect waves-light">
+            <i class="material-icons left">card_giftcard</i>
+            {% trans "Invite user" %}
+          </a>
+        {% endif %}
+        </div>
+      </div>
+    {% endif %}
+    <div class="col s12 l4">
+      <h2>{% blocktrans %}Contact details{% endblocktrans %}</h2>
+      <div class="card-panel">
+        <table class="highlight">
+        <tr>
           <td>
             <i class="material-icons small">person</i>
           </td>
-          <td>{{ person.first_name }}</td>
-          <td>{{ person.additional_name }}</td>
-          <td>{{ person.last_name }}</td>
+          <td>{{ person.first_name }} {{ person.additional_name }} {{ person.last_name }}</td>
         </tr>
         <tr>
           <td>
             <i class="material-icons small">face</i>
           </td>
-          <td colspan="3">{{ person.get_sex_display }}</td>
+          <td>{% firstof person.get_sex_display "–" %}</td>
         </tr>
         {% has_perm 'core.view_address_rule' user person as can_view_address %}
         {% if can_view_address %}
           <tr>
-            <td>
+            <td rowspan="2">
               <i class="material-icons small">home</i>
             </td>
-            <td colspan="2">{{ person.street }} {{ person.housenumber }}</td>
-            <td colspan="2">{{ person.postal_code }} {{ person.place }}</td>
+            <td>{% firstof person.street "–" %} {{ person.housenumber }}</td>
+          </tr>
+          <tr>
+            <td>{{ person.postal_code }} {% firstof person.place "–" %}</td>
           </tr>
         {% endif %}
         {% has_perm 'core.view_contact_details_rule' user person as can_view_contact_details %}
         {% if can_view_contact_details %}
           <tr>
-            <td>
+            <td rowspan="2">
               <i class="material-icons small">phone</i>
             </td>
-            <td>{{ person.phone_number }}</td>
-            <td>{{ person.mobile_number }}</td>
+            <td>
+              {% if person.phone_number %}
+                <a href="{{ person.phone_number.as_rfc3966 }}">{{ person.phone_number.as_international }}</a>
+              {% else %}
+                –
+              {% endif %}
+              <small>({% trans "Home phone" %})</small>
+            </td>
+          </tr>
+          <tr>
+            <td>
+              {% if person.mobile_number %}
+                <a href="{{ person.mobile_number.as_rfc3966 }}">{{ person.mobile_number.as_international }}</a>
+              {% else %}
+                –
+              {% endif %}
+              <small>({% trans "Mobile phone" %})</small>
+            </td>
           </tr>
           <tr>
             <td>
               <i class="material-icons small">email</i>
             </td>
-            <td colspan="3">{{ person.email }}</td>
+            <td>
+              {% if person.email %}
+                <a href="mailto:{{ person.email }}">{{ person.email }}</a>
+              {% else %}
+                –
+              {% endif %}
+            </td>
           </tr>
         {% endif %}
         {% has_perm 'core.view_personal_details_rule' user person as can_view_personal_details %}
@@ -110,38 +189,66 @@
             <td>
               <i class="material-icons small">cake</i>
             </td>
-            <td colspan="3">{{ person.date_of_birth|date }}</td>
+            <td>
+              <time datetime="{{ person.date_of_birth|date:'c' }}">{{ person.date_of_birth|date }}</time>
+              {% firstof person.place_of_birth "–" %}
+            </td>
           </tr>
         {% endif %}
       </table>
-    </div>
-    {% if person.description %}
-      <div class="col s12 m12">
-        <h2>{% trans "Description" %}</h2>
-        <p>
-          {{ person.description }}
-        </p>
       </div>
-    {% endif %}
-  </div>
-
-  {% if person.children.all and can_view_personal_details %}
-    <div class="col s12 m12">
-      <h2>{% trans "Children" %}</h2>
-      {% include "core/person/collection.html" with persons=person.children.all %}
+      {% has_perm 'core.view_avatar_rule' user person as can_view_avatar %}
+      {% has_perm 'core.view_photo_rule' user person as can_view_photo %}
+      {% if person.photo and can_view_photo and not SITE_PREFERENCES.account__person_prefer_photo %}
+        <div class="card">
+          <div class="card-image">
+            <img src="{{ person.photo.url }}" alt="{{ person.first_name }} {{ person.last_name }}" class="materialboxed">
+            <span class="card-title">{{ person.first_name }} {{ person.last_name }}</span>
+          </div>
+        </div>
+      {% elif person.avatr and can_view_avatar %}
+        <div class="card">
+          <div class="card-image">
+            <img src="{{ person.avatar.url }}"
+                 alt="{{ person.first_name }} {{ person.last_name }}  ({% trans "Avatar" %})" class="materialboxed">
+            <span class="card-title">{{ person.first_name }} {{ person.last_name }} ({% trans "Avatar" %})</span>
+          </div>
+        </div>
+      {% endif %}
     </div>
-  {% endif %}
 
-  {% if person.guardians.all and can_view_personal_details %}
-    <div class="col s12 m12">
-      <h2>{% trans "Guardians / Parents" %}</h2>
-      {% include "core/person/collection.html" with persons=person.guardians.all %}
+    {% if person.children.all or person.guardians.all and can_view_personal_details %}
+    <div class="col s12 m6 l4">
+      {% if person.children.all and can_view_personal_details %}
+        <h2>{% trans "Children" %}</h2>
+        <div class="card-panel">
+          {% include "core/person/collection.html" with persons=person.children.all %}
+        </div>
+      {% endif %}
+
+      {% if person.guardians.all and can_view_personal_details %}
+        <h2>{% trans "Guardians / Parents" %}</h2>
+        <div class="card-panel">
+          {% include "core/person/collection.html" with persons=person.guardians.all %}
+        </div>
+      {% endif %}
     </div>
-  {% endif %}
+    {% endif %}
 
-  {% has_perm 'core.view_person_groups_rule' user person as can_view_groups %}
-  {% if can_view_groups %}
-    <h2>{% blocktrans %}Groups{% endblocktrans %}</h2>
-    {% render_table groups_table %}
-  {% endif %}
+    {% has_perm 'core.view_person_groups_rule' user person as can_view_groups %}
+    {% if can_view_groups and groups %}
+      <div class="col s12 m6 l4">
+        <h2>{% blocktrans %}Groups{% endblocktrans %}</h2>
+        <div class="card-panel">
+          <div class="collection">
+            {% for group in groups %}
+              <a href="{{ group.get_absolute_url }}" class="collection-item">
+                {{ group.name }} ({{ group.school_term }})
+              </a>
+            {% endfor %}
+          </div>
+        </div>
+      </div>
+    {% endif %}
+  </div>
 {% endblock %}
diff --git a/aleksis/core/templates/dynamic_preferences/form.html b/aleksis/core/templates/dynamic_preferences/form.html
index 692d25e90b3dd5cb6554fe23010f5cbc542c3c94..22f036912c6493ac3f9a17815f1c46c6bdb413fe 100644
--- a/aleksis/core/templates/dynamic_preferences/form.html
+++ b/aleksis/core/templates/dynamic_preferences/form.html
@@ -10,7 +10,7 @@
   {% elif registry_name == "person" and instance == request.user.person %}
     {% blocktrans %}My preferences{% endblocktrans %}
   {% else %}
-    {% blocktrans with instace=instance %}Preferences for {{ instance }}{% endblocktrans %}
+    {% blocktrans with instance=instance %}Preferences for {{ instance }}{% endblocktrans %}
   {% endif %}
 {% endblock %}
 
diff --git a/aleksis/core/templates/impersonate/list_users.html b/aleksis/core/templates/impersonate/list_users.html
deleted file mode 100644
index fd273ca60902e5defbc41e567a9205cfe99fb349..0000000000000000000000000000000000000000
--- a/aleksis/core/templates/impersonate/list_users.html
+++ /dev/null
@@ -1,28 +0,0 @@
-{# -*- engine:django -*- #}
-{# Derived from the original template in django-impersonate #}
-
-{% extends "core/base.html" %}
-{% load i18n %}
-
-{% block browser_title %}{% trans "Impersonate" %}{% endblock %}
-{% block page_title %}{% trans "Impersonate" %}{% endblock %}
-
-{% block content %}
-  {% if page.object_list %}
-    <div class="collection">
-      {% for user in page.object_list %}
-        <a class="collection-item" href="{% url 'impersonate-start' user.pk %}{{ redirect }}">
-          {{ user }}
-        </a>
-      {% endfor %}
-    </div>
-  {% endif %}
-
-  {% if page.has_previous %}
-    <a href="{% url 'impersonate-list' %}?page={{ page.previous_page_number }}">Previous Page</a> &nbsp;
-  {% endif %}
-
-  {% if page.has_next %}
-    <a href="{% url 'impersonate-list' %}?page={{ page.next_page_number }}">Next Page</a> &nbsp;
-  {% endif %}
-{% endblock %}
diff --git a/aleksis/core/templates/invitations/disabled.html b/aleksis/core/templates/invitations/disabled.html
new file mode 100644
index 0000000000000000000000000000000000000000..2515a01f2437c7f617cc14fcee0226bedeacd341
--- /dev/null
+++ b/aleksis/core/templates/invitations/disabled.html
@@ -0,0 +1,21 @@
+{% extends "core/base.html" %}
+
+{% load i18n %}
+
+{% block browser_title %}{% blocktrans %}The invite feature is disabled{% endblocktrans %}{% endblock %}
+{% block no_page_title %}{% endblock %}
+
+{% block content %}
+  <div class="container">
+    <div class="card red">
+      <div class="card-content white-text">
+        <i class="material-icons small left">disabled_by_default</i>
+        <span class="card-title">{% blocktrans %}The invite feature is disabled.{% endblocktrans %}</span>
+        <p>
+          {% trans "To enable it, switch on the corresponding checkbox in the authentication section of the " %}
+          <a href="{% url "preferences_site" %}">{% trans "site preferences page" %}</a>.
+        </p>
+      </div>
+    </div>
+  </div>
+{% endblock %}
diff --git a/aleksis/core/templates/invitations/enter.html b/aleksis/core/templates/invitations/enter.html
new file mode 100644
index 0000000000000000000000000000000000000000..03605a710697334567384a74a289783b64a7f55e
--- /dev/null
+++ b/aleksis/core/templates/invitations/enter.html
@@ -0,0 +1,44 @@
+{# -*- engine:django -*- #}
+
+{% extends "core/base.html" %}
+
+{% load material_form i18n %}
+
+{% block browser_title %}{% blocktrans %}Accept invitation{% endblocktrans %}{% endblock %}
+{% block no_page_title %}{% endblock %}
+
+{% block extra_head %}
+  {{ form.media.css }}
+{% endblock %}
+
+{% block content %}
+  <div class="row">
+  <div class="col m1 l2 xl3"></div>
+  <div class="col s12 m10 l8 xl6">
+    <div class="card">
+      <form method="post">
+        <div class="card-content">
+          <div class="card-title">{% trans "Accept your invitation" %}</div>
+          <div class="alert primary">
+            <div>
+              <i class="material-icons left">info</i>
+              {% blocktrans %}
+                Please enter your invitation code to register
+                your new user account:
+              {% endblocktrans %}
+            </div>
+          </div>
+          {% csrf_token %}
+          {% form form=form %}{% endform %}
+        </div>
+        <div class="card-action-light">
+          <button type="submit" class="btn green waves-effect waves-light">
+            <i class="material-icons left">card_giftcard</i>
+            {% trans "Accept invite" %}
+          </button>
+        </div>
+      </form>
+    </div>
+  </div>
+  {{ form.media.js }}
+{% endblock %}
diff --git a/aleksis/core/templates/invitations/forms/_invite.html b/aleksis/core/templates/invitations/forms/_invite.html
new file mode 100644
index 0000000000000000000000000000000000000000..297997e0b04f3d99a1e1279c8900d0bcc9c50024
--- /dev/null
+++ b/aleksis/core/templates/invitations/forms/_invite.html
@@ -0,0 +1,38 @@
+{# -*- engine:django -*- #}
+
+{% extends "core/base.html" %}
+
+{% load material_form i18n %}
+
+{% load render_table from django_tables2 %}
+
+{% block browser_title %}{% blocktrans %}Invite{% endblocktrans %}{% endblock %}
+{% block page_title %}{% blocktrans %}Invite{% endblocktrans %}{% endblock %}
+
+
+{% block content %}
+
+  <div class="row">
+    <div class="col s6">
+      <h5>{% trans "Invite by email address" %}</h5>
+      <form method="post">
+        {% csrf_token %}
+        {% form form=form %}{% endform %}
+        {% trans "Invite" as caption %}
+        {% include "core/partials/save_button.html" with caption=caption icon="card_giftcard" %}
+      </form>
+    </div>
+    <div class="col s6">        
+      <h5>{% trans "Generate invitation code" %}</h5>
+
+      <a class="btn waves-effect waves-light hundred-percent" href="{% url 'generate_invitation_code' %}">
+        {% trans "Generate code" %}
+      </a>
+    </div>
+    <div class="col s12">
+      <h5>{% trans "Invitations" %}</h5>
+
+      {% render_table table %}
+    </div>
+  </div>
+{% endblock %}
diff --git a/aleksis/core/templates/invitations/messages/invite_accepted.txt b/aleksis/core/templates/invitations/messages/invite_accepted.txt
new file mode 100644
index 0000000000000000000000000000000000000000..59699adee30108f241eb6dd4db1478f75399292d
--- /dev/null
+++ b/aleksis/core/templates/invitations/messages/invite_accepted.txt
@@ -0,0 +1,3 @@
+{% load i18n %}
+
+{% blocktrans %}The invitation for {{ email }} has been accepted.{% endblocktrans %}
diff --git a/aleksis/core/templates/oauth2_provider/application/create.html b/aleksis/core/templates/oauth2_provider/application/create.html
index 38b9a8d02e52b977223cfffdcb8d3c9165c42a71..73b94677206d38fca163858551536d575b2dce3d 100644
--- a/aleksis/core/templates/oauth2_provider/application/create.html
+++ b/aleksis/core/templates/oauth2_provider/application/create.html
@@ -6,11 +6,11 @@
 {% block page_title %}{% blocktrans %}Register OAuth2 Application{% endblocktrans %}{% endblock %}
 
 {% block content %}
-  <form method="post">
+  <form method="post" enctype="multipart/form-data">
     {% csrf_token %}
     {% form form=form %}{% endform %}
     {% include "core/partials/save_button.html" %}
-    <a class="btn waves-effect red waves-light" href="{% url "oauth_list" %}">
+    <a class="btn waves-effect red waves-light" href="{% url "oauth2_applications" %}">
       <i class="material-icons left">clear</i> {% trans "Cancel" %}
     </a>
   </form>
diff --git a/aleksis/core/templates/oauth2_provider/application/detail.html b/aleksis/core/templates/oauth2_provider/application/detail.html
new file mode 100644
index 0000000000000000000000000000000000000000..28e2af7d70ff4b6dd5d93b46b8ec52d31bc36538
--- /dev/null
+++ b/aleksis/core/templates/oauth2_provider/application/detail.html
@@ -0,0 +1,87 @@
+{% extends "core/base.html" %}
+
+{% load i18n %}
+
+{% block browser_title %}{% blocktrans %}OAuth2 Application{% endblocktrans %}{% endblock %}
+{% block page_title %}
+  <a href="{% url "oauth2_applications" %}"
+     class="btn-flat primary-color-text waves-light waves-effect">
+    <i class="material-icons left">chevron_left</i> {% trans "Back" %}
+  </a>
+  {{ application.name }}
+{% endblock %}
+
+{% block content %}
+  <a class="btn orange waves-effect waves-light btn-margin" href="{% url "edit_oauth2_application" application.id %}">
+    <i class="material-icons left">edit</i>
+    {% trans "Edit" %}
+  </a>
+  <a class="btn red waves-effect waves-light btn-margin" href="{% url "delete_oauth2_application" application.id %}">
+    <i class="material-icons left">delete</i>
+    {% trans "Delete" %}
+  </a>
+  <table class="responsive-table">
+    <tbody>
+    <tr>
+      <th>{% trans "Icon" %}</th>
+      <td>
+        {% if application.icon %}
+          <div class="application-circle materialboxed z-depth-2">
+            <img src="{{ application.icon.url }}" alt="{{ oauth_application.name }}" class="hundred-percent">
+          </div>
+        {% else %}
+          –
+        {% endif %}
+      </td>
+    </tr>
+    <tr>
+      <th>
+        {% trans "Client id" %}
+      </th>
+      <td>
+        <code class="break-word">{{ application.client_id }}</code>
+      </td>
+    </tr>
+    <tr>
+      <th>
+        {% trans "Client secret" %}
+      </th>
+      <td>
+        <code class="break-word">{{ application.client_secret }}</code>
+      </td>
+    </tr>
+    <tr>
+      <th>
+        {% trans "Client type" %}
+      </th>
+      <td>
+        {{ application.client_type }}
+      </td>
+    </tr>
+    <tr>
+      <th>
+        {% trans "Allowed scopes" %}
+      </th>
+      <td>
+        {{ application.allowed_scopes|join:", " }}
+      </td>
+    </tr>
+    <tr>
+      <th>
+        {% trans "Redirect URIs" %}
+      </th>
+      <td>
+        {{ application.redirect_uris }}
+      </td>
+    </tr>
+    <tr>
+      <th>
+        {% trans "Skip Authorisation" %}
+      </th>
+      <td>
+        <i class="material-icons">{{ application.skip_authorization|yesno:"check,close" }}</i>
+      </td>
+    </tr>
+    </tbody>
+  </table>
+{% endblock %}
diff --git a/aleksis/core/templates/oauth2_provider/application/edit.html b/aleksis/core/templates/oauth2_provider/application/edit.html
index ac2b7d3468c114d7d3739c8fec9729b6d4c7ab54..30f50fff94e330e941d7b4730fe7d875b039d74e 100644
--- a/aleksis/core/templates/oauth2_provider/application/edit.html
+++ b/aleksis/core/templates/oauth2_provider/application/edit.html
@@ -6,11 +6,11 @@
 {% block page_title %}{% blocktrans %}Edit OAuth2 Application{% endblocktrans %}{% endblock %}
 
 {% block content %}
-  <form method="post">
+  <form method="post" enctype="multipart/form-data">
     {% csrf_token %}
     {% form form=form %}{% endform %}
     {% include "core/partials/save_button.html" %}
-    <a class="btn waves-effect red waves-light" href="{% url "oauth_detail" application.id %}">
+    <a class="btn waves-effect red waves-light" href="{% url "oauth2_application" application.id %}">
       <i class="material-icons left">clear</i> {% trans "Cancel" %}
     </a>
   </form>
diff --git a/aleksis/core/templates/oauth2_provider/application/list.html b/aleksis/core/templates/oauth2_provider/application/list.html
new file mode 100644
index 0000000000000000000000000000000000000000..ced7d718dfe1b561c2ea8253e25658a9f449024d
--- /dev/null
+++ b/aleksis/core/templates/oauth2_provider/application/list.html
@@ -0,0 +1,29 @@
+{% extends "core/base.html" %}
+
+{% load i18n %}
+
+{% block browser_title %}{% blocktrans %}OAuth2 Applications{% endblocktrans %}{% endblock %}
+{% block page_title %}{% blocktrans %}OAuth2 Applications{% endblocktrans %}{% endblock %}
+
+{% block content %}
+  <a href="{% url "register_oauth_application" %}" class="btn green waves-effect waves-light">
+    <i class="material-icons left">add</i>
+    {% blocktrans %}Register new application{% endblocktrans %}
+  </a>
+  <div class="collection">
+    {% for application in applications %}
+      <a class="collection-item avatar" href="{% url "oauth2_application" application.id %}">
+        {% if application.icon %}
+          <img src="{{ application.icon.url }}" alt="{{ application.name }}" class="circle">
+        {% endif %}
+        <span class="title">
+          {{ application.name }}
+        </span>
+      </a>
+      {% empty %}
+      <div class="collection-item flow-text">
+        {% blocktrans %}No applications defined.{% endblocktrans %}
+      </div>
+    {% endfor %}
+  </div>
+{% endblock %}
diff --git a/aleksis/core/templates/oauth2_provider/application_confirm_delete.html b/aleksis/core/templates/oauth2_provider/application_confirm_delete.html
deleted file mode 100644
index f72a5be615700ee32326eda2982964e3e8de3e52..0000000000000000000000000000000000000000
--- a/aleksis/core/templates/oauth2_provider/application_confirm_delete.html
+++ /dev/null
@@ -1,27 +0,0 @@
-{% extends "core/base.html" %}
-
-{% load i18n %}
-
-{% block browser_title %}{% trans "Delete application" %}{% endblock %}
-{% block page_title %}{% trans "Delete application" %}{% endblock %}
-
-{% block content %}
-  <div class="alert info">
-    <p>
-      <i class="material-icons left">warning</i>
-      {% blocktrans with application_name=application.name %}Are you sure to delete the application {{ application_name }}?{% endblocktrans %}
-    </p>
-  </div>
-
-  <form method="post" action="{% url 'oauth2_provider:delete' application.pk %}">
-    {% csrf_token %}
-    <button type="submit" class="btn waves-effect waves-light red">
-      <i class="material-icons left">delete</i>
-      {% trans "Delete" %}
-    </button>
-    <a class="btn waves-effect waves-light" href="{% url "oauth2_provider:list" %}">
-      <i class="material-icons left">close</i>
-      {% trans "Cancel" %}
-    </a>
-  </form>
-{% endblock %}
diff --git a/aleksis/core/templates/oauth2_provider/application_detail.html b/aleksis/core/templates/oauth2_provider/application_detail.html
deleted file mode 100644
index 093bc37192bbdf1cf58447963ada4d4f19085724..0000000000000000000000000000000000000000
--- a/aleksis/core/templates/oauth2_provider/application_detail.html
+++ /dev/null
@@ -1,59 +0,0 @@
-{% extends "core/base.html" %}
-
-{% load i18n %}
-
-{% block browser_title %}{% blocktrans %}OAuth2 Applications{% endblocktrans %}{% endblock %}
-{% block page_title %}
-    <a href="{% url "oauth_list" %}"
-       class="btn-flat primary-color-text waves-light waves-effect">
-      <i class="material-icons left">chevron_left</i> {% trans "Back" %}
-    </a>
-    {{ application.name }}
-{% endblock %}
-
-{% block content %}
-  <a class="btn waves-effect waves-light btn-margin" href="{% url "edit_oauth_application" application.id %}">
-    <i class="material-icons left">edit</i>
-    {% trans "Edit" %}
-  </a>
-  <a class="btn red waves-effect waves-light btn-margin" href="{% url "oauth_delete" application.id %}">
-    <i class="material-icons left">delete_forever</i>
-    {% trans "Delete" %}
-  </a>
-  <table class="responsive-table">
-    <tbody>
-      <tr>
-        <th>
-          {% trans "Client id" %}
-        </td>
-        <td>
-          <code class="break-word">{{ application.client_id }}</code>
-        </td>
-      </tr>
-      <tr>
-        <th>
-          {% trans "Client secret"%}
-        </td>
-        <td>
-          <code class="break-word">{{ application.client_secret }}</code>
-        </td>
-      </tr>
-      <tr>
-        <th>
-          {% trans "Client type"%}
-        </td>
-        <td>
-          {{ application.client_type }}
-        </td>
-      </tr>
-      <tr>
-        <th>
-          {% trans "Redirect URIs"%}
-        </td>
-        <td>
-          {{ application.redirect_uris }}
-        </td>
-      </tr>
-    </tbody>
-  </table>
-{% endblock %}
diff --git a/aleksis/core/templates/oauth2_provider/application_list.html b/aleksis/core/templates/oauth2_provider/application_list.html
deleted file mode 100644
index 894315940ffe8a8faaa1d93e26e06c76e1226fc8..0000000000000000000000000000000000000000
--- a/aleksis/core/templates/oauth2_provider/application_list.html
+++ /dev/null
@@ -1,26 +0,0 @@
-{% extends "core/base.html" %}
-
-{% load i18n %}
-
-{% block browser_title %}{% blocktrans %}OAuth2 Applications{% endblocktrans %}{% endblock %}
-
-{% block content %}
-  <h1>{% blocktrans %}OAuth2 applications{% endblocktrans %}</h1>
-  <a href="{% url "register_oauth_application" %}" class="btn green waves-effect waves-light">
-    <i class="material-icons left">add</i>
-    {% blocktrans %}Register new application{% endblocktrans %}
-  </a>
-  <ul class="collection">
-  {% for application in applications %}
-      <li class="collection-item">
-        <div>
-          <a href="{% url "oauth_detail" application.id %}">{{ application.name }}</a>
-        </div>
-      </li>
-  {% empty %}
-      <li class="collection-item flow-text">
-        {% blocktrans %}No applications defined.{% endblocktrans %}
-      </li>
-  {% endfor %}
-  </ul>
-{% endblock %}
diff --git a/aleksis/core/templates/oauth2_provider/authorize.html b/aleksis/core/templates/oauth2_provider/authorize.html
index 5eaf3151021c27fd24b8e19e195a4a2e14d5279a..c90d5e8dd9d3d72cff9e19ce167487900e904636 100644
--- a/aleksis/core/templates/oauth2_provider/authorize.html
+++ b/aleksis/core/templates/oauth2_provider/authorize.html
@@ -12,8 +12,15 @@
     <div class="col s12 m10 l8 xl6">
       <div class="card">
         <div class="card-content">
-          <div class="card-title">
-            {% trans "Authorize" %} {{ application.name }}
+          {% if application.icon %}
+            <div class="center-via-flex margin-bottom">
+              <div class="application-circle materialboxed z-depth-2">
+                <img src="{{ application.icon.url }}" alt="{{ application.name }}" class="hundred-percent">
+              </div>
+            </div>
+          {% endif %}
+          <div class="card-title {% if application.icon %}center{% endif %}">
+            {% blocktrans with name=application.name %}Authorize {{ name }}{% endblocktrans %}
           </div>
           <p class="margin-bottom">{% trans "The application requests access to the following scopes:" %}</p>
           {% for scope in scopes_descriptions %}
@@ -32,7 +39,7 @@
             <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 %}">
+            <a class="btn red waves-effect waves-light btn-margin" href="{% block app-form-back-url %}{% url "oauth2_application" application.id %}{% endblock app-form-back-url %}">
               <i class="material-icons left">cancel</i> {% trans "Disallow" %}
             </a>
           </form>
diff --git a/aleksis/core/templates/oauth2_provider/authorized-token-delete.html b/aleksis/core/templates/oauth2_provider/authorized-token-delete.html
index f658822980bda161890060fb9002cec8fb317297..7f6f25920bebcbbaad7bb93fd486db617f25d662 100644
--- a/aleksis/core/templates/oauth2_provider/authorized-token-delete.html
+++ b/aleksis/core/templates/oauth2_provider/authorized-token-delete.html
@@ -15,11 +15,11 @@
 
   <form method="post">
     {% csrf_token %}
-    <a class="btn waves-effect waves-light red" href="{% url "oauth_list" %}">
+    <a class="btn waves-effect waves-light red" href="{% url "oauth2_applications" %}">
       <i class="material-icons left">delete</i>
       {% trans "Revoke" %}
     </a>
-    <a class="btn waves-effect waves-light" href="{% url "oauth_list" %}">
+    <a class="btn waves-effect waves-light" href="{% url "oauth2_applications" %}">
       <i class="material-icons left">cancel</i>
       {% trans "Cancel" %}
     </a>
diff --git a/aleksis/core/templates/socialaccount/login.html b/aleksis/core/templates/socialaccount/login.html
new file mode 100644
index 0000000000000000000000000000000000000000..1630b01fa4104f563a92663bc7fd386a2abd5266
--- /dev/null
+++ b/aleksis/core/templates/socialaccount/login.html
@@ -0,0 +1,33 @@
+{% extends "core/base.html" %}
+
+{% load i18n material_form account %}
+
+{% block browser_title %}{% trans "Authorize" %}{% endblock %}
+{% block page_title %}{% trans "Authorize" %}{% endblock %}
+
+{% block content %}
+{% if process == "connect" %}
+    <p class="flow-text">
+       <i class="material-icons left">info</i>
+       {% blocktrans with provider.name as provider %}You are about to connect a new third party account from {{ provider }}.{% endblocktrans %}
+    </p>
+    <form method="post">
+      {% csrf_token %}
+      {% form form=form %}{% endform %}
+      {% trans "Confirm" as caption %}
+      {% include "core/partials/save_button.html" with caption=caption icon="how_to_reg" %}
+    </form>
+{% else %}
+    <p class="flow-text">
+       <i class="material-icons left small">info</i>
+       {% blocktrans with provider.name as provider %}You are about to sign in using a third party account from {{ provider }}.{% endblocktrans %}
+    </p>
+    <form method="post">
+      {% csrf_token %}
+      {% form form=form %}{% endform %}
+      {% trans "Continue" as caption %}
+      {% include "core/partials/save_button.html" with caption=caption icon="how_to_reg" %}
+    </form>
+
+{% endif %}
+{% endblock %}
diff --git a/aleksis/core/templates/socialaccount/snippets/provider_list.html b/aleksis/core/templates/socialaccount/snippets/provider_list.html
index 9d9a9c87712076d8954ebce192ea2ac38383cc48..706e81b877936c946c9498201d8af46e95ffdb2e 100644
--- a/aleksis/core/templates/socialaccount/snippets/provider_list.html
+++ b/aleksis/core/templates/socialaccount/snippets/provider_list.html
@@ -7,7 +7,7 @@
         {% for brand in provider.get_brands %}
             <a title="{{brand.name}}" 
               class="socialaccount_provider {{provider.id}} {{brand.id}}
-              btn-large waves-effect waves-light primary-color" 
+              btn-large waves-effect waves-light primary-color margin-bottom"
               href="{% provider_login_url provider.id openid=brand.openid_url process=process %}">
               {% blocktrans with name=brand.name %}
                 Login with {{ name }}
@@ -16,7 +16,7 @@
         {% endfor %}
       {% endif %}
         <a title="{{provider.name}}" class="socialaccount_provider {{provider.id}}
-        btn hundred-percent waves-effect waves-light primary-color" 
+        btn hundred-percent waves-effect waves-light primary-color margin-bottom"
           href="{% provider_login_url provider.id process=process scope=scope auth_params=auth_params %}">
           {% blocktrans with name=provider.name %}
             Login with {{ name }}
diff --git a/aleksis/core/templates/templated_email/base.email b/aleksis/core/templates/templated_email/base.email
new file mode 100644
index 0000000000000000000000000000000000000000..cce8e7a2fdfc6a005bec62715b8b7646e26b1cea
--- /dev/null
+++ b/aleksis/core/templates/templated_email/base.email
@@ -0,0 +1,21 @@
+{% load i18n %}
+
+{% block subject %}[{{ SITE_PREFERENCES.general__title }}] {% block subject_content %}{% endblock %}{% endblock %}
+
+{% block plain %}{% block plain_greeting %}{% trans "Hello" %},{% endblock %}
+{% block plain_content %}{% endblock %}
+- {{ SITE_PREFERENCES.general__title }}
+{% endblock %}
+
+{% block html %}
+  <style>
+    {% include "templated_email/email.css" %}
+  </style>
+  <body>
+    <div class="main">
+      {% block html_greeting %}<p>{% trans "Hello" %},</p>{% endblock %}
+      {% block html_content %}{% endblock %}
+      <p>- {{ SITE_PREFERENCES.general__title }}</p>
+    </div>
+  </body>
+{% endblock %}
diff --git a/aleksis/core/templates/templated_email/celery_failure.email b/aleksis/core/templates/templated_email/celery_failure.email
new file mode 100644
index 0000000000000000000000000000000000000000..8718b6f522dfdca868abfbaaae9a6e5032983846
--- /dev/null
+++ b/aleksis/core/templates/templated_email/celery_failure.email
@@ -0,0 +1,50 @@
+{% extends "templated_email/base.email" %}
+{% load i18n %}
+
+{% block subject_content %}{% blocktrans with task_name=task_name %}Celery task {{ task_name }} failed!{% endblocktrans %}{% endblock %}
+
+{% block plain_content %}
+{% blocktrans with task_name=task_name %}the celery task {{ task_name }} failed with following information:{% endblocktrans %}
+
+  * {% trans "Task" %}: {{ task_name }}
+  * {% trans "Task ID" %}: {{ task_id }}
+  * {% trans "Raised exception" %}: {{ exception }}
+  * {% trans "Positional arguments" %}:
+    {% for arg in args %}- {{ arg }}
+    {% endfor %}
+  * {% trans "Keyword arguments" %}:
+    {% for key, value in kwargs.items %}- {{ key }}: {{ value }}
+    {% endfor %}
+{{ traceback }}{% endblock %}
+
+{% block html_content %}
+  <p>
+    {% blocktrans with task_name=task_name %}
+      the celery task {{ task_name }} failed with following information:
+    {% endblocktrans %}
+  </p>
+
+  <ul>
+      <li>{% trans "Task" %}: {{ task_name }}</li>
+      <li>{% trans "Task ID" %}: {{ task_id }}</li>
+      <li>{% trans "Raised exception" %}: {{ exception }}</li>
+      <li>{% trans "Positional arguments" %}:
+        <ol>
+          {% for arg in args %}
+            <li>{{ arg }}</li>
+          {% endfor %}
+        </ol>
+      </li>
+      <li>{% trans "Keyword arguments" %}:
+        <ul>
+          {% for key, value in kwargs.items %}
+            <li>{{ key }}: {{ value }}</li>
+          {% endfor %}
+        </ul>
+      </li>
+  </ul>
+
+  <code>
+    {{ traceback|linebreaksbr }}
+  </code>
+{% endblock %}
diff --git a/aleksis/core/templates/templated_email/data_checks.css b/aleksis/core/templates/templated_email/data_checks.css
deleted file mode 100644
index b385b185ecfe66dcca070e8eb024bbb091917f04..0000000000000000000000000000000000000000
--- a/aleksis/core/templates/templated_email/data_checks.css
+++ /dev/null
@@ -1,16 +0,0 @@
-body {
-    line-height: 1.5;
-    font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen-Sans, Ubuntu, Cantarell, "Helvetica Neue", sans-serif;
-    font-weight: normal;
-    color: rgba(0, 0, 0, 0.87);
-}
-
-.count {
-    text-align: right;
-    font-family: monospace;
-    font-size: 14pt;
-}
-
-td, th {
-    padding: 10px;
-}
diff --git a/aleksis/core/templates/templated_email/data_checks.email b/aleksis/core/templates/templated_email/data_checks.email
index 0f9b5a5307fba9152683d4e03b0c41a2105629c7..1b3e04960596283327dbb5994fe6e8c6cae83b58 100644
--- a/aleksis/core/templates/templated_email/data_checks.email
+++ b/aleksis/core/templates/templated_email/data_checks.email
@@ -1,27 +1,16 @@
+{% extends "templated_email/base.email" %}
 {% load i18n %}
+{% block subject_content %}{% trans "The system detected some new problems with your data." %}{% endblock %}
 
-{% block subject %}
- {% trans "The system detected some new problems with your data." %}
-{% endblock %}
-
-{% block plain %}
- {% trans "Hello," %}
-
- {% blocktrans %}
-  the system detected some new problems with your data.
-  Please take some time to inspect them and solve the issues or mark them as ignored.
- {% endblocktrans %}
+{% block plain_content %}
+{% blocktrans %}the system detected some new problems with your data.
+Please take some time to inspect them and solve the issues or mark them as ignored.{% endblocktrans %}
 
- {% for result in results %}
-  {{ result.0.problem_name }}: {{ result.1 }}
- {% endfor %}
+{% for result in results %}- {{ result.0.problem_name }}: {{ result.1 }}
+{% endfor %}
 {% endblock %}
 
-{% block html %}
- <style>
-  {% include "templated_email/data_checks.css" %}
- </style>
- <p>{% trans "Hello," %}</p>
+{% block html_content %}
  <p>
   {% blocktrans %}
    the system detected some new problems with your data.
diff --git a/aleksis/core/templates/templated_email/email.css b/aleksis/core/templates/templated_email/email.css
new file mode 100644
index 0000000000000000000000000000000000000000..8cd112624c0c0b78f663027841895f4e07ce608b
--- /dev/null
+++ b/aleksis/core/templates/templated_email/email.css
@@ -0,0 +1,41 @@
+body {
+    line-height: 1.5;
+    font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen-Sans, Ubuntu, Cantarell, "Helvetica Neue", sans-serif;
+    font-weight: normal;
+    color: rgba(0, 0, 0, 0.87);
+    display: flex;
+    justify-content: center;
+    align-items: center;
+}
+
+table, tr {
+    width: 100%;
+}
+
+.main {
+    max-width: 700px;
+    box-shadow: 0 2px 2px 0 rgba(0, 0, 0, 0.14), 0 3px 1px -2px rgba(0, 0, 0, 0.12), 0 1px 5px 0 rgba(0, 0, 0, 0.2);
+    -webkit-box-shadow: 0 2px 2px 0 rgba(0, 0, 0, 0.14), 0 3px 1px -2px rgba(0, 0, 0, 0.12), 0 1px 5px 0 rgba(0, 0, 0, 0.2);
+    -webkit-transition: -webkit-box-shadow .25s;
+    transition: -webkit-box-shadow .25s;
+    transition: box-shadow .25s;
+    transition: box-shadow .25s, -webkit-box-shadow .25s;
+    border-radius: 2px;
+    background-color: #fff;
+    margin: 30px;
+    padding: 20px;
+}
+
+.first th {
+    border-bottom: 1px solid;
+}
+
+
+td, th {
+    padding-left: 5px;
+    padding-right: 5px;
+}
+
+.align-center {
+    text-align: center;
+}
diff --git a/aleksis/core/templates/templated_email/notification.email b/aleksis/core/templates/templated_email/notification.email
index 68a3e79570d1ba93013f2287b6d96cf8679a3572..e1290238f9713829640e6ebc518a618cc61d762f 100644
--- a/aleksis/core/templates/templated_email/notification.email
+++ b/aleksis/core/templates/templated_email/notification.email
@@ -1,31 +1,25 @@
+{% extends "templated_email/base.email" %}
 {% load i18n %}
 
-{% block subject %} {% trans "New notification for" %} {{ notification_user }} {% endblock %}
+{% block subject_content %}{% trans "New notification for" %} {{ notification_user }}{% endblock %}
 
-{% block plain %}
-    {% blocktrans with notification_user=notification_user %}Dear {{ notification_user }},{% endblocktrans %}
+{% block plain_greeting %}{% blocktrans with notification_user=notification_user %}Hello {{ notification_user }},{% endblocktrans %}{% endblock %}
 
-    {% trans "we got a new notification for you:" %}
+{% block plain_content %}
+{% trans "we got a new notification for you:" %}
 
-    {{ notification.title }}
+{{ notification.title }}
 
-    {{ notification.description }}
+{{ notification.description }}{% if notification.link %}
 
-    {% if notification.link %}
-        {% trans "More information" %} → {{ notification.link }}
-    {% endif %}
+{% trans "More information" %} → {{ notification.link }}{% endif %}
 
-    {% blocktrans with trans_sender=notification.sender trans_created_at=notification.created %}
-        Sent by {{ trans_sender }} at {{ trans_created_at }}
-    {% endblocktrans %}
-
-    {% trans "Your AlekSIS team" %}
+{% blocktrans with trans_sender=notification.sender trans_created_at=notification.created %}Sent by {{ trans_sender }} at {{ trans_created_at }}{% endblocktrans %}
 {% endblock %}
 
-{% block html %}
-<main>
-    <p>{% blocktrans with notification_user=notification_user %}Dear {{ notification_user }},{% endblocktrans %}</p>
+{% block html_greeting %}<p>{% blocktrans with notification_user=notification_user %}Hello {{ notification_user }},{% endblocktrans %}</p>{% endblock %}
 
+{% block html_content %}
     <p>{% trans "we got a new notification for you:" %}</p>
 
     <blockquote>
@@ -41,9 +35,4 @@
             Sent by {{ trans_sender }} at {{ trans_created_at }}
         {% endblocktrans %}
     </p>
-
-    <p>
-        <i>{% trans "Your AlekSIS team" %}</i>
-    </p>
-</main>
 {% endblock %}
diff --git a/aleksis/core/templates/templated_email/person_changed.email b/aleksis/core/templates/templated_email/person_changed.email
index 1e0d8be25684486bfdceac0b979b461253c21df7..e4cd967aebd4f3d9a0a46f5762fe86e1d019afba 100644
--- a/aleksis/core/templates/templated_email/person_changed.email
+++ b/aleksis/core/templates/templated_email/person_changed.email
@@ -1,23 +1,16 @@
+{% extends "templated_email/base.email" %}
 {% load i18n %}
 
-{% block subject %}
- {% blocktrans with person=person %}{{ person }} changed their data!{% endblocktrans %}
-{% endblock %}
-
-{% block plain %}
- {% trans "Hello," %}
+{% block subject_content %}{% blocktrans with person=person %}{{ person }} changed their data!{% endblocktrans %}{% endblock %}
 
- {% blocktrans with person=person %}
-   the person {{ person }} recently changed the following fields:
- {% endblocktrans %}
+{% block plain_content %}
+{% blocktrans with person=person %}the person {{ person }} recently changed the following fields:{% endblocktrans %}
 
- {% for field in changed_fields %}
-  * {{ field }}
- {% endfor %}
+{% for field in changed_fields %}* {{ field }}
+{% endfor %}
 {% endblock %}
 
 {% block html %}
- <p>{% trans "Hello," %}</p>
  <p>
   {% blocktrans with person=person %}
     the person {{ person }} recently changed the following fields:
diff --git a/aleksis/core/templates/two_factor/core/login.html b/aleksis/core/templates/two_factor/core/login.html
index 9ea4bc6c24b7d9e764742bbddf75ceb86b785868..77b135569d797181a45bd49d7fd7e9cded4e9a5c 100644
--- a/aleksis/core/templates/two_factor/core/login.html
+++ b/aleksis/core/templates/two_factor/core/login.html
@@ -16,7 +16,17 @@
       <div class="col s12 m10 l8 xl6">
         <div class="card">
           <div class="card-content">
-            {% if wizard.steps.current == 'auth' and socialaccount_providers %}
+            {% if oauth and oauth_application.icon %}
+              <div class="center-via-flex margin-bottom">
+                <div class="application-circle materialboxed z-depth-2">
+                  <img src="{{ oauth_application.icon.url }}" alt="{{ oauth_application.name }}"
+                       class="hundred-percent">
+                </div>
+              </div>
+              <div class="card-title center">
+                {% blocktrans with name=oauth_application.name %}Login for {{ name }}{% endblocktrans %}
+              </div>
+            {% elif wizard.steps.current == 'auth' and socialaccount_providers %}
               <div class="card-title">{% trans "Login with username and password" %}</div>
             {% else %}
               <div class="card-title">{% trans "Login" %}</div>
@@ -30,12 +40,21 @@
                 </p>
               </div>
             {% elif wizard.steps.current == 'auth' %}
-              <div class="alert primary">
-                <p>
-                  <i class="material-icons left">info</i>
-                  {% blocktrans %}Please login to see this page.{% endblocktrans %}
-                </p>
-              </div>
+              {% if oauth %}
+                <div class="alert primary">
+                  <p>
+                    <i class="material-icons left">info</i>
+                    {% blocktrans %}Please login with your account to use the external application.{% endblocktrans %}
+                  </p>
+                </div>
+              {% else %}
+                <div class="alert primary">
+                  <p>
+                    <i class="material-icons left">info</i>
+                    {% blocktrans %}Please login to see this page.{% endblocktrans %}
+                  </p>
+                </div>
+              {% endif %}
             {% endif %}
             {% if not wizard.steps.current == "auth" %}
               <div class="alert primary">
@@ -71,13 +90,13 @@
 
             {% include "two_factor/_wizard_forms.html" %}
           </div>
-          <div class="card-action-light">
+          <div class="card-action-light login-card-action">
             <button type="submit" class="btn green waves-effect waves-light">
               {% trans "Login" %}
               <i class="material-icons right">send</i>
             </button>
-            {% if request.site.preferences.auth__allow_password_change and wizard.steps.current == "auth" %}
-              <a href="{% url "account_reset_password" %}" class="btn-flat right waves-effect waves-light">
+            {% if request.site.preferences.auth__allow_password_reset and wizard.steps.current == "auth" %}
+              <a href="{% url "account_reset_password" %}" class="btn-flat right waves-effect waves-red">
                 {% trans "Reset password" %}
               </a>
             {% endif %}
@@ -92,7 +111,8 @@
                 <p>{% trans "Or, alternatively, use one of your backup phones:" %}</p>
                 <p>
                   {% for other in other_devices %}
-                    <button name="challenge_device" value="{{ other.persistent_id }}" class="btn" type="submit">
+                    <button name="challenge_device" value="{{ other.persistent_id }}" class="btn margin-bottom"
+                            type="submit">
                       {{ other|device_action }}
                     </button>
                   {% endfor %}
diff --git a/aleksis/core/tests/models/test_group.py b/aleksis/core/tests/models/test_group.py
new file mode 100644
index 0000000000000000000000000000000000000000..8849a842d043b6bd3c91d04393cf5ccc695d2cf3
--- /dev/null
+++ b/aleksis/core/tests/models/test_group.py
@@ -0,0 +1,163 @@
+import pytest
+
+from aleksis.core.models import Group, Person
+
+pytestmark = pytest.mark.django_db
+
+
+def test_child_groups_recursive():
+    g_1st_grade = Group.objects.create(name="1st grade")
+    g_1a = Group.objects.create(name="1a")
+    g_1b = Group.objects.create(name="1b")
+    g_2nd_grade = Group.objects.create(name="2nd grade")
+    g_2a = Group.objects.create(name="2a")
+    g_2b = Group.objects.create(name="2b")
+    g_2c = Group.objects.create(name="2c")
+    g_2nd_grade_french = Group.objects.create(name="2nd grade French")
+
+    g_1a.parent_groups.set([g_1st_grade])
+    g_1b.parent_groups.set([g_1st_grade])
+    g_2a.parent_groups.set([g_2nd_grade])
+    g_2b.parent_groups.set([g_2nd_grade])
+    g_2c.parent_groups.set([g_2nd_grade])
+    g_2nd_grade_french.parent_groups.set([g_2b, g_2c])
+
+    assert g_2nd_grade_french in g_2nd_grade.child_groups_recursive
+    assert g_2nd_grade_french in g_2b.child_groups_recursive
+    assert g_2nd_grade_french in g_2c.child_groups_recursive
+    assert g_2nd_grade_french not in g_2a.child_groups_recursive
+    assert g_2nd_grade_french not in g_1st_grade.child_groups_recursive
+
+
+def test_parent_groups_recursive():
+    g_1st_grade = Group.objects.create(name="1st grade")
+    g_1a = Group.objects.create(name="1a")
+    g_1b = Group.objects.create(name="1b")
+    g_2nd_grade = Group.objects.create(name="2nd grade")
+    g_2a = Group.objects.create(name="2a")
+    g_2b = Group.objects.create(name="2b")
+    g_2c = Group.objects.create(name="2c")
+    g_2nd_grade_french = Group.objects.create(name="2nd grade French")
+
+    g_1a.parent_groups.set([g_1st_grade])
+    g_1b.parent_groups.set([g_1st_grade])
+    g_2a.parent_groups.set([g_2nd_grade])
+    g_2b.parent_groups.set([g_2nd_grade])
+    g_2c.parent_groups.set([g_2nd_grade])
+    g_2nd_grade_french.parent_groups.set([g_2b, g_2c])
+
+    assert g_1st_grade in g_1a.parent_groups_recursive
+    assert g_2nd_grade in g_2a.parent_groups_recursive
+    assert g_2nd_grade in g_2nd_grade_french.parent_groups_recursive
+    assert g_1st_grade not in g_2nd_grade_french.parent_groups_recursive
+
+
+def test_members_recursive():
+    g_2nd_grade = Group.objects.create(name="2nd grade")
+    g_2a = Group.objects.create(name="2a")
+    g_2b = Group.objects.create(name="2b")
+    g_2c = Group.objects.create(name="2c")
+    g_2nd_grade_french = Group.objects.create(name="2nd grade French")
+
+    g_2a.parent_groups.set([g_2nd_grade])
+    g_2b.parent_groups.set([g_2nd_grade])
+    g_2c.parent_groups.set([g_2nd_grade])
+    g_2nd_grade_french.parent_groups.set([g_2b, g_2c])
+
+    p_2a_1 = Person.objects.create(first_name="A", last_name="B")
+    p_2a_2 = Person.objects.create(first_name="A", last_name="B")
+    p_2b_1 = Person.objects.create(first_name="A", last_name="B")
+    p_2b_2 = Person.objects.create(first_name="A", last_name="B")
+    p_2c_1 = Person.objects.create(first_name="A", last_name="B")
+    p_2c_2 = Person.objects.create(first_name="A", last_name="B")
+    p_french_only = Person.objects.create(first_name="A", last_name="B")
+
+    g_2a.members.set([p_2a_1, p_2a_2])
+    g_2b.members.set([p_2b_1, p_2b_2])
+    g_2c.members.set([p_2c_1, p_2c_2])
+    g_2nd_grade_french.members.set([p_2b_1, p_2c_1, p_french_only])
+
+    assert p_2a_1 in g_2nd_grade.members_recursive
+    assert p_2a_2 in g_2nd_grade.members_recursive
+    assert p_2b_1 in g_2nd_grade.members_recursive
+    assert p_2b_2 in g_2nd_grade.members_recursive
+    assert p_2c_1 in g_2nd_grade.members_recursive
+    assert p_2c_2 in g_2nd_grade.members_recursive
+    assert p_french_only in g_2nd_grade.members_recursive
+    assert p_french_only in g_2b.members_recursive
+    assert p_french_only in g_2c.members_recursive
+    assert p_french_only not in g_2a.members_recursive
+
+
+def test_member_of_recursive():
+    g_2nd_grade = Group.objects.create(name="2nd grade")
+    g_2a = Group.objects.create(name="2a")
+    g_2b = Group.objects.create(name="2b")
+    g_2c = Group.objects.create(name="2c")
+    g_2nd_grade_french = Group.objects.create(name="2nd grade French")
+
+    g_2a.parent_groups.set([g_2nd_grade])
+    g_2b.parent_groups.set([g_2nd_grade])
+    g_2c.parent_groups.set([g_2nd_grade])
+    g_2nd_grade_french.parent_groups.set([g_2b, g_2c])
+
+    p_2a_1 = Person.objects.create(first_name="A", last_name="B")
+    p_2a_2 = Person.objects.create(first_name="A", last_name="B")
+    p_2b_1 = Person.objects.create(first_name="A", last_name="B")
+    p_2b_2 = Person.objects.create(first_name="A", last_name="B")
+    p_2c_1 = Person.objects.create(first_name="A", last_name="B")
+    p_2c_2 = Person.objects.create(first_name="A", last_name="B")
+    p_french_only = Person.objects.create(first_name="A", last_name="B")
+
+    g_2a.members.set([p_2a_1, p_2a_2])
+    g_2b.members.set([p_2b_1, p_2b_2])
+    g_2c.members.set([p_2c_1, p_2c_2])
+    g_2nd_grade_french.members.set([p_2b_1, p_2c_1, p_french_only])
+
+    assert g_2nd_grade in p_2a_1.member_of_recursive
+    assert g_2nd_grade in p_2a_2.member_of_recursive
+    assert g_2nd_grade in p_2b_1.member_of_recursive
+    assert g_2nd_grade in p_2b_2.member_of_recursive
+    assert g_2nd_grade in p_2c_1.member_of_recursive
+    assert g_2nd_grade in p_2c_2.member_of_recursive
+    assert g_2nd_grade in p_french_only.member_of_recursive
+    assert g_2b in p_french_only.member_of_recursive
+    assert g_2c in p_french_only.member_of_recursive
+
+
+def test_owners_recursive():
+    g_2nd_grade = Group.objects.create(name="2nd grade")
+    g_2a = Group.objects.create(name="2a")
+    g_2b = Group.objects.create(name="2b")
+
+    g_2a.parent_groups.set([g_2nd_grade])
+    g_2b.parent_groups.set([g_2nd_grade])
+
+    p_1 = Person.objects.create(first_name="A", last_name="B")
+    p_2 = Person.objects.create(first_name="A", last_name="B")
+
+    g_2nd_grade.owners.set([p_1])
+
+    assert p_1 in g_2a.owners_recursive
+    assert p_1 in g_2b.owners_recursive
+    assert p_2 not in g_2a.owners_recursive
+    assert p_2 not in g_2b.owners_recursive
+
+
+def test_owner_of_recursive():
+    g_2nd_grade = Group.objects.create(name="2nd grade")
+    g_2a = Group.objects.create(name="2a")
+    g_2b = Group.objects.create(name="2b")
+
+    g_2a.parent_groups.set([g_2nd_grade])
+    g_2b.parent_groups.set([g_2nd_grade])
+
+    p_1 = Person.objects.create(first_name="A", last_name="B")
+    p_2 = Person.objects.create(first_name="A", last_name="B")
+
+    g_2nd_grade.owners.set([p_1])
+
+    assert g_2a in p_1.owner_of_recursive.all()
+    assert g_2b in p_1.owner_of_recursive.all()
+    assert g_2a not in p_2.owner_of_recursive.all()
+    assert g_2b not in p_2.owner_of_recursive.all()
diff --git a/aleksis/core/tests/models/test_notification.py b/aleksis/core/tests/models/test_notification.py
index 1b1a6df054fe24f06bd363e825df3f1259af53e8..cdb01a04b99a398469ddfcd4466101c0b2d79f34 100644
--- a/aleksis/core/tests/models/test_notification.py
+++ b/aleksis/core/tests/models/test_notification.py
@@ -1,11 +1,81 @@
+from datetime import timedelta
+from time import sleep
+from unittest.mock import patch
+
+from django.test import override_settings
+from django.utils import timezone
+
 import pytest
+from freezegun import freeze_time
 
 from aleksis.core.models import Notification, Person
+from aleksis.core.util.notifications import _send_due_notifications
 
 pytestmark = pytest.mark.django_db
 
 
-def test_email_notification(mailoutbox):
+def test_send_notification():
+    email = "doe@example.com"
+    recipient = Person.objects.create(first_name="Jane", last_name="Doe", email=email)
+
+    sender = "Foo"
+    title = "There is happened something."
+    description = "Here you get some more information."
+    link = "https://aleksis.org/"
+
+    notification = Notification(
+        sender=sender,
+        recipient=recipient,
+        title=title,
+        description=description,
+        link=link,
+    )
+
+    with patch("aleksis.core.models.Notification.send") as patched_send:
+        patched_send.assert_not_called()
+
+        notification.save()
+
+        patched_send.assert_called()
+
+
+def test_send_scheduled_notification():
+    email = "doe@example.com"
+    recipient = Person.objects.create(first_name="Jane", last_name="Doe", email=email)
+
+    sender = "Foo"
+    title = "There is happened something."
+    description = "Here you get some more information."
+    link = "https://aleksis.org/"
+
+    notification = Notification(
+        sender=sender,
+        recipient=recipient,
+        title=title,
+        description=description,
+        link=link,
+        send_at=timezone.now() + timedelta(days=1),
+    )
+    notification.save()
+
+    with patch("aleksis.core.models.Notification.send") as patched_send:
+        patched_send.assert_not_called()
+
+        _send_due_notifications()
+
+        patched_send.assert_not_called()
+
+        with freeze_time(timezone.now() + timedelta(days=1)):
+            _send_due_notifications()
+
+        patched_send.assert_called()
+
+
+@override_settings(CELERY_BROKER_URL="memory://localhost//")
+@pytest.mark.django_db(
+    databases=["default", "default_oot"], serialized_rollback=True, transaction=True
+)
+def test_email_notification(mailoutbox, celery_worker):
     email = "doe@example.com"
     recipient = Person.objects.create(first_name="Jane", last_name="Doe", email=email)
 
@@ -19,6 +89,9 @@ def test_email_notification(mailoutbox):
     )
     notification.save()
 
+    sleep(3)
+
+    notification.refresh_from_db()
     assert notification.sent
 
     assert len(mailoutbox) == 1
diff --git a/aleksis/core/tests/models/test_pdffile.py b/aleksis/core/tests/models/test_pdffile.py
index 1f1e936a8c72a6dca3c4796a18a69b9370adbb0a..d18e621d77043b3adafe93e5b18a04355950cfad 100644
--- a/aleksis/core/tests/models/test_pdffile.py
+++ b/aleksis/core/tests/models/test_pdffile.py
@@ -18,6 +18,7 @@ from aleksis.core.util.pdf import clean_up_expired_pdf_files
 pytestmark = pytest.mark.django_db
 
 
+@pytest.mark.skip
 @pytest.mark.usefixtures("celery_worker")
 @override_settings(CELERY_BROKER_URL="memory://localhost//")
 class PDFFIleTest(TransactionTestCase):
diff --git a/aleksis/core/tests/regression/test_regression.py b/aleksis/core/tests/regression/test_regression.py
new file mode 100644
index 0000000000000000000000000000000000000000..b9d33e6981312817324b92d4fe67176b96763cda
--- /dev/null
+++ b/aleksis/core/tests/regression/test_regression.py
@@ -0,0 +1,84 @@
+from django.contrib.auth import get_user_model
+
+import pytest
+
+from aleksis.core.models import Group, Person
+
+pytestmark = pytest.mark.django_db
+
+
+def test_all_settigns_registered():
+    """Tests for regressions of preferences not being registered.
+
+    https://edugit.org/AlekSIS/official/AlekSIS-Core/-/issues/592
+    """
+
+    from dynamic_preferences.types import BasePreferenceType
+
+    from aleksis.core import preferences
+    from aleksis.core.preferences import person_preferences_registry, site_preferences_registry
+
+    for obj in preferences.__dict__.values():
+        if not isinstance(obj, BasePreferenceType):
+            continue
+
+        in_site_reg = site_preferences_registry.get(obj.section.name, {}).get(obj.name, None) is obj
+        in_person_reg = (
+            person_preferences_registry.get(obj.section.name, {}).get(obj.name, None) is obj
+        )
+
+        assert in_site_reg != in_person_reg
+
+
+def test_custom_managers_return_correct_qs():
+    """Tests that custom managers' get_queryset methods return the expected qs.
+
+    https://edugit.org/AlekSIS/official/AlekSIS-Core/-/issues/594
+    """
+
+    from aleksis.core import managers
+
+    def _check_get_queryset(Manager, QuerySet):
+        assert isinstance(Manager.from_queryset(QuerySet)().get_queryset(), QuerySet)
+
+    _check_get_queryset(managers.GroupManager, managers.GroupQuerySet)
+
+
+def test_reassign_user_to_person():
+    """Tests that on re-assigning a user, groups are correctly synced.
+
+    https://edugit.org/AlekSIS/official/AlekSIS-Core/-/issues/628
+    """
+
+    User = get_user_model()
+
+    group1 = Group.objects.create(name="Group 1")
+    group2 = Group.objects.create(name="Group 2")
+
+    user1 = User.objects.create(username="user1")
+    user2 = User.objects.create(username="user2")
+
+    person1 = Person.objects.create(first_name="Person", last_name="1", user=user1)
+    person2 = Person.objects.create(first_name="Person", last_name="2", user=user2)
+
+    person1.member_of.set([group1])
+    person2.member_of.set([group2])
+
+    assert user1.groups.count() == 1
+    assert user2.groups.count() == 1
+    assert user1.groups.first().name == "Group 1"
+    assert user2.groups.first().name == "Group 2"
+
+    person1.user = None
+    person1.save()
+    assert user1.groups.count() == 0
+
+    person2.user = user1
+    person2.save()
+    person1.user = user2
+    person1.save()
+
+    assert user1.groups.count() == 1
+    assert user2.groups.count() == 1
+    assert user1.groups.first().name == "Group 2"
+    assert user2.groups.first().name == "Group 1"
diff --git a/aleksis/core/tests/views/test_account.py b/aleksis/core/tests/views/test_account.py
index 28686eabf8290a7a72444d8fe5f4b71bb74f4626..ae598ab72826d403e94531ef2bf7194b4ee5225c 100644
--- a/aleksis/core/tests/views/test_account.py
+++ b/aleksis/core/tests/views/test_account.py
@@ -1,10 +1,23 @@
 from django.conf import settings
+from django.test import override_settings
 from django.urls import reverse
 
+import ldap
 import pytest
+from django_auth_ldap.config import LDAPSearch
+
+from aleksis.core.models import UserAdditionalAttributes
 
 pytestmark = pytest.mark.django_db
 
+LDAP_BASE = "dc=example,dc=com"
+LDAP_SETTINGS = {
+    "AUTH_LDAP_GLOBAL_OPTIONS": {
+        ldap.OPT_NETWORK_TIMEOUT: 1,
+    },
+    "AUTH_LDAP_USER_SEARCH": LDAPSearch(LDAP_BASE, ldap.SCOPE_SUBTREE),
+}
+
 
 def test_index_not_logged_in(client):
     response = client.get("/")
@@ -40,3 +53,34 @@ def test_logout(client, django_user_model):
 
     assert response.status_code == 200
     assert "Please login to see this page." in response.content.decode("utf-8")
+
+
+@override_settings(
+    AUTHENTICATION_BACKENDS=[
+        "aleksis.core.util.ldap.LDAPBackend",
+        "django.contrib.auth.backends.ModelBackend",
+    ],
+    AUTH_LDAP_SERVER_URI="ldap://[100::0]",
+    AUTH_LDAP_SET_USABLE_PASSWORD=True,
+    **LDAP_SETTINGS
+)
+def test_login_ldap_fail_if_previously_ldap_authenticated(client, django_user_model):
+    username = "foo"
+    password = "bar"
+
+    django_user_model.objects.create_user(username=username, password=password)
+
+    # Logging in with a fresh account should success
+    res = client.login(username=username, password=password)
+    assert res
+    client.get(reverse("logout"), follow=True)
+
+    # Logging in with a previously LDAP-authenticated account should fail
+    UserAdditionalAttributes.set_user_attribute(username, "ldap_authenticated", True)
+    res = client.login(username=username, password=password)
+    assert not res
+
+    # Explicitly noting account has not been used with LDAP should succeed
+    UserAdditionalAttributes.set_user_attribute(username, "ldap_authenticated", False)
+    res = client.login(username=username, password=password)
+    assert res
diff --git a/aleksis/core/urls.py b/aleksis/core/urls.py
index 85f91a2ccd5aa2f0f11f5d905a1d8ed5d5a61a1e..0795dfa93f3aaf106abe7db43a3af2371b566b10 100644
--- a/aleksis/core/urls.py
+++ b/aleksis/core/urls.py
@@ -23,13 +23,30 @@ urlpatterns = [
     path("serviceworker.js", views.ServiceWorkerView.as_view(), name="service_worker"),
     path("offline/", views.OfflineView.as_view(), name="offline"),
     path("about/", views.about, name="about_aleksis"),
+    path("accounts/signup/", views.AccountRegisterView.as_view(), name="account_signup"),
     path("accounts/logout/", auth_views.LogoutView.as_view(), name="logout"),
     path(
         "accounts/password/change/",
         views.CustomPasswordChangeView.as_view(),
         name="account_change_password",
     ),
+    path(
+        "accounts/password/reset/",
+        views.CustomPasswordResetView.as_view(),
+        name="account_reset_password",
+    ),
     path("accounts/", include("allauth.urls")),
+    path("invitations/send-invite", views.InvitePerson.as_view(), name="invite_person"),
+    path(
+        "invitations/code/enter", views.EnterInvitationCode.as_view(), name="enter_invitation_code"
+    ),
+    path(
+        "invitations/code/generate",
+        views.GenerateInvitationCode.as_view(),
+        name="generate_invitation_code",
+    ),
+    path("invitations/disabled", views.InviteDisabledView.as_view(), name="invite_disabled"),
+    path("invitations/", include("invitations.urls")),
     path(
         "accounts/social/connections/<int:pk>/delete",
         views.SocialAccountDeleteView.as_view(),
@@ -39,6 +56,7 @@ urlpatterns = [
     path("admin/uwsgi/", include("django_uwsgi.urls")),
     path("data_management/", views.data_management, name="data_management"),
     path("status/", views.SystemStatus.as_view(), name="system_status"),
+    path("account/login/", views.LoginView.as_view(), name="login"),
     path("", include(tf_urls)),
     path("celery_progress/<str:task_id>/", views.CeleryProgressView.as_view(), name="task_status"),
     path("accounts/logout/", auth_views.LogoutView.as_view(), name="logout"),
@@ -51,6 +69,7 @@ urlpatterns = [
     path("person/<int:id_>/", views.person, name="person_by_id"),
     path("person/<int:pk>/edit/", views.EditPersonView.as_view(), name="edit_person_by_id"),
     path("person/<int:id_>/delete/", views.delete_person, name="delete_person_by_id"),
+    path("person/<int:pk>/invite/", views.InvitePersonByID.as_view(), name="invite_person_by_id"),
     path("groups", views.groups, name="groups"),
     path("groups/additional_fields", views.additional_fields, name="additional_fields"),
     path("groups/child_groups/", views.groups_child_groups, name="groups_child_groups"),
@@ -102,18 +121,29 @@ urlpatterns = [
         ConnectDiscoveryInfoView.as_view(),
         name="oidc_configuration",
     ),
-    path("oauth/applications/", views.OAuth2List.as_view(), name="oauth_list"),
+    path("oauth/applications/", views.OAuth2ListView.as_view(), name="oauth2_applications"),
     path(
         "oauth/applications/register/",
         views.OAuth2RegisterView.as_view(),
         name="register_oauth_application",
     ),
-    path("oauth/applications/<int:pk>/detail", views.OAuth2Detail.as_view(), name="oauth_detail"),
-    path("oauth/applications/<int:pk>/delete", views.OAuth2Delete.as_view(), name="oauth_delete"),
+    path(
+        "oauth/applications/<int:pk>/", views.OAuth2DetailView.as_view(), name="oauth2_application"
+    ),
+    path(
+        "oauth/applications/<int:pk>/delete/",
+        views.OAuth2DeleteView.as_view(),
+        name="delete_oauth2_application",
+    ),
     path(
         "oauth/applications/<int:pk>/edit/",
         views.OAuth2EditView.as_view(),
-        name="edit_oauth_application",
+        name="edit_oauth2_application",
+    ),
+    path(
+        "oauth/authorize/",
+        views.CustomAuthorizationView.as_view(),
+        name="oauth2_provider:authorize",
     ),
     path("oauth/", include("oauth2_provider.urls", namespace="oauth2_provider")),
     path("__i18n__/", include("django.conf.urls.i18n")),
@@ -202,8 +232,16 @@ urlpatterns = [
     ),
     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(
+        "data_check/",
+        views.DataCheckView.as_view(),
+        name="check_data",
+    ),
+    path(
+        "data_check/run/",
+        views.RunDataChecks.as_view(),
+        name="data_check_run",
+    ),
     path(
         "data_check/<int:pk>/<str:solve_option>/",
         views.SolveDataCheckView.as_view(),
@@ -231,6 +269,56 @@ urlpatterns = [
         {"default": True},
         name="edit_default_dashboard",
     ),
+    path(
+        "permissions/global/user/",
+        views.UserGlobalPermissionsListBaseView.as_view(),
+        name="manage_user_global_permissions",
+    ),
+    path(
+        "permissions/global/group/",
+        views.GroupGlobalPermissionsListBaseView.as_view(),
+        name="manage_group_global_permissions",
+    ),
+    path(
+        "permissions/object/user/",
+        views.UserObjectPermissionsListBaseView.as_view(),
+        name="manage_user_object_permissions",
+    ),
+    path(
+        "permissions/object/group/",
+        views.GroupObjectPermissionsListBaseView.as_view(),
+        name="manage_group_object_permissions",
+    ),
+    path(
+        "permissions/global/user/<int:pk>/delete/",
+        views.UserGlobalPermissionDeleteView.as_view(),
+        name="delete_user_global_permission",
+    ),
+    path(
+        "permissions/global/group/<int:pk>/delete/",
+        views.GroupGlobalPermissionDeleteView.as_view(),
+        name="delete_group_global_permission",
+    ),
+    path(
+        "permissions/object/user/<int:pk>/delete/",
+        views.UserObjectPermissionDeleteView.as_view(),
+        name="delete_user_object_permission",
+    ),
+    path(
+        "permissions/object/group/<int:pk>/delete/",
+        views.GroupObjectPermissionDeleteView.as_view(),
+        name="delete_group_object_permission",
+    ),
+    path(
+        "permissions/assign/",
+        views.SelectPermissionForAssignView.as_view(),
+        name="select_permission_for_assign",
+    ),
+    path(
+        "permissions/<int:pk>/assign/",
+        views.AssignPermissionView.as_view(),
+        name="assign_permission",
+    ),
     path("pdfs/<int:pk>/", views.RedirectToPDFFile.as_view(), name="redirect_to_pdf_file"),
 ]
 
diff --git a/aleksis/core/util/apps.py b/aleksis/core/util/apps.py
index 8157f1dcf2668aac249e107b3e37c1b3676ebeed..5b3898dd835506ade932ebccf30e4cf0de15486e 100644
--- a/aleksis/core/util/apps.py
+++ b/aleksis/core/util/apps.py
@@ -8,6 +8,7 @@ from django.http import HttpRequest
 
 from dynamic_preferences.signals import preference_updated
 from license_expression import Licensing
+from oauthlib.common import Request as OauthlibRequest
 from spdx_license_list import LICENSES
 
 from .core_helpers import copyright_years
@@ -244,6 +245,11 @@ class AppConfig(django.apps.AppConfig):
         """Return a list of all OAuth scopes to always include for this request and application."""
         return []
 
+    @classmethod
+    def get_additional_claims(cls, scopes: list[str], request: OauthlibRequest) -> dict[str, Any]:
+        """Get claim data for requested scopes."""
+        return {}
+
     def _maintain_default_data(self):
         from django.contrib.auth.models import Permission
         from django.contrib.contenttypes.models import ContentType
@@ -260,5 +266,7 @@ class AppConfig(django.apps.AppConfig):
                 ct = ContentType.objects.get_for_model(model)
                 for perm, verbose_name in model.extra_permissions:
                     Permission.objects.get_or_create(
-                        codename=perm, content_type=ct, defaults={"name": verbose_name},
+                        codename=perm,
+                        content_type=ct,
+                        defaults={"name": verbose_name},
                     )
diff --git a/aleksis/core/util/auth_helpers.py b/aleksis/core/util/auth_helpers.py
index 4f6e403cb678ff4a39f0199b0670ff6b3d8cdb45..a27910250bf5386e4a24344e2a997d1fc53a7610 100644
--- a/aleksis/core/util/auth_helpers.py
+++ b/aleksis/core/util/auth_helpers.py
@@ -1,18 +1,24 @@
 """Helpers/overrides for django-allauth."""
 
-from typing import Optional
+from typing import Any, Optional
 
 from django.conf import settings
+from django.contrib.auth.validators import ASCIIUsernameValidator
+from django.core.validators import RegexValidator
 from django.http import HttpRequest
 
-from allauth.account.adapter import DefaultAccountAdapter
 from allauth.socialaccount.adapter import DefaultSocialAccountAdapter
+from invitations.models import InvitationsAdapter
 from oauth2_provider.models import AbstractApplication
 from oauth2_provider.oauth2_validators import OAuth2Validator
 from oauth2_provider.scopes import BaseScopes
+from oauth2_provider.views.mixins import (
+    ClientProtectedResourceMixin as _ClientProtectedResourceMixin,
+)
+from oauthlib.common import Request as OauthlibRequest
 
 from .apps import AppConfig
-from .core_helpers import get_site_preferences, has_person
+from .core_helpers import get_site_preferences
 
 
 class OurSocialAccountAdapter(DefaultSocialAccountAdapter):
@@ -31,7 +37,7 @@ class OurSocialAccountAdapter(DefaultSocialAccountAdapter):
         return super().validate_disconnect(account, accounts)
 
 
-class OurAccountAdapter(DefaultAccountAdapter):
+class OurAccountAdapter(InvitationsAdapter):
     """Customised adapter to allow to disable signup."""
 
     def is_open_for_signup(self, request):
@@ -39,46 +45,16 @@ class OurAccountAdapter(DefaultAccountAdapter):
 
 
 class CustomOAuth2Validator(OAuth2Validator):
-    def get_additional_claims(self, request):
-        django_request = HttpRequest()
-        django_request.META = request.headers
-
-        claims = {
-            "preferred_username": request.user.username,
-        }
-
-        if "profile" in request.scopes:
-            if has_person(request.user):
-                claims["given_name"] = request.user.person.first_name
-                claims["family_name"] = request.user.person.last_name
-                claims["profile"] = django_request.build_absolute_uri(
-                    request.user.person.get_absolute_url()
-                )
-                if request.user.person.photo:
-                    claims["picture"] = django_request.build_absolute_uri(
-                        request.user.person.photo.url
-                    )
-            else:
-                claims["given_name"] = request.user.first_name
-                claims["family_name"] = request.user.last_name
-
-        if "email" in request.scopes:
-            if has_person(request.user):
-                claims["email"] = request.user.person.email
-            else:
-                claims["email"] = request.user.email
-
-        if "address" in request.scopes and has_person(request.user):
-            claims["address"] = {
-                "street_address": request.user.person.street
-                + " "
-                + request.user.person.housenumber,
-                "locality": request.user.person.place,
-                "postal_code": request.user.person.postal_code,
-            }
-
-        if "groups" in request.scopes and has_person(request.user):
-            claims["groups"] = request.user.person.groups.values_list("name", flat=True).all()
+    def get_additional_claims(self, request: OauthlibRequest) -> dict[str, Any]:
+        # Pull together scopes from request and from access token
+        scopes = request.scopes.copy()
+        if request.access_token:
+            scopes += request.access_token.scope.split(" ")
+
+        claims = {}
+        # Pull together claim data from all apps
+        for app in AppConfig.__subclasses__():
+            claims.update(app.get_additional_claims(scopes, request))
 
         return claims
 
@@ -106,6 +82,9 @@ class AppScopes(BaseScopes):
         scopes = []
         for app in AppConfig.__subclasses__():
             scopes += app.get_available_scopes()
+        # Filter by allowed scopes of requesting application
+        if application and application.allowed_scopes:
+            scopes = list(filter(lambda scope: scope in application.allowed_scopes, scopes))
         return scopes
 
     def get_default_scopes(
@@ -118,4 +97,51 @@ class AppScopes(BaseScopes):
         scopes = []
         for app in AppConfig.__subclasses__():
             scopes += app.get_default_scopes()
+        # Filter by allowed scopes of requesting application
+        if application and application.allowed_scopes:
+            scopes = list(filter(lambda scope: scope in application.allowed_scopes, scopes))
         return scopes
+
+
+class ClientProtectedResourceMixin(_ClientProtectedResourceMixin):
+    """Mixin for protecting resources with client authentication as mentioned in rfc:`3.2.1`.
+
+    This involves authenticating with any of: HTTP Basic Auth, Client Credentials and
+    Access token in that order. Breaks off after first validation.
+
+    This sub-class extends the functionality of Django OAuth Toolkit's mixin with support
+    for AlekSIS's `allowed_scopes` feature. For applications that have configured allowed
+    scopes, the required scopes for the view are checked to be a subset of the application's
+    allowed scopes (best to be combined with ScopedResourceMixin).
+    """
+
+    def authenticate_client(self, request: HttpRequest) -> bool:
+        """Return a boolean representing if client is authenticated with client credentials.
+
+        If the view has configured required scopes, they are verified against the application's
+        allowed scopes.
+        """
+        # Build an OAuth request so we can handle client information
+        core = self.get_oauthlib_core()
+        uri, http_method, body, headers = core._extract_params(request)
+        oauth_request = OauthlibRequest(uri, http_method, body, headers)
+
+        # Verify general authentication of the client
+        if not core.server.request_validator.authenticate_client(oauth_request):
+            # Client credentials were invalid
+            return False
+
+        # Verify scopes of configured application
+        # The OAuth request was enriched with a reference to the Application when using the
+        #  validator above.
+        required_scopes = set(self.get_scopes() or [])
+        allowed_scopes = set(AppScopes().get_available_scopes(oauth_request.client) or [])
+        return required_scopes.issubset(allowed_scopes)
+
+
+def validate_username_preference_regex(value: str):
+    regex = get_site_preferences()["auth__allowed_username_regex"]
+    return RegexValidator(regex)(value)
+
+
+custom_username_validators = [validate_username_preference_regex, ASCIIUsernameValidator()]
diff --git a/aleksis/core/util/core_helpers.py b/aleksis/core/util/core_helpers.py
index 075636b4190785965f2710d66abd9dd9936ef481..55777f72314589ba90fb93b92dc696cd463aa070 100644
--- a/aleksis/core/util/core_helpers.py
+++ b/aleksis/core/util/core_helpers.py
@@ -4,7 +4,7 @@ from importlib import import_module, metadata
 from itertools import groupby
 from logging import Filter
 from operator import itemgetter
-from typing import Any, Callable, Optional, Sequence, Union
+from typing import Any, Callable, Dict, Optional, Sequence, Union
 from warnings import warn
 
 from django.conf import settings
@@ -14,8 +14,12 @@ from django.db.models import Model, QuerySet
 from django.http import HttpRequest
 from django.shortcuts import get_object_or_404
 from django.utils import timezone
+from django.utils.crypto import get_random_string
 from django.utils.functional import lazy
+from django.utils.module_loading import import_string
 
+from cachalot.api import invalidate
+from cachalot.signals import post_invalidation
 from cache_memoize import cache_memoize
 
 
@@ -199,7 +203,7 @@ def has_person(obj: Union[HttpRequest, Model]) -> bool:
         return True
 
 
-def custom_information_processor(request: HttpRequest) -> dict:
+def custom_information_processor(request: Union[HttpRequest, None]) -> dict:
     """Provide custom information in all templates."""
     from ..models import CustomMenu
 
@@ -214,6 +218,8 @@ def custom_information_processor(request: HttpRequest) -> dict:
         "ADMINS": settings.ADMINS,
         "PWA_ICONS": regrouped_pwa_icons,
         "SENTRY_ENABLED": settings.SENTRY_ENABLED,
+        "SITE_PREFERENCES": get_site_preferences(),
+        "BASE_URL": settings.BASE_URL,
     }
 
     if settings.SENTRY_ENABLED:
@@ -278,6 +284,11 @@ def queryset_rules_filter(
     return queryset.filter(pk__in=wanted_objects)
 
 
+def generate_random_code(length, packet_size) -> str:
+    """Generate random code for e.g. invitations."""
+    return get_random_string(packet_size * length).lower()
+
+
 def unread_notifications_badge(request: HttpRequest) -> int:
     """Generate badge content with the number of unread notifications."""
     return request.user.person.unread_notifications_count
@@ -321,3 +332,139 @@ def get_allowed_object_ids(request: HttpRequest, models: list) -> list:
 class Ignorable500URLsFilter(Filter):
     def filter(self, record):
         breakpoint()
+
+
+def process_custom_context_processors(context_processors: list) -> Dict[str, Any]:
+    """Process custom context processors."""
+    context = {}
+    processors = tuple(import_string(path) for path in context_processors)
+    for processor in processors:
+        context.update(processor(None))
+    return context
+
+
+def create_default_celery_schedule():
+    """Create default periodic tasks in database for tasks that have a schedule defined."""
+    from celery import current_app
+    from celery.schedules import BaseSchedule, crontab, schedule, solar
+    from django_celery_beat.clockedschedule import clocked
+    from django_celery_beat.models import (
+        ClockedSchedule,
+        CrontabSchedule,
+        IntervalSchedule,
+        PeriodicTask,
+        SolarSchedule,
+    )
+
+    defined_periodic_tasks = PeriodicTask.objects.values_list("task", flat=True).all()
+
+    for name, task in current_app.tasks.items():
+        if name in defined_periodic_tasks:
+            # Task is already known in database, skip
+            continue
+
+        run_every = getattr(task, "run_every", None)
+        if not run_every:
+            # Task has no default schedule, skip
+            continue
+
+        if isinstance(run_every, (float, int, timedelta)):
+            # Schedule is defined as a raw seconds value or timedelta, convert to schedule class
+            run_every = schedule(run_every)
+        elif not isinstance(run_every, BaseSchedule):
+            raise ValueError(f"Task {name} has an invalid schedule defined.")
+
+        # Find matching django-celery-beat schedule model
+        if isinstance(run_every, clocked):
+            Schedule = ClockedSchedule
+            attr = "clocked"
+        elif isinstance(run_every, crontab):
+            Schedule = CrontabSchedule
+            attr = "crontab"
+        elif isinstance(run_every, schedule):
+            Schedule = IntervalSchedule
+            attr = "interval"
+        elif isinstance(run_every, solar):
+            Schedule = SolarSchedule
+            attr = "solar"
+        else:
+            raise ValueError(f"Task {name} has an unknown schedule class defined.")
+
+        # Get or create schedule in database
+        db_schedule = Schedule.from_schedule(run_every)
+        db_schedule.save()
+
+        # Create periodic task
+        PeriodicTask.objects.create(
+            name=f"{name} (default schedule)", task=name, **{attr: db_schedule}
+        )
+
+
+class OOTRouter:
+    """Database router for operations that should run out of transaction.
+
+    This router routes database operations for certain apps through
+    the separate default_oot connection, to ensure that data get
+    updated immediately even during atomic transactions.
+    """
+
+    default_db = "default"
+    oot_db = "default_oot"
+
+    _cachalot_invalidating = []
+
+    @property
+    def oot_labels(self):
+        return settings.DATABASE_OOT_LABELS
+
+    @property
+    def default_dbs(self):
+        return set((self.default_db, self.oot_db))
+
+    def is_same_db(self, db1: str, db2: str):
+        return set((db1, db2)).issubset(self.default_dbs)
+
+    def db_for_read(self, model: Model, **hints) -> Optional[str]:
+        if model._meta.app_label in self.oot_labels:
+            return self.oot_db
+
+        return None
+
+    def db_for_write(self, model: Model, **hints) -> Optional[str]:
+        return self.db_for_read(model, **hints)
+
+    def allow_relation(self, obj1: Model, obj2: Model, **hints) -> Optional[bool]:
+        # Allow relations between default database and OOT connection
+        # They are the same database
+        if self.is_same_db(obj1._state.db, obj2._state.db):
+            return True
+
+        return None
+
+    def allow_migrate(
+        self, db: str, app_label: str, model_name: Optional[str] = None, **hints
+    ) -> Optional[bool]:
+        # Never allow any migrations on the default_oot database
+        # It connects to the same database as default, so everything
+        # migrated there
+        if db == self.oot_db:
+            return False
+
+        return None
+
+    @classmethod
+    def _invalidate_cachalot(cls, sender, **kwargs):
+        if sender in cls._cachalot_invalidating:
+            return
+        cls._cachalot_invalidating.append(sender)
+
+        if kwargs["db_alias"] == cls.default_db:
+            invalidate(sender, db_alias=cls.oot_db)
+        elif kwargs["db_alias"] == cls.oot_db:
+            invalidate(sender, db_alias=cls.default_db)
+
+        if sender in cls._cachalot_invalidating:
+            cls._cachalot_invalidating.remove(sender)
+
+
+post_invalidation.connect(OOTRouter._invalidate_cachalot)
diff --git a/aleksis/core/util/email.py b/aleksis/core/util/email.py
new file mode 100644
index 0000000000000000000000000000000000000000..eae0e584604aa4e7141edcb6b244a2e1a079a762
--- /dev/null
+++ b/aleksis/core/util/email.py
@@ -0,0 +1,30 @@
+from typing import Any, Dict, List, Optional
+
+from django.conf import settings
+
+from templated_email import send_templated_mail
+
+from aleksis.core.util.core_helpers import get_site_preferences, process_custom_context_processors
+
+
+def send_email(
+    template_name: str,
+    recipient_list: List[str],
+    context: Dict[str, Any],
+    from_email: Optional[str] = None,
+    **kwargs,
+):
+    """Send templated email with data from context processors."""
+    processed_context = process_custom_context_processors(settings.NON_REQUEST_CONTEXT_PROCESSORS)
+    processed_context.update(context)
+    if not from_email:
+        from_address = get_site_preferences()["mail__address"]
+        from_name = get_site_preferences()["general__title"]
+        from_email = f"{from_name} <{from_address}>"
+    return send_templated_mail(
+        template_name=template_name,
+        from_email=from_email,
+        recipient_list=recipient_list,
+        context=processed_context,
+        **kwargs,
+    )
diff --git a/aleksis/core/util/ldap.py b/aleksis/core/util/ldap.py
index 96b058ac0060975cdd08281be94ba4df371aa7db..b60c9de98a48ca8b34a1cbd758301f0542cc9116 100644
--- a/aleksis/core/util/ldap.py
+++ b/aleksis/core/util/ldap.py
@@ -4,6 +4,8 @@ from django.core.exceptions import PermissionDenied
 
 from django_auth_ldap.backend import LDAPBackend as _LDAPBackend
 
+from ..models import UserAdditionalAttributes
+
 
 class LDAPBackend(_LDAPBackend):
     default_settings = {"SET_USABLE_PASSWORD": False}
@@ -24,11 +26,28 @@ class LDAPBackend(_LDAPBackend):
 
         if self.settings.SET_USABLE_PASSWORD:
             if not user:
-                # Fail early and do not try other backends
-                raise PermissionDenied("LDAP failed to authenticate user")
+                # The user could not be authenticated against LDAP.
+                # We need to make sure to let other backends handle it, but also that
+                # we do not let actually deleted/locked LDAP users fall through to a
+                # backend that cached a valid password
+                if UserAdditionalAttributes.get_user_attribute(
+                    ldap_user._username, "ldap_authenticated", False
+                ):
+                    # User was LDAP-authenticated in the past, so we fail authentication now
+                    # to not let other backends override a legitimate deletion
+                    raise PermissionDenied("LDAP failed to authenticate user")
+                else:
+                    # No note about LDAP authentication in the past
+                    # The user can continue authentication like before if they exist
+                    return user
 
             # Set a usable password so users can change their LDAP password
             user.set_password(password)
             user.save()
 
+            # Not that we LDAP-autenticated the user so we can check this in the future
+            UserAdditionalAttributes.set_user_attribute(
+                ldap_user._username, "ldap_authenticated", True
+            )
+
         return user
diff --git a/aleksis/core/util/notifications.py b/aleksis/core/util/notifications.py
index 38d27057033752d6ae7cdefce9c53b4728447b80..061b8d3d8fae61f68b1c3f2fa9e9f3f421491292 100644
--- a/aleksis/core/util/notifications.py
+++ b/aleksis/core/util/notifications.py
@@ -5,12 +5,12 @@ from typing import Sequence, Union
 from django.apps import apps
 from django.conf import settings
 from django.template.loader import get_template
+from django.utils import timezone
 from django.utils.functional import lazy
 from django.utils.translation import gettext_lazy as _
 
-from templated_email import send_templated_mail
-
 from .core_helpers import lazy_preference
+from .email import send_email
 
 try:
     from twilio.rest import Client as TwilioClient
@@ -21,7 +21,7 @@ except ImportError:
 def send_templated_sms(
     template_name: str, from_number: str, recipient_list: Sequence[str], context: dict
 ) -> None:
-    """Render a plan-text template and send via SMS to all recipients."""
+    """Render a plain-text template and send via SMS to all recipients."""
     template = get_template(template_name)
     text = template.render(context)
 
@@ -35,9 +35,8 @@ def _send_notification_email(notification: "Notification", template: str = "noti
         "notification": notification,
         "notification_user": notification.recipient.addressing_name,
     }
-    send_templated_mail(
+    send_email(
         template_name=template,
-        from_email=lazy_preference("mail", "address"),
         recipient_list=[notification.recipient.email],
         context=context,
     )
@@ -84,6 +83,8 @@ def send_notification(notification: Union[int, "Notification"], resend: bool = F
             name, check, send = _CHANNELS_MAP[channel]
             if check():
                 send(notification)
+                notification.sent = True
+                notification.save()
 
 
 def get_notification_choices() -> list:
@@ -101,3 +102,12 @@ def get_notification_choices() -> list:
 
 
 get_notification_choices_lazy = lazy(get_notification_choices, tuple)
+
+
+def _send_due_notifications():
+    """Send all notifications that are due to be sent."""
+    Notification = apps.get_model("core", "Notification")
+
+    due_notifications = Notification.objects.filter(sent=False, send_at__lte=timezone.now())
+    for notification in due_notifications:
+        notification.send()
diff --git a/aleksis/core/util/pdf.py b/aleksis/core/util/pdf.py
index 3f27680d9d8b3fc569a457ec78a2ce7c154c23d1..a261017b3b2bb22decea1cc517b414858b14a68c 100644
--- a/aleksis/core/util/pdf.py
+++ b/aleksis/core/util/pdf.py
@@ -1,7 +1,8 @@
 import os
 import subprocess  # noqa
+from datetime import timedelta
 from tempfile import TemporaryDirectory
-from typing import Optional, Tuple
+from typing import Optional, Tuple, Union
 from urllib.parse import urljoin
 
 from django.conf import settings
@@ -22,6 +23,7 @@ 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, render_progress_page
+from aleksis.core.util.core_helpers import process_custom_context_processors
 
 
 @recorded_task
@@ -71,9 +73,18 @@ def generate_pdf_from_template(
     template_name: str, context: Optional[dict] = None, request: Optional[HttpRequest] = None
 ) -> Tuple[PDFFile, AsyncResult]:
     """Start a PDF generation task and return the matching file object and Celery result."""
-    html_template = render_to_string(template_name, context, request)
+    if not request:
+        processed_context = process_custom_context_processors(
+            settings.NON_REQUEST_CONTEXT_PROCESSORS
+        )
+        processed_context.update(context)
+    else:
+        processed_context = context
+    html_template = render_to_string(template_name, processed_context, request)
 
-    file_object = PDFFile.objects.create(html_file=ContentFile(html_template, name="source.html"))
+    file_object = PDFFile.objects.create(
+        html_file=ContentFile(html_template.encode(), name="source.html")
+    )
 
     # As this method may be run in background and there is no request available,
     # we have to use a predefined URL from settings then
@@ -87,7 +98,9 @@ def generate_pdf_from_template(
     return file_object, result
 
 
-def render_pdf(request: HttpRequest, template_name: str, context: dict = None) -> HttpResponse:
+def render_pdf(
+    request: Union[HttpRequest, None], template_name: str, context: dict = None
+) -> HttpResponse:
     """Start PDF generation and show progress page.
 
     The progress page will redirect to the PDF after completion.
@@ -119,7 +132,7 @@ def clean_up_expired_pdf_files() -> None:
     PDFFile.objects.filter(expires_at__lt=timezone.now()).delete()
 
 
-@app.task
+@app.task(run_every=timedelta(days=1))
 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/sass_helpers.py b/aleksis/core/util/sass_helpers.py
index 2579ed83a6f7d7f143456e227f1c662987ba3f55..21e3d0bacd0700dc7c0db74182e547e6c885172c 100644
--- a/aleksis/core/util/sass_helpers.py
+++ b/aleksis/core/util/sass_helpers.py
@@ -23,7 +23,7 @@ def get_preference(section: str, name: str) -> str:
 def clean_scss(*args, **kwargs) -> None:
     """Unlink compiled CSS (i.e. cache invalidation)."""
     sass_storage = SassFileStorage()
-    __, files = sass_storage.listdir("")
+    __, files = sass_storage.listdir("public")
 
     for source_map in filter(lambda x: x.endswith(".css.map"), files):
-        sass_storage.delete(source_map)
+        sass_storage.delete(f"public/{source_map}")
diff --git a/aleksis/core/views.py b/aleksis/core/views.py
index 1a73deee1773edc55b53f83663931b6006109993..32e83c3746630ddf96e5c179180dbfb2face50d2 100644
--- a/aleksis/core/views.py
+++ b/aleksis/core/views.py
@@ -1,7 +1,11 @@
-from typing import Any, Optional, Type
+from textwrap import wrap
+from typing import Any, Dict, Optional, Type
+from urllib.parse import urlencode, urlparse, urlunparse
 
 from django.apps import apps
 from django.conf import settings
+from django.contrib.auth.models import Group as DjangoGroup
+from django.contrib.auth.models import Permission, User
 from django.contrib.contenttypes.models import ContentType
 from django.core.exceptions import PermissionDenied, ValidationError
 from django.core.paginator import Paginator
@@ -11,14 +15,15 @@ from django.http import (
     Http404,
     HttpRequest,
     HttpResponse,
-    HttpResponseNotFound,
     HttpResponseRedirect,
     HttpResponseServerError,
     JsonResponse,
+    QueryDict,
 )
 from django.shortcuts import get_object_or_404, redirect, render
 from django.template import loader
 from django.urls import reverse, reverse_lazy
+from django.utils import timezone
 from django.utils.decorators import method_decorator
 from django.utils.translation import get_language
 from django.utils.translation import gettext_lazy as _
@@ -26,46 +31,64 @@ from django.views.decorators.cache import never_cache
 from django.views.defaults import ERROR_500_TEMPLATE_NAME
 from django.views.generic.base import TemplateView, View
 from django.views.generic.detail import DetailView, SingleObjectMixin
-from django.views.generic.edit import DeleteView
+from django.views.generic.edit import DeleteView, FormView
 from django.views.generic.list import ListView
 
 import reversion
-from allauth.account.views import PasswordChangeView
+from allauth.account.utils import _has_verified_for_login, send_email_confirmation
+from allauth.account.views import PasswordChangeView, PasswordResetView, SignupView
 from allauth.socialaccount.adapter import get_adapter
 from allauth.socialaccount.models import SocialAccount
 from celery_progress.views import get_progress
 from django_celery_results.models import TaskResult
-from django_tables2 import RequestConfig, SingleTableView
+from django_filters.views import FilterView
+from django_tables2 import RequestConfig, SingleTableMixin, SingleTableView
 from dynamic_preferences.forms import preference_form_builder
-from guardian.shortcuts import get_objects_for_user
+from guardian.shortcuts import GroupObjectPermission, UserObjectPermission, get_objects_for_user
 from haystack.generic_views import SearchView
 from haystack.inputs import AutoQuery
 from haystack.query import SearchQuerySet
 from haystack.utils.loading import UnifiedIndex
 from health_check.views import MainView
+from invitations.views import SendInvite, accept_invitation
+from oauth2_provider.exceptions import OAuthToolkitError
+from oauth2_provider.models import get_application_model
+from oauth2_provider.views import AuthorizationView
 from reversion import set_user
 from reversion.views import RevisionMixin
 from rules.contrib.views import PermissionRequiredMixin, permission_required
+from two_factor.views.core import LoginView as AllAuthLoginView
 
 from aleksis.core.data_checks import DataCheckRegistry, check_data
 
 from .celery import app
-from .filters import GroupFilter, PersonFilter
+from .filters import (
+    GroupFilter,
+    GroupGlobalPermissionFilter,
+    GroupObjectPermissionFilter,
+    PersonFilter,
+    UserGlobalPermissionFilter,
+    UserObjectPermissionFilter,
+)
 from .forms import (
+    AccountRegisterForm,
     AnnouncementForm,
+    AssignPermissionForm,
     ChildGroupsForm,
     DashboardWidgetOrderFormSet,
     EditAdditionalFieldForm,
     EditGroupForm,
     EditGroupTypeForm,
     GroupPreferenceForm,
+    InvitationCodeForm,
     OAuthApplicationForm,
     PersonForm,
     PersonPreferenceForm,
     SchoolTermForm,
+    SelectPermissionForm,
     SitePreferenceForm,
 )
-from .mixins import AdvancedCreateView, AdvancedDeleteView, AdvancedEditView
+from .mixins import AdvancedCreateView, AdvancedDeleteView, AdvancedEditView, SuccessNextMixin
 from .models import (
     AdditionalField,
     Announcement,
@@ -79,6 +102,7 @@ from .models import (
     OAuthApplication,
     PDFFile,
     Person,
+    PersonInvitation,
     SchoolTerm,
     TaskUserAssignment,
 )
@@ -90,15 +114,21 @@ from .registries import (
 from .tables import (
     AdditionalFieldsTable,
     DashboardWidgetTable,
+    GroupGlobalPermissionTable,
+    GroupObjectPermissionTable,
     GroupsTable,
     GroupTypesTable,
+    InvitationsTable,
     PersonsTable,
     SchoolTermTable,
+    UserGlobalPermissionTable,
+    UserObjectPermissionTable,
 )
 from .util import messages
 from .util.apps import AppConfig
 from .util.celery_progress import render_progress_page
 from .util.core_helpers import (
+    generate_random_code,
     get_allowed_object_ids,
     get_pwa_icons,
     get_site_preferences,
@@ -145,6 +175,7 @@ class ManifestView(View):
             {
                 "src": favicon_img.faviconImage.url,
                 "sizes": f"{favicon_img.size}x{favicon_img.size}",
+                "purpose": "maskable any" if prefs["theme__pwa_icon_maskable"] else "any",
             }
             for favicon_img in pwa_imgs
         ]
@@ -184,9 +215,11 @@ def index(request: HttpRequest) -> HttpResponse:
         person = DummyPerson()
         widgets = []
 
-    activities = person.activities.all()[:5]
-    notifications = person.notifications.all()[:5]
-    unread_notifications = person.notifications.all().filter(read=False)
+    activities = person.activities.all().order_by("-created")[:5]
+    notifications = person.notifications.filter(send_at__lte=timezone.now()).order_by("-created")[
+        :5
+    ]
+    unread_notifications = person.notifications.all().filter(read=False).order_by("-created")
 
     context["activities"] = activities
     context["notifications"] = notifications
@@ -201,9 +234,11 @@ def index(request: HttpRequest) -> HttpResponse:
         context["default_dashboard"] = True
 
     media = DashboardWidget.get_media(widgets)
+    show_edit_dashboard_button = not getattr(person, "is_dummy", False)
 
     context["widgets"] = widgets
     context["media"] = media
+    context["show_edit_dashboard_button"] = show_edit_dashboard_button
 
     return render(request, "core/index.html", context)
 
@@ -213,7 +248,9 @@ class NotificationsListView(PermissionRequiredMixin, ListView):
     template_name = "core/notifications.html"
 
     def get_queryset(self) -> QuerySet:
-        return self.request.user.person.notifications.order_by("-created")
+        return self.request.user.person.notifications.filter(send_at__lte=timezone.now()).order_by(
+            "-created"
+        )
 
     def get_context_data(self, **kwargs: Any) -> dict[str, Any]:
         self.get_queryset().filter(read=False).update(read=True)
@@ -270,9 +307,7 @@ def persons(request: HttpRequest) -> HttpResponse:
     context = {}
 
     # Get all persons
-    persons = get_objects_for_user(
-        request.user, "core.view_person", Person.objects.filter(is_active=True)
-    )
+    persons = get_objects_for_user(request.user, "core.view_person", Person.objects.all())
 
     # Get filter
     persons_filter = PersonFilter(request.GET, queryset=persons)
@@ -297,12 +332,10 @@ def person(request: HttpRequest, id_: Optional[int] = None) -> HttpResponse:
     context["person"] = person
 
     # Get groups where person is member of
-    groups = Group.objects.filter(members=person)
+    context["groups"] = person.member_of.all()
 
-    # Build table
-    groups_table = GroupsTable(groups)
-    RequestConfig(request).configure(groups_table)
-    context["groups_table"] = groups_table
+    # Get whether the invitation feature is enabled in the preferences
+    context["invite_enabled"] = get_site_preferences()["auth__invite_enabled"]
 
     return render(request, "core/person/full.html", context)
 
@@ -319,7 +352,7 @@ def group(request: HttpRequest, id_: int) -> HttpResponse:
     group = Group.objects.get(pk=id_)
 
     # Get members
-    members = group.members.filter(is_active=True)
+    members = group.members.all()
 
     # Build table
     members_table = PersonsTable(members)
@@ -327,7 +360,7 @@ def group(request: HttpRequest, id_: int) -> HttpResponse:
     context["members_table"] = members_table
 
     # Get owners
-    owners = group.owners.filter(is_active=True)
+    owners = group.owners.all()
 
     # Build table
     owners_table = PersonsTable(owners)
@@ -645,11 +678,14 @@ def preferences(
             raise PermissionDenied()
     else:
         # Invalid registry name passed from URL
-        return HttpResponseNotFound()
+        raise Http404(_("The requested preference registry does not exist"))
 
     if not section and len(registry.sections()) > 0:
         default_section = list(registry.sections())[0]
-        return redirect(f"preferences_{registry_name}", default_section)
+        if instance:
+            return redirect(f"preferences_{registry_name}", instance.pk, default_section)
+        else:
+            return redirect(f"preferences_{registry_name}", default_section)
 
     # Build final form from dynamic-preferences
     form_class = preference_form_builder(form_class, instance=instance, section=section)
@@ -882,7 +918,7 @@ class SolveDataCheckView(PermissionRequiredMixin, RevisionMixin, DetailView):
             messages.success(request, msg)
             return redirect("check_data")
         else:
-            return HttpResponseNotFound()
+            raise Http404(_("The requested solve option does not exist"))
 
 
 class DashboardWidgetListView(PermissionRequiredMixin, SingleTableView):
@@ -966,7 +1002,11 @@ class EditDashboardView(PermissionRequiredMixin, View):
         context = {}
         self.default_dashboard = kwargs.get("default", False)
 
-        if self.default_dashboard and not request.user.has_perm("core.edit_default_dashboard_rule"):
+        if (
+            self.default_dashboard
+            and not request.user.has_perm("core.edit_default_dashboard_rule")
+            or getattr(person, "is_dummy", False)
+        ):
             raise PermissionDenied()
 
         context["default_dashboard"] = self.default_dashboard
@@ -1033,44 +1073,246 @@ class EditDashboardView(PermissionRequiredMixin, View):
         return render(request, "core/edit_dashboard.html", context=context)
 
 
-class OAuth2List(PermissionRequiredMixin, ListView):
+class InvitePerson(PermissionRequiredMixin, SingleTableView, SendInvite):
+    """View to invite a person to register an account."""
+
+    template_name = "invitations/forms/_invite.html"
+    permission_required = "core.can_invite"
+    model = PersonInvitation
+    table_class = InvitationsTable
+    context = {}
+
+    def dispatch(self, request, *args, **kwargs):
+        if not get_site_preferences()["auth__invite_enabled"]:
+            return HttpResponseRedirect(reverse_lazy("invite_disabled"))
+        return super().dispatch(request, *args, **kwargs)
+
+    # Get queryset of invitations
+    def get_context_data(self, **kwargs):
+        queryset = kwargs.pop("object_list", None)
+        if queryset is None:
+            self.object_list = self.model.objects.all()
+        return super().get_context_data(**kwargs)
+
+
+class EnterInvitationCode(FormView):
+    """View to enter an invitation code."""
+
+    template_name = "invitations/enter.html"
+    form_class = InvitationCodeForm
+
+    def form_valid(self, form):
+        code = "".join(form.cleaned_data["code"].lower().split("-"))
+        # Check if valid invitations exists
+        if (
+            PersonInvitation.objects.filter(key=code).exists()
+            and not PersonInvitation.objects.get(key=code).accepted
+            and not PersonInvitation.objects.get(key=code).key_expired()
+        ):
+            invitation = PersonInvitation.objects.get(key=code)
+            # Mark invitation as accepted and redirect to signup
+            accept_invitation(
+                invitation=invitation, request=self.request, signal_sender=self.request.user
+            )
+            self.request.session["invitation_code_entered"] = True
+            return redirect("account_signup")
+        return redirect("invitations:accept-invite", code)
+
+
+class GenerateInvitationCode(View):
+    """View to generate an invitation code."""
+
+    def get(self, request):
+        # Build code
+        length = get_site_preferences()["auth__invite_code_length"]
+        packet_size = get_site_preferences()["auth__invite_code_packet_size"]
+        code = generate_random_code(length, packet_size)
+
+        # Create invitation object
+        invitation = PersonInvitation.objects.create(
+            email="", inviter=request.user, key=code, sent=timezone.now()
+        )
+
+        # Make code more readable
+        code = "-".join(wrap(invitation.key, 5))
+
+        # Generate success message and print code
+        messages.success(
+            request,
+            _(f"The invitation was successfully created. The invitation code is {code}"),
+        )
+
+        return redirect("invite_person")
+
+
+class PermissionsListBaseView(PermissionRequiredMixin, SingleTableMixin, FilterView):
+    """Base view for list of all permissions."""
+
+    template_name = "core/perms/list.html"
+    permission_required = "core.manage_permissions"
+
+    def get_context_data(self, **kwargs):
+        context = super().get_context_data(**kwargs)
+        context["assign_form"] = SelectPermissionForm()
+        context["tab"] = self.tab
+
+        return context
+
+
+class UserGlobalPermissionsListBaseView(PermissionsListBaseView):
+    """List all global user permissions."""
+
+    filterset_class = UserGlobalPermissionFilter
+    table_class = UserGlobalPermissionTable
+    tab = "user_global"
+
+
+class GroupGlobalPermissionsListBaseView(PermissionsListBaseView):
+    """List all global group permissions."""
+
+    filterset_class = GroupGlobalPermissionFilter
+    table_class = GroupGlobalPermissionTable
+    tab = "group_global"
+
+
+class UserObjectPermissionsListBaseView(PermissionsListBaseView):
+    """List all object user permissions."""
+
+    filterset_class = UserObjectPermissionFilter
+    table_class = UserObjectPermissionTable
+    tab = "user_object"
+
+
+class GroupObjectPermissionsListBaseView(PermissionsListBaseView):
+    """List all object group permissions."""
+
+    filterset_class = GroupObjectPermissionFilter
+    table_class = GroupObjectPermissionTable
+    tab = "group_object"
+
+
+class SelectPermissionForAssignView(PermissionRequiredMixin, FormView):
+    """View for selecting a permission to assign."""
+
+    permission_required = "core.manage_permissions"
+    form_class = SelectPermissionForm
+
+    def form_valid(self, form: SelectPermissionForm) -> HttpResponse:
+        url = reverse("assign_permission", args=[form.cleaned_data["selected_permission"].pk])
+        params = {"next": self.request.GET["next"]} if "next" in self.request.GET else {}
+        return redirect(f"{url}?{urlencode(params)}")
+
+    def form_invalid(self, form: SelectPermissionForm) -> HttpResponse:
+        return redirect("manage_group_object_permissions")
+
+
+class AssignPermissionView(SuccessNextMixin, PermissionRequiredMixin, DetailView, FormView):
+    """View for assigning a permission to users/groups for all/some objects."""
+
+    permission_required = "core.manage_permissions"
+    queryset = Permission.objects.all()
+    template_name = "core/perms/assign.html"
+    form_class = AssignPermissionForm
+    success_url = "manage_user_global_permissions"
+
+    def get_form_kwargs(self) -> Dict[str, Any]:
+        kwargs = super().get_form_kwargs()
+        kwargs["permission"] = self.get_object()
+        return kwargs
+
+    def get_context_data(self, **kwargs: Any) -> Dict[str, Any]:
+        # Overwrite get_context_data to ensure correct function call order
+        self.object = self.get_object()
+        context = super().get_context_data(**kwargs)
+        return context
+
+    def form_valid(self, form: AssignPermissionForm) -> HttpResponse:
+        form.save_perms()
+        messages.success(
+            self.request,
+            _("We have successfully assigned the permissions."),
+        )
+        return redirect(self.get_success_url())
+
+
+class UserGlobalPermissionDeleteView(PermissionRequiredMixin, AdvancedDeleteView):
+    """Delete a global user permission."""
+
+    permission_required = "core.manage_permissions"
+    model = User.user_permissions.through
+    success_message = _("The global user permission has been deleted.")
+    success_url = reverse_lazy("manage_user_global_permissions")
+    template_name = "core/pages/delete.html"
+
+
+class GroupGlobalPermissionDeleteView(PermissionRequiredMixin, AdvancedDeleteView):
+    """Delete a global group permission."""
+
+    permission_required = "core.manage_permissions"
+    model = DjangoGroup.permissions.through
+    success_message = _("The global group permission has been deleted.")
+    success_url = reverse_lazy("manage_group_global_permissions")
+    template_name = "core/pages/delete.html"
+
+
+class UserObjectPermissionDeleteView(PermissionRequiredMixin, AdvancedDeleteView):
+    """Delete a object user permission."""
+
+    permission_required = "core.manage_permissions"
+    model = UserObjectPermission
+    success_message = _("The object user permission has been deleted.")
+    success_url = reverse_lazy("manage_user_object_permissions")
+    template_name = "core/pages/delete.html"
+
+
+class GroupObjectPermissionDeleteView(PermissionRequiredMixin, AdvancedDeleteView):
+    """Delete a object group permission."""
+
+    permission_required = "core.manage_permissions"
+    model = GroupObjectPermission
+    success_message = _("The object group permission has been deleted.")
+    success_url = reverse_lazy("manage_group_object_permissions")
+    template_name = "core/pages/delete.html"
+
+
+class OAuth2ListView(PermissionRequiredMixin, ListView):
     """List view for all the applications."""
 
-    permission_required = "core.list_oauth_applications_rule"
+    permission_required = "core.view_oauthapplications_rule"
     context_object_name = "applications"
-    template_name = "oauth2_provider/application_list.html"
+    template_name = "oauth2_provider/application/list.html"
 
     def get_queryset(self):
         return OAuthApplication.objects.all()
 
 
-class OAuth2Detail(PermissionRequiredMixin, DetailView):
+class OAuth2DetailView(PermissionRequiredMixin, DetailView):
     """Detail view for an application instance."""
 
     context_object_name = "application"
-    permission_required = "core.view_oauth_applications_rule"
-    template_name = "oauth2_provider/application_detail.html"
+    permission_required = "core.view_oauthapplication_rule"
+    template_name = "oauth2_provider/application/detail.html"
 
     def get_queryset(self):
         return OAuthApplication.objects.all()
 
 
-class OAuth2Delete(PermissionRequiredMixin, DeleteView):
+class OAuth2DeleteView(PermissionRequiredMixin, AdvancedDeleteView):
     """View used to delete an application."""
 
-    permission_required = "core.delete_oauth_applications_rule"
+    permission_required = "core.delete_oauthapplication_rule"
     context_object_name = "application"
-    success_url = reverse_lazy("oauth_list")
-    template_name = "oauth2_provider/application_confirm_delete.html"
+    success_url = reverse_lazy("oauth2_applications")
+    template_name = "core/pages/delete.html"
 
     def get_queryset(self):
         return OAuthApplication.objects.all()
 
 
 class OAuth2EditView(PermissionRequiredMixin, AdvancedEditView):
-    """View used to update an application."""
+    """View used to edit an application."""
 
-    permission_required = "core.update_oauth_applications_rule"
+    permission_required = "core.edit_oauthapplication_rule"
     context_object_name = "application"
     template_name = "oauth2_provider/application/edit.html"
     form_class = OAuthApplicationForm
@@ -1082,7 +1324,7 @@ class OAuth2EditView(PermissionRequiredMixin, AdvancedEditView):
 class OAuth2RegisterView(PermissionRequiredMixin, AdvancedCreateView):
     """View used to register an application."""
 
-    permission_required = "core.add_oauth_applications_rule"
+    permission_required = "core.create_oauthapplication_rule"
     context_object_name = "application"
     template_name = "oauth2_provider/application/create.html"
     form_class = OAuthApplicationForm
@@ -1096,7 +1338,7 @@ class RedirectToPDFFile(SingleObjectMixin, View):
     def get(self, *args, **kwargs):
         file_object = self.get_object()
         if not file_object.file:
-            raise Http404()
+            raise Http404(_("The requested PDF file does not exist"))
         return redirect(file_object.file.url)
 
 
@@ -1105,11 +1347,11 @@ class CeleryProgressView(View):
 
     def get(self, request: HttpRequest, task_id: str, *args, **kwargs) -> HttpResponse:
         if request.user.is_anonymous:
-            raise Http404()
+            raise Http404(_("The requested task does not exist or is not accessible"))
         if not TaskUserAssignment.objects.filter(
             task_result__task_id=task_id, user=request.user
         ).exists():
-            raise Http404()
+            raise Http404(_("The requested task does not exist or is not accessible"))
         return get_progress(request, task_id, *args, **kwargs)
 
 
@@ -1127,6 +1369,20 @@ class CustomPasswordChangeView(PermissionRequiredMixin, PasswordChangeView):
         super().__init__(*args, **kwargs)
 
 
+class CustomPasswordResetView(PermissionRequiredMixin, PasswordResetView):
+    """Custom password reset view to allow to disable resetting of password."""
+
+    permission_required = "core.can_reset_password"
+
+    def __init__(self, *args, **kwargs):
+        if get_site_preferences()["auth__allow_password_reset"]:
+            self.template_name = "account/password_reset.html"
+        else:
+            self.template_name = "account/password_change_disabled.html"
+
+        super().__init__(*args, **kwargs)
+
+
 class SocialAccountDeleteView(DeleteView):
     """Custom view to delete django-allauth social account."""
 
@@ -1167,3 +1423,144 @@ def server_error(
     context = {"request": request}
 
     return HttpResponseServerError(template.render(context))
+
+
+class AccountRegisterView(SignupView):
+    """Custom view to register a user account.
+
+    Rewrites dispatch function from allauth to check if signup is open or if the user
+    has a verified email address from an invitation; otherwise raises permission denied.
+    """
+
+    form_class = AccountRegisterForm
+    success_url = reverse_lazy("index")
+
+    def dispatch(self, request, *args, **kwargs):
+        if (
+            not request.user.has_perm("core.can_register")
+            and not request.session.get("account_verified_email")
+            and not request.session.get("invitation_code_entered")
+        ):
+            raise PermissionDenied()
+        return super(AccountRegisterView, self).dispatch(request, *args, **kwargs)
+
+    def get_form_kwargs(self):
+        kwargs = super(AccountRegisterView, self).get_form_kwargs()
+        kwargs["request"] = self.request
+        return kwargs
+
+
+class InvitePersonByID(PermissionRequiredMixin, SingleObjectMixin, View):
+    """Custom view to invite person by their ID."""
+
+    model = Person
+    success_url = reverse_lazy("persons")
+    permission_required = "core.can_invite"
+
+    def dispatch(self, request, *args, **kwargs):
+        if not get_site_preferences()["auth__invite_enabled"]:
+            return HttpResponseRedirect(reverse_lazy("invite_disabled"))
+        return super().dispatch(request, *args, **kwargs)
+
+    def get(self, request, *args, **kwargs):
+        person = self.get_object()
+
+        if not person.email or not PersonInvitation.objects.filter(email=person.email).exists():
+            length = get_site_preferences()["auth__invite_code_length"]
+            packet_size = get_site_preferences()["auth__invite_code_packet_size"]
+            key = generate_random_code(length, packet_size)
+            invite = PersonInvitation.objects.create(person=person, key=key)
+            if person.email:
+                invite.email = person.email
+            invite.inviter = self.request.user
+            invite.save()
+
+            invite.send_invitation(self.request)
+
+            if person.email:
+                messages.success(
+                    self.request,
+                    _(
+                        "Person was invited successfully and an email "
+                        "with further instructions has been send to them."
+                    ),
+                )
+            else:
+                readable_key = "-".join(wrap(key, packet_size))
+                messages.success(
+                    self.request,
+                    f"{_('Person was invited successfully. Their key is')} {readable_key}.",
+                )
+        else:
+            messages.success(self.request, _("Person was already invited."))
+
+        return HttpResponseRedirect(person.get_absolute_url())
+
+
+class InviteDisabledView(PermissionRequiredMixin, TemplateView):
+    """View to display a notice that the invite feature is disabled and how to enable it."""
+
+    template_name = "invitations/disabled.html"
+    permission_required = "core.change_site_preferences_rule"
+
+    def dispatch(self, request, *args, **kwargs):
+        if get_site_preferences()["auth__invite_enabled"]:
+            raise PermissionDenied()
+        return super().dispatch(request, *args, **kwargs)
+
+
+class LoginView(AllAuthLoginView):
+    """Custom login view covering e-mail verification if mandatory.
+
+    Overrides view from allauth to check if email verification from django-invitations is
+    mandatory. If it i, checks if the user has a verified email address, if not,
+    it re-sends verification.
+    """
+
+    def done(self, form_list, **kwargs):
+        if settings.ACCOUNT_EMAIL_VERIFICATION == "mandatory":
+            user = self.get_user()
+            if not _has_verified_for_login(user, user.email):
+                send_email_confirmation(self.request, user, signup=False, email=user.email)
+                return render(self.request, "account/verification_sent.html")
+
+        return super().done(form_list, **kwargs)
+
+    def get_context_data(self, form, **kwargs):
+        """Override context data to hide side menu and include OAuth2 application if given."""
+        context = super().get_context_data(form, **kwargs)
+        if self.request.GET.get("oauth"):
+            context["no_menu"] = True
+
+            if self.request.GET.get("client_id"):
+                application = get_application_model().objects.get(
+                    client_id=self.request.GET["client_id"]
+                )
+                context["oauth_application"] = application
+        return context
+
+
+class CustomAuthorizationView(AuthorizationView):
+    def handle_no_permission(self):
+        """Override handle_no_permission to provide OAuth2 information to login page."""
+        redirect_obj = super().handle_no_permission()
+
+        try:
+            scopes, credentials = self.validate_authorization_request(self.request)
+        except OAuthToolkitError as error:
+            # Application is not available at this time.
+            return self.error_response(error, application=None)
+
+        login_url_parts = list(urlparse(redirect_obj.url))
+        querystring = QueryDict(login_url_parts[4], mutable=True)
+        querystring["oauth"] = "yes"
+        querystring["client_id"] = credentials["client_id"]
+        login_url_parts[4] = querystring.urlencode(safe="/")
+
+        return HttpResponseRedirect(urlunparse(login_url_parts))
+
+    def get_context_data(self, **kwargs):
+        """Override context data to hide side menu."""
+        context = super().get_context_data(**kwargs)
+        context["no_menu"] = True
+        return context
diff --git a/docker-startup.sh b/docker-startup.sh
index 85013154a99fa5d9558a569aa687b5c8d2729821..1e02ce1d4bfdbd4373e93da8e2bd3f0c1ae02d56 100755
--- a/docker-startup.sh
+++ b/docker-startup.sh
@@ -48,8 +48,7 @@ wait_database() {
 
 prepare_database() {
 	# Migrate database; should only be run in app container or job
-	aleksis-admin migrate
-	aleksis-admin createinitialrevisions
+	aleksis-admin migrate && aleksis-admin createinitialrevisions
 }
 
 # Wait for database to be reachable under all conditions
diff --git a/docs/_static/2fa.png b/docs/_static/2fa.png
new file mode 100644
index 0000000000000000000000000000000000000000..e8d35aaf19e56df08c5cbf0fdafdfd7c63d2d0fe
Binary files /dev/null and b/docs/_static/2fa.png differ
diff --git a/docs/_static/accept_invite.png b/docs/_static/accept_invite.png
new file mode 100644
index 0000000000000000000000000000000000000000..598839ab61686ef58cc979ce2cc437b093f9e456
Binary files /dev/null and b/docs/_static/accept_invite.png differ
diff --git a/docs/_static/create_dashboard_widget.png b/docs/_static/create_dashboard_widget.png
new file mode 100644
index 0000000000000000000000000000000000000000..31cfcb31474aaf5b97b88e1e818f0c8363dda1a7
Binary files /dev/null and b/docs/_static/create_dashboard_widget.png differ
diff --git a/docs/_static/create_social_application.png b/docs/_static/create_social_application.png
new file mode 100644
index 0000000000000000000000000000000000000000..c28c5c30a6d71f8aa0f1177b92048449c688d113
Binary files /dev/null and b/docs/_static/create_social_application.png differ
diff --git a/docs/_static/dashboard.png b/docs/_static/dashboard.png
new file mode 100644
index 0000000000000000000000000000000000000000..a25e5ca64139e80a35e05a07bffe3b7112113139
Binary files /dev/null and b/docs/_static/dashboard.png differ
diff --git a/docs/_static/dashboard_widgets.png b/docs/_static/dashboard_widgets.png
new file mode 100644
index 0000000000000000000000000000000000000000..7e345543a30489b902a261296fe53de8d2c67e30
Binary files /dev/null and b/docs/_static/dashboard_widgets.png differ
diff --git a/docs/_static/data_checks.png b/docs/_static/data_checks.png
new file mode 100644
index 0000000000000000000000000000000000000000..f3ff5b36f2773ae2dfd6839f2dbcdad8b034397f
Binary files /dev/null and b/docs/_static/data_checks.png differ
diff --git a/docs/_static/edit_dashboard.png b/docs/_static/edit_dashboard.png
new file mode 100644
index 0000000000000000000000000000000000000000..92a277482c91ecb778fd3651d9351dc97221c3a1
Binary files /dev/null and b/docs/_static/edit_dashboard.png differ
diff --git a/docs/_static/edit_default_dashboard.png b/docs/_static/edit_default_dashboard.png
new file mode 100644
index 0000000000000000000000000000000000000000..c7547249c4152b433b984f5f40a258cf149306df
Binary files /dev/null and b/docs/_static/edit_default_dashboard.png differ
diff --git a/docs/_static/invitations.png b/docs/_static/invitations.png
new file mode 100644
index 0000000000000000000000000000000000000000..07cb25b9b1b741cd9a37aa07385b283c6c2805fe
Binary files /dev/null and b/docs/_static/invitations.png differ
diff --git a/docs/_static/invite_existing.png b/docs/_static/invite_existing.png
new file mode 100644
index 0000000000000000000000000000000000000000..6ae7c7b819fd43e3dc3bd5b35dc204a1661a1eca
Binary files /dev/null and b/docs/_static/invite_existing.png differ
diff --git a/docs/_static/signup.png b/docs/_static/signup.png
new file mode 100644
index 0000000000000000000000000000000000000000..cb60572b41a58327f366f7d9db665f6618220554
Binary files /dev/null and b/docs/_static/signup.png differ
diff --git a/docs/admin/00_index.rst b/docs/admin/00_index.rst
index 3c60f9d9540f1bba978f665cdd0c342160d6a2e1..b41aee2b80bc0d58181ca8374e696bb5a265cc44 100644
--- a/docs/admin/00_index.rst
+++ b/docs/admin/00_index.rst
@@ -1,5 +1,5 @@
-Configuration and administration
-================================
+Setup of the AlekSIS instance and the AlekSIS core
+==================================================
 
 .. toctree::
    :glob:
diff --git a/docs/admin/01_core_concepts.rst b/docs/admin/01_core_concepts.rst
new file mode 100644
index 0000000000000000000000000000000000000000..cdd49c7183217897b09cf1fda5575307e0a5f08c
--- /dev/null
+++ b/docs/admin/01_core_concepts.rst
@@ -0,0 +1,117 @@
+Concepts of the AlekSIS core
+============================
+
+The AlekSIS core provides functionality and data models as a base for
+all apps.
+
+.. _core-concept-schoolterm:
+
+The concept of school terms
+---------------------------
+
+In AlekSIS, mostly everything is based on school terms. A school term is
+a defined time range which can be used to link data to it. Typically,
+such data are learning groups, timetables or class register records.
+Although their name suggests it, school terms don’t have to be a half or
+a full year. They should depend on the way how you organise data in your
+institution.
+
+For example, if you issue reports at the end of every half year, a half
+year would be a good time range for your school terms because the class
+register statistics are evaluated for school terms.
+
+Anyway, you should create a school term before you start to import or
+create other data entries like persons or groups.
+
+Manage school terms
+~~~~~~~~~~~~~~~~~~~
+
+You can manage the school terms if you login with your admin account and
+open the menu entry ``Admin → School terms``. There you can find a list
+of all school terms and buttons to create, edit or delete school terms.
+Please be aware that there can be only one school term at time and each
+school term needs a unique name.
+
+.. _core-concept-person:
+
+The concept of persons
+----------------------
+
+The person model is designed to save all the data of students, teachers,
+guardians and any other persons of the school society. It tracks
+information like the following:
+
+-  Full name
+-  Short name
+-  Sex
+-  Date of birth
+-  Contact details (phone numbers, email)
+-  Address details
+-  Photo
+-  Relation to guardians
+-  Primary group (e. g. a class or a tutor group, cf. :ref:`core-concept-group`)
+
+Except for the name, all data points are optional, so you can decide on
+your own (and based on your local data protection laws) which data should be
+included in AlekSIS.
+
+There are two important things you should know about persons:
+
+-  **Persons are not automatically users:** That means that persons can
+   be linked to a user account including things like a password and the
+   ability to login, but they don’t have to be. For example, your
+   AlekSIS instance could save the data about parents, but you don’t
+   want them to login: In this scenario, the guardians are available as
+   persons **without** user accounts.
+-  **Persons are not linked to school terms:** As persons like students
+   are not only at the school for one school term, persons are not
+   linked to school terms.
+
+Manage persons
+~~~~~~~~~~~~~~
+
+The main method to manage persons is the view under
+``People → Persons``. To add person to groups, you have to open the
+respective group and set the person as a member or an owner.
+
+.. _core-concept-group:
+
+The concept of groups
+---------------------
+
+The AlekSIS groups are a universal way to organise persons in
+collections like classes, courses, tutor groups, clubs, or any other
+division you could imagine. They track the following data:
+
+-  Group name and short name
+-  Owners (e. g. class or course teacher(s))
+-  Members (e. g. students)
+-  Parent groups (e. g. a class could be a parent group for a course)
+-  Group type (e. g. class, course, club, etc.)
+
+In contrast to persons, groups are supposed to be **linked to school
+terms** (but they don’t have to be). For example, the composition of a
+class or a course varies from school term to school term. In order to
+archive historical data according to local laws, these groups have to be
+separeted which is solved by linking them to a school term.
+
+Manage groups
+~~~~~~~~~~~~~
+
+Groups are managed on the page ``People → Groups``. There you can
+search, view, create, change and delete groups.
+
+.. _core-concept-grouptype:
+
+Manage group types
+~~~~~~~~~~~~~~~~~~
+
+You can manage your local group types by opening the menu entry
+``People → Group types`` as an admin user.
+
+Import school terms, persons and groups from other data sources
+---------------------------------------------------------------
+
+When AlekSIS is not your single date source, all these data can be
+imported from other sources. You can find further information in the
+respective integration apps.
diff --git a/docs/admin/04_monitoring.rst b/docs/admin/04_monitoring.rst
deleted file mode 100644
index 1b80a43be057a285d223be803bc347ae2758fdc4..0000000000000000000000000000000000000000
--- a/docs/admin/04_monitoring.rst
+++ /dev/null
@@ -1,39 +0,0 @@
-Monitoring
-##########
-
-Prometheus
-**********
-
-AlekSIS provides a metric endpoint at `/metrics`, so you can scrape metrics in
-your Prometheus instance.
-
-Available metrics
-=================
-
-The exporter provides metrics about responses and requests, e.g.  statistics
-about response codes, request latency and requests per view.  It also
-provides data about database operations.
-
-Prometheus config to get metrics
-================================
-
-To get metrics of your AlekSIS instance, just add the following to your
-`prometheus.yml`::
-
-  - job_name: aleksis
-    static_configs:
-      - targets: ['my.aleksis-instance.com']
-    metrics_path: /metrics
-
-
-Grafana
-*******
-
-Visualise metrics with Grafana
-==============================
-
-If you want to visualise your AlekSIS metrics with Grafana, you can use one
-of the public available Grafana dashboards, for example the following one,
-or just write your own.
-
-https://grafana.com/grafana/dashboards/9528
diff --git a/docs/admin/05_storage.rst b/docs/admin/05_storage.rst
deleted file mode 100644
index b9b67e698c158ef6ea0b3ad2819ccd753ff9aea0..0000000000000000000000000000000000000000
--- a/docs/admin/05_storage.rst
+++ /dev/null
@@ -1,23 +0,0 @@
-Storage
-##########
-
-Amazon S3
-*********
-
-AlekSIS allows you to configure an Amazon S3 endpoint for static and media
-files. This is useful e.g. for loadbalancing with multiple AlekSIS
-instances.
-
-Configure an S3 endpoint
-=======================
-
-If you want to use an S3 endpoint to store files you have to configure the
-endpoint in your configuration file (`/etc/aleksis/aleksis.toml`)::
-
-  # Default values
-  [storage.s3]
-  enabled = true
-  endpoint_url = "https://minio.example.com"
-  bucket_name = "aleksis-test"
-  access_key_id = "XXXXXXXXXXXXXX"
-  secret_key = "XXXXXXXXXXXXXXXXXXXXXX"
diff --git a/docs/admin/01_install.rst b/docs/admin/10_install.rst
similarity index 98%
rename from docs/admin/01_install.rst
rename to docs/admin/10_install.rst
index 5251a054be53d1e587fbadc0c9c4b848d25df249..d41bd44e56c1fd85208467e9849d448d960c1900 100644
--- a/docs/admin/01_install.rst
+++ b/docs/admin/10_install.rst
@@ -20,6 +20,8 @@ AlekSIS will need and use the following paths:
 
 You can change any of the paths as you like.
 
+.. _core-install-prerequisites:
+
 Prerequisites
 ~~~~~~~~~~~~~
 
@@ -46,9 +48,7 @@ Install some packages from the Debian package system.
                nginx-full \
                python3 \
                python3-dev \
-               libldap2-dev \
                libpq-dev \
-               libsasl2-dev \
                yarnpkg \
                python3-virtualenv \
                chromium \
@@ -125,6 +125,8 @@ from `yarnpkg`, collect static files, and migrate the database to the final sche
    aleksis-admin migrate
    aleksis-admin createinitialrevisions
 
+.. _core-configure-uwsgi:
+
 Configure uWSGI
 ~~~~~~~~~~~~~~~
 
@@ -164,6 +166,7 @@ First, you should get a TLS certificate, e.g. by using `Let's Encrypt`_.
 Then, create a virtual host in nginx, by editing `/etc/nginx/sites-available/aleksis.example.com`.
 
 .. code-block:: nginx
+
    upstream aleksis {
      server unix:///run/uwsgi/app/aleksis/socket;
    }
@@ -249,6 +252,5 @@ Finally, bring the stack up using:
 AlekSIS will be reachable on port 80 if you forgot to configure the environment.
 You are responsible for adding a reverse proxy like nginx providing TLS, etc.
 
-.. _Dynaconf: https://dynaconf.readthedocs.io/en/latest/
 .. _Let's Encrypt: https://certbot.eff.org/instructions
 .. _PyPI: https://pypi.org
diff --git a/docs/admin/01_config.rst b/docs/admin/15_config_files.rst
similarity index 95%
rename from docs/admin/01_config.rst
rename to docs/admin/15_config_files.rst
index 1af436d556ab82254d1c5cfbd213e5eaae85bb0d..556713437cc467c38a4b3f2acf05c070b46554c6 100644
--- a/docs/admin/01_config.rst
+++ b/docs/admin/15_config_files.rst
@@ -1,3 +1,5 @@
+.. _core-configuration-files:
+
 Configuration files and format
 ==============================
 
@@ -38,7 +40,7 @@ A configuration file might look like this::
   password = "SuperSecretPassword"
 
   [caching]
-  redis = { enabled = true, address = "127.0.0.1" }
+  redis = { enabled = true, address = "redis://127.0.0.1" }
 
 The `secret_key` setting above defines a single value. The following `http`
 section defines a table (cf. a dictionary) in one way, and you can see the
diff --git a/docs/admin/05_configuration_options.rst b/docs/admin/16_config_options.rst
similarity index 92%
rename from docs/admin/05_configuration_options.rst
rename to docs/admin/16_config_options.rst
index 19321db371728406086e35e3a4d7dd1e6979f367..d87d2a3c6a7054ae847684019817f74a1324bab5 100644
--- a/docs/admin/05_configuration_options.rst
+++ b/docs/admin/16_config_options.rst
@@ -63,6 +63,6 @@ Example configuration file::
 Configuration in frontend
 -------------------------
 
-Everything that must not be configured before the AlekSIS instance fully starts can be configured in frontend, such as site title and logo.
+Everything that does not have to be configured before the AlekSIS instance fully starts can be configured in frontend, such as site title and logo.
 
 You can find the configuration options in your AlekSIS instance under ``Admin → Configuration``.
diff --git a/docs/admin/17_storage.rst b/docs/admin/17_storage.rst
new file mode 100644
index 0000000000000000000000000000000000000000..f4a7f6130735644298207edc290ee72ddb4f2d65
--- /dev/null
+++ b/docs/admin/17_storage.rst
@@ -0,0 +1,49 @@
+Storage
+=======
+
+AlekSIS needs a writable storage, both for media files (pictures,
+generated PDF files, and the like), and to store generated frontend
+assets like the themed CSS stylesheet.
+
+.. note::
+    Everything except this media storage can be mounted and used
+    entirely read-only, i.e. to keep the AlekSIS installation immutable.
+
+Local filesystem storage
+------------------------
+
+By default, the media storage resides in the local filesystem, in the
+location defined in the ``static.root`` configuration key.
+
+.. warning::
+    Do not expose the media storage directly through a webserver.
+    AlekSIS uses a specially protected storage framework that
+    employs cryptographic tokens to protect user data from URL
+    guessing.
+
+Amazon S3 (or other S#-compatible storage)
+------------------------------------------
+
+AlekSIS allows you to configure an Amazon S3 endpoint for  media
+files. This is useful e.g. for loadbalancing with multiple AlekSIS
+instances.
+
+.. note::
+   For some background jobs, AlekSIS stores HTML snippets in the media
+   storage for later use. You must ensure your S3 endpoint is part of
+   your ``Access-Control-Allow-Origin`` CORS header, so HTML loaded from
+   there can load resources from the ALekSIS instance.
+
+Configure an S3 endpoint
+~~~~~~~~~~~~~~~~~~~~~~~~
+
+If you want to use an S3 endpoint to store files you have to configure the
+endpoint in your configuration file (`/etc/aleksis/aleksis.toml`)::
+
+  # Default values
+  [storage.s3]
+  enabled = true
+  endpoint_url = "https://minio.example.com"
+  bucket_name = "aleksis-test"
+  access_key_id = "XXXXXXXXXXXXXX"
+  secret_key = "XXXXXXXXXXXXXXXXXXXXXX"
diff --git a/docs/admin/18_mail.rst b/docs/admin/18_mail.rst
new file mode 100644
index 0000000000000000000000000000000000000000..60e02d1c72a62d9e90ce438a9595e1de54e5f08b
--- /dev/null
+++ b/docs/admin/18_mail.rst
@@ -0,0 +1,36 @@
+Mail
+====
+
+AlekSIS needs to send mails e.g. for account confirmations, feedback or
+error reports.
+
+Configure mailing
+-----------------
+
+The mailserver can be configured via the configuration file
+
+.. code-block:: toml
+
+	[mail.server]
+	host = "mail.example.com"
+	tls = False
+	ssl = True
+	port = 25
+	user = "mailuser"
+	password = "password"
+
+Name and address for mails sent by AlekSIS can be configured in the
+webinterface.  To configure, visit `Admin → Configuration` and click on the
+`Mail` tab.
+
+Configure mail recipients
+-------------------------
+
+You can configure admin contacts in your configuration file, located at
+``/etc/aleksis/``.
+
+.. code-blocK:: toml
+
+	[contact]
+	admins = [["AlekSIS - Admins", "root@example.com"],["AlekSIS - Admins2", "root2@example.com"]]
+	from = 'aleksis@example.com'
diff --git a/docs/admin/02_ldap.rst b/docs/admin/21_ldap.rst
similarity index 94%
rename from docs/admin/02_ldap.rst
rename to docs/admin/21_ldap.rst
index b8fbd657dedc3d3e7a6ef04a7fa36d9b7f1b66bf..7b123a594154950482a6cdf7ac095a3030ee5359 100644
--- a/docs/admin/02_ldap.rst
+++ b/docs/admin/21_ldap.rst
@@ -1,3 +1,5 @@
+.. _core-ldap:
+
 Authenticating against LDAP
 ===========================
 
@@ -20,7 +22,7 @@ very straightforward under all circumstances. On Debian, install these packages:
 Configuration of LDAP support
 -----------------------------
 
-Configuration is done under the ``default.ldap`` section in AlekSIS’
+Configuration is done under the ``ldap`` section in AlekSIS’
 configuration file. For example, add something like the following to your
 configuration (normally in ``/etc/aleksis``; you can either append to an
 existing file or add a new one)::
diff --git a/docs/admin/22_registration.rst b/docs/admin/22_registration.rst
new file mode 100644
index 0000000000000000000000000000000000000000..510500f2198ec6377c91d97bf1be903411f0daea
--- /dev/null
+++ b/docs/admin/22_registration.rst
@@ -0,0 +1,92 @@
+Registration and user invitations
+=================================
+
+In addition to central management of user accounts, AlekSIS allows self-registration
+by users. Registration can be either fully open, or based on personal invitations.
+
+In a system handling ciritcal data, access control should be as tight as possible.
+However, there are scenarios where central account creation is not feasible, e.g.
+for optional guardian accounts. In such a scenario, the invitation system allows
+for processes like handing out invitation codes as a letter or through e-mail
+campaigns.
+
+Configuration
+-------------
+
+.. _core-registration:
+
+Registration
+~~~~~~~~~~~~
+
+Registration can be enabled via the configuration interface in frontend.
+
+In the ``Authentication`` tab, click the checkbox ``Enable signup`` to enable
+signup for everyone. A menu item will be added for public registration.
+
+.. warning::
+   Do not enable this feature unless you intend to run a public AlekSIS instance.
+
+Before enabling registration, you should consider restricting allowed usernames.
+By default, all ASCII characters are allowed in usernames. Often, it is advisable
+to not allow special characters. This often depends on the systems that will be
+linked to AlekSIS.
+
+To restrict usernames to a certain format, a regular expression can be defined
+in the ``Regular expression for allowed usernames`` preference. For example, to
+restrict the username to lower case letters and numbers, and beginning with a number,
+the regex can be set to ``^[a-z][a-z0-9]+$`.
+
+User invitations
+~~~~~~~~~~~~~~~~
+
+.. _core-user-invitations:
+
+In the same location as public registration, the invitation system can be enabled.
+
+* Authentication
+
+  * Enable invitations: Click to enable invitations.
+  * Length of invite code: Length of invitation code packets, defaults to 5.
+  * Size of packets: Configure how many packets are generated, defaults to 3.
+
+By default, an invitation code looks like the following:
+``abcde-abcde-abcde``.
+
+A menu item will become available for users to enter their invitation code.
+
+Usage
+-----
+
+Invite by email or code
+~~~~~~~~~~~~~~~~~~~~~~~
+
+To invite a new user , visit the invitation page located at ``People → Invite
+person``
+
+Here you are able to invite the user by email address or generate an
+invitation code.
+
+.. image:: ../_static/invitations.png
+  :width: 100%
+  :alt: Invitations page
+
+This mechanism allows for registration of entirely new persons that do not
+exist in the system, e.g. if perosnal details are not known in advance.
+
+Invite existing person
+~~~~~~~~~~~~~~~~~~~~~~
+
+To invite an existing person, open the person in AlekSIS and click ``Invite
+user``.
+
+The invitation will be sent to the person's email address, and can only
+be used by this person. Upon registration, the new account will automatically
+be linked to the existing person.
+
+.. image:: ../_static/invite_existing.png
+  :width: 100%
+  :alt: Invite existing person
+
+.. note::
+   Before using this feature, make sure to read and understand
+   :ref:`core-concept-person`.
diff --git a/docs/admin/23_socialaccounts.rst b/docs/admin/23_socialaccounts.rst
new file mode 100644
index 0000000000000000000000000000000000000000..97f2fc7bb15c59f7cbac769759c6a485122e5ea8
--- /dev/null
+++ b/docs/admin/23_socialaccounts.rst
@@ -0,0 +1,38 @@
+Social accounts
+===============
+
+AlekSIS can authenticate users against third party applications using OAuth2
+or OpenID.
+
+This can be used to grant access to persons whose credentials shall not be
+managed in AlekSIS itself, for example because another authentication provider
+is already used throughout the school, or for guardians that can or should for
+some reason not get an LDAP account, or similar situations.
+
+.. warning::
+  Social accounts are **not** working with two factor authentication! If a user
+  authenticates with a social account, the two factor authentication is
+  ignored on login (but enforced for views that require two factor authentication later).
+
+Configuring social account provider
+-----------------------------------
+
+For available providers, see documentation of `django-allauth
+<https://django-allauth.readthedocs.io/en/latest/providers.html>`_.
+
+A new social account provider can be configured in your configuration file
+(located in ``/etc/aleksis/``).
+
+Configuration example::
+
+  [auth.providers.gitlab]
+  GITLAB_URL = "https://gitlab.exmaple.com"
+
+After configuring a new auth provider, you have to restart AlekSIS and configure client id and secret in the Backend Admin interface.
+Click "Social applications" and add a new application. Choose your
+provider and enter client id and secret from your application and choose
+your site:
+
+.. image:: ../_static/create_social_application.png
+  :width: 100%
+  :alt: Create social application
diff --git a/docs/admin/31_monitoring.rst b/docs/admin/31_monitoring.rst
new file mode 100644
index 0000000000000000000000000000000000000000..1847297352a135aafdff954d98eb2a2cc1a69573
--- /dev/null
+++ b/docs/admin/31_monitoring.rst
@@ -0,0 +1,113 @@
+.. _sec:Monitoring:
+
+Monitoring and health checks
+============================
+
+Configuration
+-------------
+
+Thresholds
+~~~~~~~~~~
+
+Thresholds for health checks can be configured via config file
+(``/etc/aleksis``).
+
+.. code:: toml
+
+   [health]
+   disk_usage_max_percent = 90
+   memory_min_mb = 500
+
+   [backup.database]
+   check_seconds = 7200
+
+   [backup.media]
+   check_seconds = 7200
+
+Status page
+-----------
+
+AlekSIS' status page shows information about the health of your AlekSIS
+instance. You can visit it via the left navigation bar (Admin → Status).
+
+The page shows information about debug and maintenance mode, a summary of
+your health checks and the last exit status of your celery tasks. This
+page can not be used as a health check, it will always return HTTP 200
+if the site is reachable.
+
+Health check
+------------
+
+The health check can be used to verify the health of your AlekSIS
+instance. You can access it via the browser
+(https://aleksis.example.com/health/) and it will show you a summary of
+your health checks. If something is wrong it will return HTTP 500.
+
+It is also possible to get a JSON response from the health check, for
+example via ``curl``. You only have to pass a valid
+``Accept: application/json`` header to your request.
+
+The health check can also be executed via ``aleksis-admin``:
+
+.. code:: shell
+
+   $ aleksis-admin health_check
+
+Monitoring with Icinga2
+-----------------------
+
+As already mentioned, there is a JSON endpoint at
+https://aleksis.example.com/health/. You can use an json check plugin to
+check seperate health checks or just use a HTTP check to check if the
+site returns HTTP 200.
+
+Performance monitoring with Prometheus
+--------------------------------------
+
+AlekSIS provides a Prometheus exporter. The exporter provides metrics
+about responses and requests, e.g. about response codes, request
+latency and requests per view. It also provides data about database
+operations.
+
+The metrics endpoint can be found at
+https://aleksis.example.com/metrics. In the default configuration it can
+be scraped from everywhere. You might want to add some webserver
+configuration to restrict access to this url.
+
+To get metrics of your AlekSIS instance, just add the following to
+``prometheus.yml``
+
+.. code:: yaml
+
+     - job_name: aleksis
+       static_configs:
+         - targets: ['aleksis.example.com']
+       metrics_path: /metrics
+
+Rules for prometheus alertmanager
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+If you are using the prometheus alertmanager, it is possible to create
+some alerting rules so that an alert is fired when your AlekSIS instance
+is slow or something.
+
+.. code:: yaml
+
+   groups:
+   - name: aleksis
+     rules:
+     - alert: HighRequestLatency
+       expr: histogram_quantile(0.999, sum(rate(django_http_requests_latency_seconds_by_view_method_bucket{instance="YOUR-INSTANCE",view!~"prometheus-django-metrics|healthcheck"}[15m])) by (job, le)) < 30
+       for: 15m
+       labels:
+         severity: page
+       annotations:
+         summary: High request latency for 15 minutes
+
+Grafana dashboard
+~~~~~~~~~~~~~~~~~
+
+There is a Grafana dashboard available to visualise the metrics.
+
+The dashboard is available at
+https://grafana.com/grafana/dashboards/9528.
diff --git a/docs/admin/32_tasks.rst b/docs/admin/32_tasks.rst
new file mode 100644
index 0000000000000000000000000000000000000000..4b7fd22353deebdf51b9ebec74fda531bc465633
--- /dev/null
+++ b/docs/admin/32_tasks.rst
@@ -0,0 +1,28 @@
+Background tasks
+================
+
+Operations that are expected to take a long time are run as background tasks.
+For this, at least one `Celery`_ worker has to be running, e.g. by coupling it
+with uWSGI as laid out in :ref:`core-configure-uwsgi`.
+
+If a task is triggered from the AlekSIS frontend, for example by starting an import
+job, a progress page is displayed, and the result of the job is waited for.
+
+.. _core-periodic-tasks:
+
+Periodic tasks
+~~~~~~~~~~~~~~
+
+Some tasks are also run on a schedule. For example, the backup job is run on
+a regular basis.
+
+All tasks in AlekSIS that are expected to run have a default schedule, which
+is registered when migrating the database. Changing this default schedule
+is currently only possible through the Django Admin backend, under
+*Admin → Backend Admin*.
+
+Under the *Periodic Tasks* app, you can define schedules and tasks. The names
+of tasks you can add manually are documented in the respective sections
+of the manual.
+
+.. _Celery: https://celeryproject.org/
diff --git a/docs/admin/33_data_checks.rst b/docs/admin/33_data_checks.rst
new file mode 100644
index 0000000000000000000000000000000000000000..92de54485aea5fada1162c3e18f7b9313e36de0c
--- /dev/null
+++ b/docs/admin/33_data_checks.rst
@@ -0,0 +1,42 @@
+.. _core-data-checks:
+
+Data checks
+===========
+
+Data checks are AlekSIS' mechanism for highlighting issues with the
+contents of the database. These checks are not of a technical nature,
+but strictly concern the contextual integrity of the data stored.
+
+
+Verify data checks
+------------------
+
+In the menu under ``Admin → Data checks``, the status of all known
+checks can be verified.
+
+.. image:: ../_static/data_checks.png
+   :width: 100%
+   :alt: Data check overview
+
+The first card shows the current global check state. If any data checks
+reported issues, they will be listed here. In that case, administrators can
+choose between options provided by the data checks to resolve the issues.
+
+.. note::
+   Details about the checks and solve options are described in the
+   respective chapters of the manual.
+
+Configure notifications
+-----------------------
+
+In the ``General`` tab of the configuration interface, you can configure
+email notifications for problems detected by the data checks.
+
+* General
+
+  * Send emails if data checks detect problems: Enable email notifications
+  * Email recipients for data checks problem emails: Choose recipient persons
+  * Email recipient groups for data checks problem emails: Choose recipient groups
+
+Data checks normally run once per hour, and if notifications are enabled, results
+will be mailed to the selected recipients if problems are detected.
diff --git a/docs/admin/50_dashboard.rst b/docs/admin/50_dashboard.rst
new file mode 100644
index 0000000000000000000000000000000000000000..bf2c72d8d4ed0d76832635bc727612ac982e182f
--- /dev/null
+++ b/docs/admin/50_dashboard.rst
@@ -0,0 +1,100 @@
+Providing important information to users using the dashboard
+============================================================
+
+The dashboard is a central place for providing important information to users.
+This is done by so-called dashboard widgets provided by the Core and apps.
+
+Built-in dashboard widgets
+--------------------------
+
+External link widget
+^^^^^^^^^^^^^^^^^^^^
+
+The external link widget will show a link to an external site on the dashboard,
+optionally with an icon or picture next to it. It therefore provides the following additional attributes:
+
+* **URL**: The URL of the external site.
+* **Icon URL**: The URL of the icon or picture shown next to the link.
+
+As link title, the widget title will be used.
+
+Static content widget
+^^^^^^^^^^^^^^^^^^^^
+
+The static content widget allows to display custom static information on the dashboard,
+It therefore provides the following additional attribute:
+
+* **Content**: The content of the widget. HTML can be used for formatting.
+
+More dashboard widgets from apps
+--------------------------------
+
+In addition to the built-in widgets, apps can provide their own dashboard widgets.
+Best examples for such apps are currently *AlekSIS-App-DashboardFeeds* and *AlekSIS-App-Chronos*.
+
+.. Add References to the apps
+
+.. _core-configure-dashboard-widgets:
+
+Add and configure dashboard widgets
+-----------------------------------
+
+If you want to add a new dashboard widget, you can do so by adding the dashboard widget at *Admin → Dashboard widgets*.
+There you will see all currently configured dashboard widgets and
+can add new ones using the *Create dashboard widget* button which will ask your for the widget type.
+
+.. image:: ../_static/dashboard_widgets.png
+  :width: 100%
+  :alt: All configured dashboard widgets
+
+Each dashboard widget has at least the followong attributes
+
+* **Widget Title**: The title of the widget (will be shown in some widgets).
+* **Activate Widget**: If this isn't checked, the widget will not be shown.
+* **Widget is broken**: If this is checked, the widget will be shown
+  but the user will get a message that this widget is currently out of order because of an error.
+  This shouldn't be checked by yourself, but might be activated automatically by a widget if it encounters an error.
+  If this case enters, you should check for the cause of the error and fix it. After that, you can unmark the widget as broken.
+* **Size on different screens**: The size of the widget on different screens.
+  We work with a grid system containing a maximum of 12 columns. So, one column is 1/12 of the screen width.
+  The width in the following fields has to be entered as number of columns (1 to 12).
+
+  * **Size on mobile devices**: The size of the widget on mobile devices (600px and less).
+  * **Size on tablet devices**: The size of the widget on desktop devices (600px - 992px).
+  * **Size on desktop devices**: The size of the widget on desktop devices (992px - 1200px).
+  * **Size on large desktop devices**: The size of the widget on large desktop devices (1200px and above).
+
+All other attributes are specific to the widget type and are explained in the documentation of the widget.
+
+.. image:: ../_static/create_dashboard_widget.png
+  :width: 100%
+  :alt: Form to create an external link widget
+
+Setup a default dashboard
+-------------------------
+
+To make the configured dashboard widgets accessible to all users, we recommend to configure the default dashboard.
+If you don't do so, the dashboard widgets will only be available to users if they customise their dashboard.
+
+The default dashboard can be configured via *Admin → Dashboard widgets → Edit default dashboard*.
+The edit page works exactly as the page described in :ref:`core-user-customising-dashboard`.
+
+.. image:: ../_static/edit_default_dashboard.png
+  :width: 100%
+  :alt: Edit the default dashboard
+
+Preferences
+-----------
+
+The behavior of the dashboard can be configured via *Admin → Configuration → General*. The following settings are available:
+
+* **Show dashboard to users without login**: If this is checked, the dashboard will be also shown to users who are not logged in.
+
+.. warning::
+
+    That won't work with all dashboard widgets. Some widgets, like the timetable widgets, require a logged in user.
+
+* **Allow users to edit their dashboard**: With this preference, system administrators can decide whether users
+  can edit their own dashboard as described in :ref:`core-user-customising-dashboard`.
+* **Automatically update the dashboard and its widgets sitewide**: If enabled,
+  the dashboard will be updated automatically every 15 seconds.
diff --git a/docs/conf.py b/docs/conf.py
index 136f619aebbbcefc457b9e1d316c6aa70b670e7a..2acaa4acf472ffb6ea2a1124d9371644186cc5c6 100644
--- a/docs/conf.py
+++ b/docs/conf.py
@@ -24,14 +24,14 @@ django.setup()
 
 # -- Project information -----------------------------------------------------
 
-project = "AlekSIS"
-copyright = "2019, 2020, AlekSIS team"
-author = "AlekSIS team"
+project = "AlekSIS-Core"
+copyright = "2019-2022 The AlekSIS team"
+author = "The AlekSIS Team"
 
 # The short X.Y version
-version = "2.0"
+version = "2.7"
 # The full version, including alpha/beta/rc tags
-release = "2.0b0"
+release = "2.7.5.dev0"
 
 
 # -- General configuration ---------------------------------------------------
@@ -84,13 +84,25 @@ pygments_style = None
 # The theme to use for HTML and HTML Help pages.  See the documentation for
 # a list of builtin themes.
 #
-html_theme = "sphinx_materialdesign_theme"
+html_theme = "sphinx_material"
+
+html_favicon = "../aleksis/core/static/img/aleksis-icon.png"
+html_logo = "../aleksis/core/static/img/aleksis-banner.svg"
 
 # Theme options are theme-specific and customize the look and feel of a theme
 # further.  For a list of options available for each theme, see the
 # documentation.
 #
-# html_theme_options = {}
+html_theme_options = {
+    "repo_url": f"https://edugit.org/AlekSIS/official/{project}",
+    "repo_name": "EduGit",
+    "repo_type": "gitlab",
+    "theme_color": "#0d5eaf",
+    "color_primary": "#0d5eaf",
+    "color_accent": "#0d5eaf",
+    "globaltoc_depth": 2,
+    "globaltoc_collapse": False,
+}
 
 # Add any paths that contain custom static files (such as style sheets) here,
 # relative to this directory. They are copied after the builtin static files,
@@ -105,13 +117,15 @@ html_static_path = ["_static"]
 # default: ``['localtoc.html', 'relations.html', 'sourcelink.html',
 # 'searchbox.html']``.
 #
-# html_sidebars = {}
+html_sidebars = {
+    "**": ["logo-text.html", "globaltoc.html", "localtoc.html", "searchbox.html"]
+}
 
 
 # -- Options for HTMLHelp output ---------------------------------------------
 
 # Output file base name for HTML help builder.
-htmlhelp_basename = "AlekSISdoc"
+htmlhelp_basename = f"{project}doc"
 
 
 # -- Options for LaTeX output ------------------------------------------------
@@ -135,7 +149,7 @@ latex_elements = {
 # (source start file, target name, title,
 #  author, documentclass [howto, manual, or own class]).
 latex_documents = [
-    (master_doc, "AlekSIS.tex", "AlekSIS Documentation", "AlekSIS team", "manual"),
+    (master_doc, f"{project}.tex", f"{project} Documentation", author, "manual"),
 ]
 
 
@@ -143,7 +157,7 @@ latex_documents = [
 
 # One entry per manual page. List of tuples
 # (source start file, name, description, authors, manual section).
-man_pages = [(master_doc, "aleksis", "AlekSIS Documentation", [author], 1)]
+man_pages = [(master_doc, "aleksis", f"{project} Documentation", [author], 1)]
 
 
 # -- Options for Texinfo output ----------------------------------------------
@@ -154,10 +168,10 @@ man_pages = [(master_doc, "aleksis", "AlekSIS Documentation", [author], 1)]
 texinfo_documents = [
     (
         master_doc,
-        "AlekSIS",
-        "AlekSIS Documentation",
+        project,
+        f"{project} Documentation",
         author,
-        "AlekSIS",
+        project,
         "One line description of project.",
         "Miscellaneous",
     ),
diff --git a/docs/dev/00_index.rst b/docs/dev/00_index.rst
index 02a5cc03c73ee203e3c77c29866135c091fb3162..7a0783cf5ef4767c8bc78cca7c0cdf57f8a357ae 100644
--- a/docs/dev/00_index.rst
+++ b/docs/dev/00_index.rst
@@ -1,5 +1,5 @@
-Development
-===========
+Development of AlekSIS apps using the AlekSIS core
+==================================================
 
 .. toctree::
    :glob:
diff --git a/docs/dev/01_setup.rst b/docs/dev/01_setup.rst
index dd3121a822e53c603b21511b790f5ef6950f295a..e52941af0c77b266ffd74bf410cccb0ef04b2678 100644
--- a/docs/dev/01_setup.rst
+++ b/docs/dev/01_setup.rst
@@ -20,7 +20,7 @@ AlekSIS requires `PostgreSQL`_ (version 13 or newer) as database
 backend. To provide a database names `aleksis` with a user named
 `aleksis` on Debian::
 
-  sudo apt install postgresql-13
+  sudo apt install postgresql
   sudo -u postgres createuser -P aleksis
   sudo -u postgres createdb -O aleksis aleksis
 
@@ -86,16 +86,15 @@ some maintenance tasks need to be done:
 2. Collect static files
 3. Run database migrations
 
-All three steps can be done with the ``poetry run`` command and
+All three steps can be done with the ``poetry shell`` command and
 ``aleksis-admin``::
 
-  poetry run aleksis-admin yarn install
-  poetry run aleksis-admin collectstatic
-  poetry run aleksis-admin compilemessages
-  poetry run aleksis-admin migrate
-  poetry run aleksis-admin createinitialrevisions
-
-(You might need database settings for the `migrate` command; see below.)
+  ALEKSIS_maintenance__debug=true ALEKSIS_database__password=aleksis poetry shell
+   poetry run aleksis-admin yarn install
+   poetry run aleksis-admin collectstatic
+   poetry run aleksis-admin compilemessages
+   poetry run aleksis-admin migrate
+   poetry run aleksis-admin createinitialrevisions
 
 Running the development server
 ------------------------------
@@ -108,14 +107,7 @@ basic settings in as environment variable. Here is an example that runs the
 development server against a local PostgreSQL database with password
 `aleksis` (all else remains default) and with the `debug` setting enabled::
 
-  ALEKSIS_debug=true ALEKSIS_database__password=aleksis poetry run aleksis-admin runuwsgi
-
-.. figure:: /screenshots/index.png
-   :scale: 50%
-   :alt: Screenshot of index page
-
-   After installing the development environment with default settings,
-   you should see the index page with the Bootstrap style.
+  ALEKSIS_maintenance__debug=true ALEKSIS_database__password=aleksis poetry run aleksis-admin runuwsgi
 
 .. _Poetry: https://poetry.eustace.io/
 .. _Poetry installation methods: https://poetry.eustace.io/docs/#installation
diff --git a/docs/dev/02_install_apps.rst b/docs/dev/02_install_apps.rst
index bbfbb9b4007e533c1e910ae829ddf80f382b7d99..a376dd4f413380750c8b466c73fdd07973ac36f6 100644
--- a/docs/dev/02_install_apps.rst
+++ b/docs/dev/02_install_apps.rst
@@ -9,6 +9,26 @@ sub-folder of the meta repository. If you followed the documentation, they
 will already be checked out in the version required for the bundle you
 are running.
 
+Installing a development environment for own apps
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+If you are developing your own app, you probably do not want to
+run a development environment from the `AlekSIS-Core` repository.
+
+Instead, simply install the environment using ``poetry install`` from
+your app repository – it will pull in `AlekSIS-Core` as a dependency
+automatically, and everything will work as described beforehand.
+
+.. note::
+   Take care not to mix up environments, especially if using ``poetry shell``.
+
+
+Using one virtual environment for everything
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+.. note::
+   This method is not encouraged for most use cases.
+
 Installing apps into the existing virtual environment of `AlekSIS-Core` can
 be easily done after starting `poetry shell`::
 
@@ -17,5 +37,6 @@ be easily done after starting `poetry shell`::
 Do not forget to run the maintenance tasks described earlier after
 installing any app.
 
-**Heads up:** This is not suitable for working on the core, because it
-will install the `AlekSIS-Core` version used by the app using `pip` again.
+.. note::
+   This is not suitable for working on the core, because it
+   will install the `AlekSIS-Core` version used by the app using `pip` again.
diff --git a/docs/dev/03_run_tests.rst b/docs/dev/03_run_tests.rst
index ae38192ed8fb8fdaae51046839269cac1ef61af4..c50eda52d1c04d183c518a938a6f6a7a1c579ab3 100644
--- a/docs/dev/03_run_tests.rst
+++ b/docs/dev/03_run_tests.rst
@@ -6,8 +6,7 @@ Running default test suite
 
 The test suite can be run using the `tox` tool::
 
-  poetry run tox
-
+  tox
 
 Enabling Selenium browser tests
 -------------------------------
@@ -32,7 +31,7 @@ Selenium tests are enabled if `TEST_SELENIUM_BROWSERS` is non-empty.
 
 To set variables, use env to wrap the tox ommand::
 
-  poetry run env TEST_SELENIUM_BROWSERS=chrome,firefox tox
+  TEST_SELENIUM_BROWSERS=chrome,firefox tox
 
 
 Using a Selenium hub on local Docker host
@@ -50,10 +49,9 @@ First, get Selenium Hub and one or more browser nodes up and running::
 After that, you can run the test suite, setting the needed variables to use
 Docker Hub::
 
-  poetry run env \
-    TEST_SELENIUM_BROWSERS=chrome,firefox \
-    TEST_SELENIUM_HUB=http://127.0.0.1:4444/wd/hub \
-    TEST_HOST=172.17.0.1 \
+  TEST_SELENIUM_BROWSERS=chrome,firefox \
+  TEST_SELENIUM_HUB=http://127.0.0.1:4444/wd/hub \
+  TEST_HOST=172.17.0.1 \
   tox
 
 The `TEST_HOST` variable is set to the Docker host's IP address, where the
diff --git a/docs/dev/04_materialize_templates.rst b/docs/dev/04_materialize_templates.rst
index 34d582166caa4688ad5e6b2e841d082a4dfd649c..b997c32f293e6268a2deae5b69001abaf9a84e10 100644
--- a/docs/dev/04_materialize_templates.rst
+++ b/docs/dev/04_materialize_templates.rst
@@ -1,9 +1,9 @@
 Materialize templates
 ======================
 
-AlekSIS frontend uses the `MaterializeCSS`_ framework and the `django-material`_ library.
+The AlekSIS frontend uses the `MaterializeCSS`_ framework and the `django-material`_ library.
 
-Internationalization
+Internationalisation
 --------------------
 
 Load the ``i18n`` template tag and start translating strings in templates with
@@ -29,6 +29,51 @@ To fully remove page or browser title, use these template tags::
     {% block no_browser_title %}{% endblock %}
     {% block no_page_title %}{% endblock %}
 
+
+Extended navbar
+---------------
+
+The top navbar with the site title and the login status can be extended by an additional tab bar, for example.
+
+To add normal Materialize tabs without icons, you can use a snippet like described in `Extended Navbar with Tabs`_:
+
+.. code-block:: html+django
+
+    {% block nav_content %}
+        <ul class="tabs tabs-transparent">
+            <li class="tab"><a href="#test1">Test 1</a></li>
+            <li class="tab"><a class="active" href="#test2">Test 2</a></li>
+            <li class="tab disabled"><a href="#test3">Disabled Tab</a></li>
+            <li class="tab"><a href="#test4">Test 4</a></li>
+        </ul>
+    {% endblock %}
+
+Furthermore, you can use tabs with integrated icons that are higher, but more compact in width:
+
+.. code-block:: html+django
+
+    {% block nav_content %}
+        <ul class="tabs tabs-transparent tabs-icons tabs-fixed-width">
+            <li class="tab">
+                <a href="#test1">
+                    <i class="material-icons">speaker_notes</i>
+                    Test 1
+                </a>
+            </li>
+            <li class="tab">
+                <a href="#test2">
+                    <i class="material-icons">people</i>
+                    Test 2
+                </a>
+            </li>
+        </ul>
+    {% endblock %}
+
+
+.. warning::
+
+    Don't define the ``nav_content`` block within the ``content`` block – that won't work.
+
 Forms in templates
 ------------------
 
@@ -66,7 +111,6 @@ In your ``forms.py`` you can configure the layout of the fields like in the Edit
             _("Base data"),
             "short_name",
             Row("user", "primary_group"),
-            "is_active",
             Row("first_name", "additional_name", "last_name"),
         ),
         Fieldset(_("Address"), Row("street", "housenumber"), Row("postal_code", "place")),
@@ -91,3 +135,4 @@ After you've loaded the template tag, you can simply generate the table like thi
 
 .. _MaterializeCSS: https://materializecss.com/
 .. _django-material: https://pypi.org/project/django-material/
+.. _Extended Navbar with Tabs: https://materializecss.com/navbar.html#navbar-tabs
diff --git a/docs/dev/06_merging_app_settings.rst b/docs/dev/06_merging_app_settings.rst
index 5c8bc1f65cced9153946bf36fbd005740e5fabb7..ab24e91138cb7ecb646cd5c299a1f8b9833ed8f7 100644
--- a/docs/dev/06_merging_app_settings.rst
+++ b/docs/dev/06_merging_app_settings.rst
@@ -23,7 +23,4 @@ the following into your ``settings.py``::
             "HOST": "127.0.0.1",
             "PORT": 5432,
         }
-
-If you install new apps and want to configure these, or need some other settings you can easily add
-settings to your ``settings.py``.  Only settings that does not exist in the
-main ``settings.py`` will be respected.
+    }
diff --git a/docs/dev/10_dashboard_widgets.rst b/docs/dev/10_dashboard_widgets.rst
new file mode 100644
index 0000000000000000000000000000000000000000..434c8c9a78cd2f8664f2a783ea997f0d4e1b6df1
--- /dev/null
+++ b/docs/dev/10_dashboard_widgets.rst
@@ -0,0 +1,40 @@
+Registering dashboard widgets
+=============================
+
+Apps can register their own dashboard widgets which are automatically registered in the corresponding frontend for
+configuring them.
+
+To implement a widget, add a model that subclasses ``DashboardWidget``, set the template
+and implement the ``get_context`` method to return a dictionary to be passed as context
+to the template. The template system works as in every Django view and allows you to use the normal Django
+template language.
+
+If your widget does not add any custom database fields, you should mark it as a proxy model.
+
+You can provide a ``Media`` meta class with custom JS and CSS files which
+will be added to the HTML head on the dashboard if the dashboard widget is shown.
+For further information on media definition, see `Django Media`_.
+
+Example::
+
+  from django.forms.widgets import Media
+
+  from aleksis.core.models import DashboardWidget
+
+  class MyWidget(DashboardWidget):
+      template = "myapp/widget.html"
+
+      def get_context(self, request):
+          context = {"some_content": "foo"}
+          return context
+
+      class Meta:
+          proxy = True
+
+      media = Media(css={
+              'all': ('pretty.css',)
+          },
+          js=('animations.js', 'actions.js')
+      )
+
+.. _Django Media: https://docs.djangoproject.com/en/3.0/topics/forms/media/
diff --git a/docs/dev/99_contributing.rst b/docs/dev/99_contributing.rst
deleted file mode 100644
index a5ffcc710e824f4cdcf235f12b25914efcf5ae28..0000000000000000000000000000000000000000
--- a/docs/dev/99_contributing.rst
+++ /dev/null
@@ -1,2 +0,0 @@
-.. include:: ../../CONTRIBUTING.rst
-.. include:: ../../CODE_OF_CONDUCT.rst
diff --git a/docs/index.rst b/docs/index.rst
index aded3b28d2e41186b0f871d0064627bc807a7a29..22d975df1533dfd7e506496c381fcefc9803c349 100644
--- a/docs/index.rst
+++ b/docs/index.rst
@@ -10,6 +10,7 @@ Welcome to AlekSIS-Core's documentation!
    :maxdepth: 2
    :caption: Contents:
 
+   user/00_index
    admin/00_index
    dev/00_index
    ref/00_index
diff --git a/docs/ref/00_index.rst b/docs/ref/00_index.rst
index 1f1524f16bf49c6a0f43cd0dcc4e135bebdd8413..9e452214d47658b936edbde780e69b1622a7bebe 100644
--- a/docs/ref/00_index.rst
+++ b/docs/ref/00_index.rst
@@ -1,5 +1,5 @@
-API and models reference
-========================
+Core API and models reference
+=============================
 
 .. toctree::
    :glob:
diff --git a/docs/user/00_index.rst b/docs/user/00_index.rst
new file mode 100644
index 0000000000000000000000000000000000000000..d8da67c26ad55de8d1dfb29a986bab355bde0803
--- /dev/null
+++ b/docs/user/00_index.rst
@@ -0,0 +1,7 @@
+Using basic features of the AlekSIS Core
+========================================
+
+.. toctree::
+   :glob:
+
+   *
diff --git a/docs/user/01_registration.rst b/docs/user/01_registration.rst
new file mode 100644
index 0000000000000000000000000000000000000000..3beb4fb51621af47b97a51a1cb6719773249ecf7
--- /dev/null
+++ b/docs/user/01_registration.rst
@@ -0,0 +1,36 @@
+Register a new account
+======================
+
+.. _core-user-registration:
+
+Public registration
+-------------------
+
+If public registration is enabled on the AlekSIS instance, you can click the
+``Signup`` button located in the navigation sidebar and register for an
+AlekSIS account.
+
+.. image:: ../_static/signup.png
+  :width: 100%
+  :alt: Signup formular
+
+If enabled, you have to verify your email address after signup. To do so, click
+on the link you recieved on the email address you entered in the signup form.
+
+.. note::
+   Normally, AlekSIS does not allow public registratio, and all accounts are
+   centrally managed by administrators. Public registration is an optional
+   feature that has to be enabled by adminsitrators after careful consideration.
+
+Using an invitation code
+------------------------
+
+If you have an invitation code, click ``Accept invitation`` in the sidebar and
+enter it. You will be redirected to the signup form.
+
+.. image:: ../_static/accept_invite.png
+  :width: 100%
+  :alt: Accept invitation
+
+If you've recieved an invitation link (e.g. via email), clicking the link
+will redirect you to the signup form automatically.
diff --git a/docs/user/02_personal_account.rst b/docs/user/02_personal_account.rst
new file mode 100644
index 0000000000000000000000000000000000000000..a506b9e759ddf0b9f0e8235db784c51c150317c8
--- /dev/null
+++ b/docs/user/02_personal_account.rst
@@ -0,0 +1,95 @@
+Managing your personal account
+==============================
+
+Each logged in user has several options to provided through the AlekSIS
+core. Which of these items are display depends on whether the user has a
+person and what your system administrator has configured.
+
+.. _core-notifications:
+
+Notifications
+-------------
+
+The AlekSIS core has a built-in notification system which can be used by
+apps to send urgent information to specific persons (e. g. timetable
+changes). Notifications are shown on the dashboard and the notifications
+page reachable over the menu entry ``Notifications``. In addition to
+that, notifications can be sent to users through several communication
+channels. These channels can be switched on or off in your personal
+preferences (cf. :ref:`core-user-preferences`).
+
+Setup two-factor authentication
+-------------------------------
+
+
+.. image:: ../_static/2fa.png
+  :width: 100%
+  :alt: Configure two factor authentication
+
+AlekSIS provides two factor authentication using hardware tokens such as
+yubikeys which can generate OTPs or OTP application.
+
+To configure the second factor, visit `Account → 2FA` and follow the
+instructions.
+
+Please keep the backup codes somewhere safe so you do not lose access to
+your account. If you are unable to login with two factor authentication,
+please contact your site administrator.
+
+If you forget to safe your backup codes, but you are still logged in, visit
+`Account → 2FA`, and press `Show codes`.
+
+To disable two factor authentication, login to your account and navigate to
+`Account → 2FA`, then press the big red button to disable 2fa.
+
+Change password
+---------------
+
+If your system administrator has activated this function, you can change
+your password via ``Account → Change password``. If you forgot your
+password, there is a link ``Password forgotten?`` on this page which
+helps with resetting your password. The system then will send you a
+password reset link via email.
+
+Me page
+-------
+
+Reachable under ``Account → Me``, this page shows the personal
+information saved about you in the system. If activated, you can upload
+a picture of yourself or edit some information.
+
+.. _core-user-preferences:
+
+Personal preferences
+--------------------
+
+You can configure some behavior using the preferences under
+``Account → Preferences``. By default, the Core only provides some
+preferences, but apps can extend this list. You can find further
+information about such preferences in the chapter of the respective
+apps.
+
+-  **Notifications**
+
+   -  **Name format for addressing**: Here you can select how AlekSIS
+      should address you.
+   -  **Channels to use for notifications:** This channel is used to
+      sent notifications to you (cf. :ref:`core-notifications`).
+
+Third-party accounts
+--------------------
+
+If you logged in using a third-party account (e. g. a Google or
+Microsoft account), you can manage the connections to these accounts on
+the page ``Account → Third-party accounts``.
+
+The feature to use third-party accounts needs to be enabled by
+an administrator, as described in :doc:`../admin/04_socialaccounts`.
+
+Authorized applications
+-----------------------
+
+On the page ``Account → Authorized applications`` you can see all
+external applications you authorized to retreive data about you from
+AlekSIS. That can be services provided by your local institution like a
+chat platform, for example.
diff --git a/docs/user/10_dashboard.rst b/docs/user/10_dashboard.rst
new file mode 100644
index 0000000000000000000000000000000000000000..20eae0260650350a23c2e64ce39ffbe36c33142d
--- /dev/null
+++ b/docs/user/10_dashboard.rst
@@ -0,0 +1,43 @@
+Dashboard
+=========
+
+The first thing you will see after the login is the dashboard.
+Depending on what your system administrator configured,
+you will be able to see information from different apps at one glance.
+
+.. image:: ../_static/dashboard.png
+  :width: 100%
+  :alt: The dashboard
+
+Dashboard widgets
+-----------------
+
+The dashboard consists of different parts, the so-called *dashboard widgets*.
+They are configured by the system administrator and can be freely
+arranged on the dashboard (cf. :ref:`core-user-customising-dashboard`).
+
+.. _core-user-customising-dashboard:
+
+Customising the dashboard
+-------------------------
+
+There are several options for customising your personal dashboard. By default,
+you will see a layout provided by your system administrator. Using the button
+*Edit dashboard* on the top right corner of the dashboard,
+you can change the selection and position of the widgets.
+
+.. image:: ../_static/edit_dashboard.png
+  :width: 100%
+  :alt: Edit the dashboard
+
+On the edit page, you will see a list of all available widgets and your current dashboard.
+If the section *Your dashboard* is empty, the default dashboard will be shown.
+To make an own layout, you can drag widgets from the *Available widgets* to *Your dashboard*.
+Within *Your dashboard* you also can arrange the widgets by dragging them.
+To remove widgets from the dashboard, you just have to drag them back to *Available widgets*.
+
+In addition to editing the dashboard, you can also change same preferences referring to the dashboard.
+This is done under the menu item *Account → Preferences → General*:
+
+* **Automatically update the dashboard and its widgets:** If enabled by you and the system administrator,
+  the dashboard will be updated automatically every 15 seconds.
diff --git a/docs/user/20_pwa.rst b/docs/user/20_pwa.rst
new file mode 100644
index 0000000000000000000000000000000000000000..461bd113c14747843ac22f66a77bb22bde96d6d3
--- /dev/null
+++ b/docs/user/20_pwa.rst
@@ -0,0 +1,48 @@
+PWA (progressive web application)
+=================================
+
+What is a progressive web application?
+--------------------------------------
+
+A PWA is an application developed with common web technologies and
+delivered in form of a website, but which offers some features a
+traditional website does not and ,overall, creates an impression that
+resembles that of a native application.
+
+AlekSIS PWA features
+--------------------
+
+The AlekSIS PWA offers the following features (not all available on all
+platforms):
+
+-  Installable and displayable in a separate window
+-  Caching and serving, if given page cannot be accessed, of
+   non-interactive pages and needed assets
+-  Provision of an offline fallback page if wanted page cannot be
+   accessed and there is no cached one
+-  Indicator whether the served page is served from the PWA cache
+
+Installation of the PWA
+-----------------------
+
+The procedure to get a native feeling using the AlekSIS PWA varies from
+platform to platform. On some, you are prompted to add AlekSIS to your
+home screen of desktop using a popup; on others, you have to take action
+yourself and find the corresponding menu entry. As of the time of
+writing, “installable” PWAs are supported by all major platforms except
+Firefox Desktop which nevertheless supports the other features.
+
+-  Chromium-based browsers (e.g. Chromium, Google Chrome, Microsoft
+   Edge) will usually prompt you to install the PWA by a popup on both
+   mobile and desktop devices; for the former using a banner and for the
+   latter using an appearing button in the adress bar. In both cases, a
+   click on the notification is enough to start the installation
+   process.
+-  Firefox Mobile will also probably prompt you using a dot near the
+   menu button; then “install” has to been clicked.
+-  On Safari you need to open the share popup and click on the “Add to
+   Home Screen” button.
+
+Independent of the used platform, AlekSIS can be accessed as an
+independent application entry, just like any other installed native
+application, after installation.
diff --git a/poetry.lock b/poetry.lock
deleted file mode 100644
index 5b8035e59280dcc14adefa0cb88178c007ced081..0000000000000000000000000000000000000000
--- a/poetry.lock
+++ /dev/null
@@ -1,4023 +0,0 @@
-[[package]]
-name = "alabaster"
-version = "0.7.12"
-description = "A configurable sidebar-enabled Sphinx theme"
-category = "dev"
-optional = false
-python-versions = "*"
-
-[[package]]
-name = "aleksis-builddeps"
-version = "4"
-description = "AlekSIS (School Information System) — Build/Dev dependencies for apps"
-category = "dev"
-optional = false
-python-versions = ">=3.6,<4.0"
-
-[package.dependencies]
-black = ">=19.10b0,<20.0"
-curlylint = ">=0.12.0,<0.13.0"
-django-stubs = ">=1.1,<2.0"
-flake8 = ">=3.7.9,<4.0.0"
-flake8-bandit = ">=2.1.2,<3.0.0"
-flake8-black = ">=0.2.0,<0.3.0"
-flake8-builtins = ">=1.4.1,<2.0.0"
-flake8-django = ">=1.0.0,<2.0.0"
-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.2.0,<0.3.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"
-pytest-django = ">=4.1,<5.0"
-pytest-django-testing-postgresql = ">=0.1,<0.2"
-pytest-sugar = ">=0.9.2,<0.10.0"
-safety = ">=1.8.5,<2.0.0"
-selenium = ">=3.141.0,<4.0.0"
-sphinx = ">=3.0,<4.0"
-sphinx-autodoc-typehints = ">=1.7,<2.0"
-sphinx_materialdesign_theme = ">=0.1.11,<0.2.0"
-sphinxcontrib-django = ">=0.5.0,<0.6.0"
-
-[package.source]
-type = "legacy"
-url = "https://edugit.org/api/v4/projects/461/packages/pypi/simple"
-reference = "gitlab"
-
-[[package]]
-name = "amqp"
-version = "5.0.6"
-description = "Low-level AMQP client for Python (fork of amqplib)."
-category = "main"
-optional = false
-python-versions = ">=3.6"
-
-[package.dependencies]
-vine = "5.0.0"
-
-[[package]]
-name = "appdirs"
-version = "1.4.4"
-description = "A small Python module for determining appropriate platform-specific dirs, e.g. a \"user data dir\"."
-category = "dev"
-optional = false
-python-versions = "*"
-
-[[package]]
-name = "appnope"
-version = "0.1.2"
-description = "Disable App Nap on macOS >= 10.9"
-category = "main"
-optional = false
-python-versions = "*"
-
-[[package]]
-name = "asgiref"
-version = "3.4.1"
-description = "ASGI specs, helper code, and adapters"
-category = "main"
-optional = false
-python-versions = ">=3.6"
-
-[package.extras]
-tests = ["pytest", "pytest-asyncio", "mypy (>=0.800)"]
-
-[[package]]
-name = "asn1crypto"
-version = "1.4.0"
-description = "Fast ASN.1 parser and serializer with definitions for private keys, public keys, certificates, CRL, OCSP, CMS, PKCS#3, PKCS#7, PKCS#8, PKCS#12, PKCS#5, X.509 and TSP"
-category = "dev"
-optional = false
-python-versions = "*"
-
-[[package]]
-name = "atomicwrites"
-version = "1.4.0"
-description = "Atomic file writes."
-category = "dev"
-optional = false
-python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
-
-[[package]]
-name = "attrs"
-version = "21.2.0"
-description = "Classes Without Boilerplate"
-category = "dev"
-optional = false
-python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
-
-[package.extras]
-dev = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "mypy", "pytest-mypy-plugins", "zope.interface", "furo", "sphinx", "sphinx-notfound-page", "pre-commit"]
-docs = ["furo", "sphinx", "zope.interface", "sphinx-notfound-page"]
-tests = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "mypy", "pytest-mypy-plugins", "zope.interface"]
-tests_no_zope = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "mypy", "pytest-mypy-plugins"]
-
-[[package]]
-name = "babel"
-version = "2.9.1"
-description = "Internationalization utilities"
-category = "dev"
-optional = false
-python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
-
-[package.dependencies]
-pytz = ">=2015.7"
-
-[[package]]
-name = "backcall"
-version = "0.2.0"
-description = "Specifications for callback functions passed in to an API"
-category = "main"
-optional = false
-python-versions = "*"
-
-[[package]]
-name = "bandit"
-version = "1.7.0"
-description = "Security oriented static analyser for python code."
-category = "dev"
-optional = false
-python-versions = ">=3.5"
-
-[package.dependencies]
-colorama = {version = ">=0.3.9", markers = "platform_system == \"Windows\""}
-GitPython = ">=1.0.1"
-PyYAML = ">=5.3.1"
-six = ">=1.10.0"
-stevedore = ">=1.20.0"
-
-[[package]]
-name = "beautifulsoup4"
-version = "4.10.0"
-description = "Screen-scraping library"
-category = "main"
-optional = false
-python-versions = ">3.0.0"
-
-[package.dependencies]
-soupsieve = ">1.2"
-
-[package.extras]
-html5lib = ["html5lib"]
-lxml = ["lxml"]
-
-[[package]]
-name = "billiard"
-version = "3.6.4.0"
-description = "Python multiprocessing fork with improvements and bugfixes"
-category = "main"
-optional = false
-python-versions = "*"
-
-[[package]]
-name = "black"
-version = "19.10b0"
-description = "The uncompromising code formatter."
-category = "dev"
-optional = false
-python-versions = ">=3.6"
-
-[package.dependencies]
-appdirs = "*"
-attrs = ">=18.1.0"
-click = ">=6.5"
-pathspec = ">=0.6,<1"
-regex = "*"
-toml = ">=0.9.4"
-typed-ast = ">=1.4.0"
-
-[package.extras]
-d = ["aiohttp (>=3.3.2)", "aiohttp-cors"]
-
-[[package]]
-name = "bleach"
-version = "4.1.0"
-description = "An easy safelist-based HTML-sanitizing tool."
-category = "main"
-optional = false
-python-versions = ">=3.6"
-
-[package.dependencies]
-packaging = "*"
-six = ">=1.9.0"
-webencodings = "*"
-
-[[package]]
-name = "boolean.py"
-version = "3.8"
-description = "Define boolean algebras, create and parse boolean expressions and create custom boolean DSL."
-category = "main"
-optional = false
-python-versions = "*"
-
-[[package]]
-name = "boto3"
-version = "1.19.3"
-description = "The AWS SDK for Python"
-category = "main"
-optional = true
-python-versions = ">= 3.6"
-
-[package.dependencies]
-botocore = ">=1.22.3,<1.23.0"
-jmespath = ">=0.7.1,<1.0.0"
-s3transfer = ">=0.5.0,<0.6.0"
-
-[package.extras]
-crt = ["botocore[crt] (>=1.21.0,<2.0a0)"]
-
-[[package]]
-name = "botocore"
-version = "1.22.3"
-description = "Low-level, data-driven core of boto 3."
-category = "main"
-optional = true
-python-versions = ">= 3.6"
-
-[package.dependencies]
-jmespath = ">=0.7.1,<1.0.0"
-python-dateutil = ">=2.1,<3.0.0"
-urllib3 = ">=1.25.4,<1.27"
-
-[package.extras]
-crt = ["awscrt (==0.12.5)"]
-
-[[package]]
-name = "bs4"
-version = "0.0.1"
-description = "Dummy package for Beautiful Soup"
-category = "main"
-optional = false
-python-versions = "*"
-
-[package.dependencies]
-beautifulsoup4 = "*"
-
-[[package]]
-name = "calendarweek"
-version = "0.5.0"
-description = "Utilities for working with calendar weeks in Python and Django"
-category = "main"
-optional = false
-python-versions = ">=3.7,<4.0"
-
-[package.extras]
-django = ["Django (>=2.2,<4.0)"]
-
-[[package]]
-name = "celery"
-version = "5.1.2"
-description = "Distributed Task Queue."
-category = "main"
-optional = false
-python-versions = ">=3.6,"
-
-[package.dependencies]
-billiard = ">=3.6.4.0,<4.0"
-click = ">=7.0,<8.0"
-click-didyoumean = ">=0.0.3"
-click-plugins = ">=1.1.1"
-click-repl = ">=0.1.6"
-Django = {version = ">=1.11", optional = true, markers = "extra == \"django\""}
-kombu = ">=5.1.0,<6.0"
-pytz = ">0.0-dev"
-redis = {version = ">=3.2.0", optional = true, markers = "extra == \"redis\""}
-vine = ">=5.0.0,<6.0"
-
-[package.extras]
-arangodb = ["pyArango (>=1.3.2)"]
-auth = ["cryptography"]
-azureblockblob = ["azure-storage-blob (==12.6.0)"]
-brotli = ["brotli (>=1.0.0)", "brotlipy (>=0.7.0)"]
-cassandra = ["cassandra-driver (<3.21.0)"]
-consul = ["python-consul2"]
-cosmosdbsql = ["pydocumentdb (==2.3.2)"]
-couchbase = ["couchbase (>=3.0.0)"]
-couchdb = ["pycouchdb"]
-django = ["Django (>=1.11)"]
-dynamodb = ["boto3 (>=1.9.178)"]
-elasticsearch = ["elasticsearch"]
-eventlet = ["eventlet (>=0.26.1)"]
-gevent = ["gevent (>=1.0.0)"]
-librabbitmq = ["librabbitmq (>=1.5.0)"]
-memcache = ["pylibmc"]
-mongodb = ["pymongo[srv] (>=3.3.0)"]
-msgpack = ["msgpack"]
-pymemcache = ["python-memcached"]
-pyro = ["pyro4"]
-pytest = ["pytest-celery"]
-redis = ["redis (>=3.2.0)"]
-s3 = ["boto3 (>=1.9.125)"]
-slmq = ["softlayer-messaging (>=1.0.3)"]
-solar = ["ephem"]
-sqlalchemy = ["sqlalchemy"]
-sqs = ["boto3 (>=1.9.125)", "pycurl (==7.43.0.5)"]
-tblib = ["tblib (>=1.3.0)", "tblib (>=1.5.0)"]
-yaml = ["PyYAML (>=3.10)"]
-zookeeper = ["kazoo (>=1.3.1)"]
-zstd = ["zstandard"]
-
-[[package]]
-name = "celery-haystack-ng"
-version = "0.20.post2"
-description = "An app for integrating Celery with Haystack"
-category = "main"
-optional = false
-python-versions = "*"
-
-[package.dependencies]
-celery = ">=4.0"
-django-appconf = ">=0.4.1"
-django-haystack = ">=2.0"
-
-[[package]]
-name = "celery-progress"
-version = "0.1.1"
-description = "Drop in, configurable, dependency-free progress bars for your Django/Celery applications."
-category = "main"
-optional = false
-python-versions = "*"
-
-[package.extras]
-rabbitmq = ["channels-rabbitmq"]
-redis = ["channels-redis"]
-websockets = ["channels"]
-
-[[package]]
-name = "certifi"
-version = "2021.10.8"
-description = "Python package for providing Mozilla's CA Bundle."
-category = "main"
-optional = false
-python-versions = "*"
-
-[[package]]
-name = "cffi"
-version = "1.15.0"
-description = "Foreign Function Interface for Python calling C code."
-category = "main"
-optional = false
-python-versions = "*"
-
-[package.dependencies]
-pycparser = "*"
-
-[[package]]
-name = "charset-normalizer"
-version = "2.0.7"
-description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet."
-category = "main"
-optional = false
-python-versions = ">=3.5.0"
-
-[package.extras]
-unicode_backport = ["unicodedata2"]
-
-[[package]]
-name = "click"
-version = "7.1.2"
-description = "Composable command line interface toolkit"
-category = "main"
-optional = false
-python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
-
-[[package]]
-name = "click-didyoumean"
-version = "0.3.0"
-description = "Enables git-like *did-you-mean* feature in click"
-category = "main"
-optional = false
-python-versions = ">=3.6.2,<4.0.0"
-
-[package.dependencies]
-click = ">=7"
-
-[[package]]
-name = "click-plugins"
-version = "1.1.1"
-description = "An extension module for click to enable registering CLI commands via setuptools entry-points."
-category = "main"
-optional = false
-python-versions = "*"
-
-[package.dependencies]
-click = ">=4.0"
-
-[package.extras]
-dev = ["pytest (>=3.6)", "pytest-cov", "wheel", "coveralls"]
-
-[[package]]
-name = "click-repl"
-version = "0.2.0"
-description = "REPL plugin for Click"
-category = "main"
-optional = false
-python-versions = "*"
-
-[package.dependencies]
-click = "*"
-prompt-toolkit = "*"
-six = "*"
-
-[[package]]
-name = "colorama"
-version = "0.4.4"
-description = "Cross-platform colored terminal text."
-category = "main"
-optional = false
-python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
-
-[[package]]
-name = "colour"
-version = "0.1.5"
-description = "converts and manipulates various color representation (HSL, RVB, web, X11, ...)"
-category = "main"
-optional = false
-python-versions = "*"
-
-[package.extras]
-test = ["nose"]
-
-[[package]]
-name = "configobj"
-version = "5.0.6"
-description = "Config file reading, writing and validation."
-category = "main"
-optional = false
-python-versions = "*"
-
-[package.dependencies]
-six = "*"
-
-[[package]]
-name = "coverage"
-version = "6.0.2"
-description = "Code coverage measurement for Python"
-category = "dev"
-optional = false
-python-versions = ">=3.6"
-
-[package.extras]
-toml = ["tomli"]
-
-[[package]]
-name = "cryptography"
-version = "35.0.0"
-description = "cryptography is a package which provides cryptographic recipes and primitives to Python developers."
-category = "main"
-optional = false
-python-versions = ">=3.6"
-
-[package.dependencies]
-cffi = ">=1.12"
-
-[package.extras]
-docs = ["sphinx (>=1.6.5,!=1.8.0,!=3.1.0,!=3.1.1)", "sphinx-rtd-theme"]
-docstest = ["doc8", "pyenchant (>=1.6.11)", "twine (>=1.12.0)", "sphinxcontrib-spelling (>=4.0.1)"]
-pep8test = ["black", "flake8", "flake8-import-order", "pep8-naming"]
-sdist = ["setuptools_rust (>=0.11.4)"]
-ssh = ["bcrypt (>=3.1.5)"]
-test = ["pytest (>=6.2.0)", "pytest-cov", "pytest-subtests", "pytest-xdist", "pretend", "iso8601", "pytz", "hypothesis (>=1.11.4,!=3.79.2)"]
-
-[[package]]
-name = "curlylint"
-version = "0.12.2"
-description = "{{ 🎀}} Experimental HTML templates linting for Jinja, Nunjucks, Django templates, Twig, Liquid"
-category = "dev"
-optional = false
-python-versions = ">=3.6"
-
-[package.dependencies]
-attrs = ">=17.2.0"
-click = ">=6.5"
-parsy = "1.1.0"
-pathspec = ">=0.6,<1"
-toml = ">=0.9.4"
-
-[package.extras]
-dev = ["black (==19.10b0)", "flake8 (==3.8.4)", "mypy (==0.812)", "pytest (==6.2.2)", "coverage (==5.4)"]
-
-[[package]]
-name = "decorator"
-version = "5.1.0"
-description = "Decorators for Humans"
-category = "main"
-optional = false
-python-versions = ">=3.5"
-
-[[package]]
-name = "defusedxml"
-version = "0.7.1"
-description = "XML bomb protection for Python stdlib modules"
-category = "main"
-optional = false
-python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
-
-[[package]]
-name = "deprecated"
-version = "1.2.13"
-description = "Python @deprecated decorator to deprecate old python classes, functions or methods."
-category = "main"
-optional = false
-python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
-
-[package.dependencies]
-wrapt = ">=1.10,<2"
-
-[package.extras]
-dev = ["tox", "bump2version (<1)", "sphinx (<2)", "importlib-metadata (<3)", "importlib-resources (<4)", "configparser (<5)", "sphinxcontrib-websupport (<2)", "zipp (<2)", "PyTest (<5)", "PyTest-Cov (<2.6)", "pytest", "pytest-cov"]
-
-[[package]]
-name = "dj-database-url"
-version = "0.5.0"
-description = "Use Database URLs in your Django Application."
-category = "dev"
-optional = false
-python-versions = "*"
-
-[[package]]
-name = "django"
-version = "3.2.8"
-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.3.2,<4"
-pytz = "*"
-sqlparse = ">=0.2.2"
-
-[package.extras]
-argon2 = ["argon2-cffi (>=19.1.0)"]
-bcrypt = ["bcrypt"]
-
-[[package]]
-name = "django-allauth"
-version = "0.45.0"
-description = "Integrated set of Django applications addressing authentication, registration, account management as well as 3rd party (social) account authentication."
-category = "main"
-optional = false
-python-versions = "*"
-
-[package.dependencies]
-Django = ">=2.0"
-pyjwt = {version = ">=1.7", extras = ["crypto"]}
-python3-openid = ">=3.0.8"
-requests = "*"
-requests-oauthlib = ">=0.3.0"
-
-[[package]]
-name = "django-any-js"
-version = "1.1"
-description = "Include JavaScript/CSS libraries with readable template tags"
-category = "main"
-optional = false
-python-versions = ">=3.7,<4.0"
-
-[package.dependencies]
-Django = ">=2.2,<4.0"
-
-[[package]]
-name = "django-appconf"
-version = "1.0.5"
-description = "A helper class for handling configuration defaults of packaged apps gracefully."
-category = "main"
-optional = false
-python-versions = ">=3.6"
-
-[package.dependencies]
-django = "*"
-
-[[package]]
-name = "django-auth-ldap"
-version = "2.4.0"
-description = "Django LDAP authentication backend."
-category = "main"
-optional = true
-python-versions = ">=3.6"
-
-[package.dependencies]
-Django = ">=2.2"
-python-ldap = ">=3.1"
-
-[[package]]
-name = "django-bleach"
-version = "0.8.0"
-description = "Easily use bleach with Django models and templates"
-category = "main"
-optional = false
-python-versions = "*"
-
-[package.dependencies]
-bleach = ">=1.5.0"
-Django = ">=1.11"
-
-[[package]]
-name = "django-cachalot"
-version = "2.4.3"
-description = "Caches your Django ORM queries and automatically invalidates them."
-category = "main"
-optional = false
-python-versions = "*"
-
-[package.dependencies]
-Django = ">=2.2,<3.3"
-
-[[package]]
-name = "django-cache-memoize"
-version = "0.1.10"
-description = "Django utility for a memoization decorator that uses the Django cache framework."
-category = "main"
-optional = false
-python-versions = ">=3.5"
-
-[package.extras]
-dev = ["flake8", "tox", "twine", "therapist", "black"]
-
-[[package]]
-name = "django-celery-beat"
-version = "2.2.1"
-description = "Database-backed Periodic Tasks."
-category = "main"
-optional = false
-python-versions = "*"
-
-[package.dependencies]
-celery = ">=5.0,<6.0"
-Django = ">=2.2,<4.0"
-django-timezone-field = ">=4.1.0,<5.0"
-python-crontab = ">=2.3.4"
-
-[[package]]
-name = "django-celery-email"
-version = "3.0.0"
-description = "An async Django email backend using celery"
-category = "main"
-optional = false
-python-versions = "*"
-
-[package.dependencies]
-celery = ">=4.0"
-django = ">=2.2"
-django-appconf = "*"
-
-[[package]]
-name = "django-celery-results"
-version = "2.2.0"
-description = "Celery result backends for Django."
-category = "main"
-optional = false
-python-versions = "*"
-
-[package.dependencies]
-celery = ">=5.0,<6.0"
-
-[[package]]
-name = "django-ckeditor"
-version = "6.1.0"
-description = "Django admin CKEditor integration."
-category = "main"
-optional = false
-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.5"
-description = "simple color field for your models with a nice color-picker in the admin-interface."
-category = "main"
-optional = false
-python-versions = "*"
-
-[[package]]
-name = "django-dbbackup"
-version = "3.3.0"
-description = "Management commands to help backup and restore a project database and media"
-category = "main"
-optional = false
-python-versions = "*"
-
-[package.dependencies]
-Django = ">=1.5"
-pytz = "*"
-six = "*"
-
-[[package]]
-name = "django-debug-toolbar"
-version = "3.2.2"
-description = "A configurable set of panels that display various debug information about the current request/response."
-category = "main"
-optional = false
-python-versions = ">=3.6"
-
-[package.dependencies]
-Django = ">=2.2"
-sqlparse = ">=0.2.0"
-
-[[package]]
-name = "django-dynamic-preferences"
-version = "1.11.0"
-description = "Dynamic global and instance settings for your django project"
-category = "main"
-optional = false
-python-versions = "*"
-
-[package.dependencies]
-django = ">=1.11"
-persisting-theory = ">=0.2.1"
-six = "*"
-
-[[package]]
-name = "django-extensions"
-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.1.3"
-description = "simple Django app which allows you to upload a image and it renders a wide variety for html link tags to display the favicon"
-category = "main"
-optional = false
-python-versions = "*"
-
-[package.dependencies]
-django = "*"
-pillow = "*"
-
-[[package]]
-name = "django-filter"
-version = "2.4.0"
-description = "Django-filter is a reusable Django application for allowing users to filter querysets dynamically."
-category = "main"
-optional = false
-python-versions = ">=3.5"
-
-[package.dependencies]
-Django = ">=2.2"
-
-[[package]]
-name = "django-formtools"
-version = "2.3"
-description = "A set of high-level abstractions for Django forms"
-category = "main"
-optional = false
-python-versions = ">=3.6"
-
-[package.dependencies]
-Django = ">=2.2"
-
-[[package]]
-name = "django-guardian"
-version = "2.4.0"
-description = "Implementation of per object permissions for Django."
-category = "main"
-optional = false
-python-versions = ">=3.5"
-
-[package.dependencies]
-Django = ">=2.2"
-
-[[package]]
-name = "django-haystack"
-version = "3.0"
-description = "Pluggable search for Django."
-category = "main"
-optional = false
-python-versions = "*"
-
-[package.dependencies]
-Django = ">=2.2"
-
-[[package]]
-name = "django-health-check"
-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 = ">=2.2"
-
-[[package]]
-name = "django-impersonate"
-version = "1.7.3"
-description = "Django app to allow superusers to impersonate other users."
-category = "main"
-optional = false
-python-versions = "*"
-
-[[package]]
-name = "django-ipware"
-version = "4.0.0"
-description = "A Django application to retrieve user's IP address"
-category = "main"
-optional = false
-python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*"
-
-[[package]]
-name = "django-js-asset"
-version = "1.2.2"
-description = "script tag with additional attributes for django.forms.Media"
-category = "main"
-optional = false
-python-versions = "*"
-
-[[package]]
-name = "django-js-reverse"
-version = "0.9.1"
-description = "Javascript url handling for Django that doesn't hurt."
-category = "main"
-optional = false
-python-versions = "*"
-
-[package.dependencies]
-Django = ">=1.5"
-
-[[package]]
-name = "django-jsonstore"
-version = "0.5.0"
-description = "Expose JSONField data as a virtual django model fields."
-category = "main"
-optional = false
-python-versions = "*"
-
-[package.dependencies]
-Django = ">=1.11"
-six = "*"
-
-[[package]]
-name = "django-maintenance-mode"
-version = "0.16.1"
-description = "django-maintenance-mode shows a 503 error page when maintenance-mode is on."
-category = "main"
-optional = false
-python-versions = "*"
-
-[[package]]
-name = "django-material"
-version = "1.9.0"
-description = "Material design for django forms and admin"
-category = "main"
-optional = false
-python-versions = "*"
-
-[package.dependencies]
-six = "*"
-
-[[package]]
-name = "django-menu-generator-ng"
-version = "1.2.3"
-description = "A straightforward menu generator for Django"
-category = "main"
-optional = false
-python-versions = "*"
-
-[[package]]
-name = "django-model-utils"
-version = "4.2.0"
-description = "Django model mixins and utilities"
-category = "main"
-optional = false
-python-versions = "*"
-
-[package.dependencies]
-Django = ">=2.0.1"
-
-[[package]]
-name = "django-oauth-toolkit"
-version = "1.5.0"
-description = "OAuth2 Provider for Django"
-category = "main"
-optional = false
-python-versions = "*"
-
-[package.dependencies]
-django = ">=2.2"
-jwcrypto = ">=0.8.0"
-oauthlib = ">=3.1.0"
-requests = ">=2.13.0"
-six = "*"
-
-[[package]]
-name = "django-otp"
-version = "1.1.1"
-description = "A pluggable framework for adding two-factor authentication to Django using one-time passwords."
-category = "main"
-optional = false
-python-versions = "*"
-
-[package.dependencies]
-django = ">=2.2"
-
-[package.extras]
-qrcode = ["qrcode"]
-
-[[package]]
-name = "django-otp-yubikey"
-version = "1.0.0.post1"
-description = "A django-otp plugin that verifies YubiKey OTP tokens."
-category = "main"
-optional = false
-python-versions = "*"
-
-[package.dependencies]
-django-otp = ">=1.0.0"
-YubiOTP = ">=0.2.2"
-
-[[package]]
-name = "django-phonenumber-field"
-version = "5.2.0"
-description = "An international phone number field for django models."
-category = "main"
-optional = false
-python-versions = ">=3.6"
-
-[package.dependencies]
-Django = ">=2.2"
-phonenumbers = {version = ">=7.0.2", optional = true, markers = "extra == \"phonenumbers\""}
-
-[package.extras]
-phonenumbers = ["phonenumbers (>=7.0.2)"]
-phonenumberslite = ["phonenumberslite (>=7.0.2)"]
-
-[[package]]
-name = "django-polymorphic"
-version = "3.0.0"
-description = "Seamless polymorphic inheritance for Django models"
-category = "main"
-optional = false
-python-versions = "*"
-
-[package.dependencies]
-Django = ">=2.1"
-
-[[package]]
-name = "django-prometheus"
-version = "2.1.0"
-description = "Django middlewares to monitor your application with Prometheus.io."
-category = "main"
-optional = false
-python-versions = "*"
-
-[package.dependencies]
-prometheus-client = ">=0.7"
-
-[[package]]
-name = "django-redis"
-version = "5.0.0"
-description = "Full featured redis cache backend for Django."
-category = "main"
-optional = false
-python-versions = ">=3.6"
-
-[package.dependencies]
-Django = ">=2.2"
-redis = ">=3.0.0"
-
-[[package]]
-name = "django-render-block"
-version = "0.8.1"
-description = "Render a particular block from a template to a string."
-category = "main"
-optional = false
-python-versions = ">=3.5"
-
-[package.dependencies]
-django = ">=2.2"
-
-[[package]]
-name = "django-reversion"
-version = "4.0.0"
-description = "An extension to the Django web framework that provides version control for model instances."
-category = "main"
-optional = false
-python-versions = ">=3.6"
-
-[package.dependencies]
-django = ">=2.0"
-
-[[package]]
-name = "django-sass-processor"
-version = "1.0.0"
-description = "SASS processor to compile SCSS files into *.css, while rendering, or offline."
-category = "main"
-optional = false
-python-versions = "*"
-
-[package.extras]
-management_command = ["django-compressor (>=2.4)"]
-
-[[package]]
-name = "django-select2"
-version = "7.7.3"
-description = "Select2 option fields for Django"
-category = "main"
-optional = false
-python-versions = "*"
-
-[package.dependencies]
-django = ">=2.2"
-django-appconf = ">=0.6.0"
-
-[package.extras]
-test = ["pytest", "pytest-cov", "pytest-django", "selenium"]
-
-[[package]]
-name = "django-storages"
-version = "1.12.2"
-description = "Support for many storage backends in Django"
-category = "main"
-optional = true
-python-versions = ">=3.5"
-
-[package.dependencies]
-Django = ">=2.2"
-
-[package.extras]
-azure = ["azure-storage-blob (>=12.0.0)"]
-boto3 = ["boto3 (>=1.4.4)"]
-dropbox = ["dropbox (>=7.2.1)"]
-google = ["google-cloud-storage (>=1.27.0)"]
-libcloud = ["apache-libcloud"]
-sftp = ["paramiko"]
-
-[[package]]
-name = "django-stubs"
-version = "1.9.0"
-description = "Mypy stubs for Django"
-category = "dev"
-optional = false
-python-versions = ">=3.6"
-
-[package.dependencies]
-django = "*"
-django-stubs-ext = ">=0.3.0"
-mypy = ">=0.910"
-toml = "*"
-types-pytz = "*"
-types-PyYAML = "*"
-typing-extensions = "*"
-
-[[package]]
-name = "django-stubs-ext"
-version = "0.3.1"
-description = "Monkey-patching and extensions for django-stubs"
-category = "dev"
-optional = false
-python-versions = ">=3.6"
-
-[package.dependencies]
-django = "*"
-typing-extensions = "*"
-
-[[package]]
-name = "django-tables2"
-version = "2.4.1"
-description = "Table/data-grid framework for Django"
-category = "main"
-optional = false
-python-versions = "*"
-
-[package.dependencies]
-Django = ">=1.11"
-
-[package.extras]
-tablib = ["tablib"]
-
-[[package]]
-name = "django-templated-email"
-version = "3.0.0"
-description = "A Django oriented templated / transaction email abstraction"
-category = "main"
-optional = false
-python-versions = "*"
-
-[package.dependencies]
-django-render-block = ">=0.5"
-
-[[package]]
-name = "django-timezone-field"
-version = "4.2.1"
-description = "A Django app providing database and form fields for pytz timezone objects."
-category = "main"
-optional = false
-python-versions = ">=3.5"
-
-[package.dependencies]
-django = ">=2.2"
-pytz = "*"
-
-[package.extras]
-rest_framework = ["djangorestframework (>=3.0.0)"]
-
-[[package]]
-name = "django-titofisto"
-version = "0.1.2.post1"
-description = "Django Time-Token File Storage"
-category = "main"
-optional = false
-python-versions = ">=3.9,<4.0"
-
-[package.dependencies]
-Django = ">2.2,<4.0"
-
-[[package]]
-name = "django-two-factor-auth"
-version = "1.13.1"
-description = "Complete Two-Factor Authentication for Django"
-category = "main"
-optional = false
-python-versions = "*"
-
-[package.dependencies]
-Django = ">=2.2"
-django-formtools = "*"
-django-otp = ">=0.8.0"
-django-otp-yubikey = {version = "*", optional = true, markers = "extra == \"yubikey\""}
-django-phonenumber-field = ">=1.1.0,<6"
-phonenumbers = {version = ">=7.0.9,<8.99", optional = true, markers = "extra == \"phonenumbers\""}
-qrcode = ">=4.0.0,<6.99"
-twilio = {version = ">=6.0", optional = true, markers = "extra == \"call\""}
-
-[package.extras]
-call = ["twilio (>=6.0)"]
-phonenumbers = ["phonenumbers (>=7.0.9,<8.99)"]
-phonenumberslite = ["phonenumberslite (>=7.0.9,<8.99)"]
-sms = ["twilio (>=6.0)"]
-yubikey = ["django-otp-yubikey"]
-
-[[package]]
-name = "django-uwsgi-ng"
-version = "1.1.2"
-description = "uWSGI stuff for Django projects"
-category = "main"
-optional = false
-python-versions = "*"
-
-[package.extras]
-uwsgi = ["uwsgi"]
-
-[[package]]
-name = "django-widget-tweaks"
-version = "1.4.9"
-description = "Tweak the form field rendering in templates, not in python-level form definitions."
-category = "main"
-optional = false
-python-versions = "*"
-
-[[package]]
-name = "django-yarnpkg"
-version = "6.0.1"
-description = "Integrate django with yarnpkg"
-category = "main"
-optional = false
-python-versions = "*"
-
-[package.dependencies]
-django = "*"
-six = "*"
-
-[[package]]
-name = "djangorestframework"
-version = "3.12.4"
-description = "Web APIs for Django, made easy."
-category = "main"
-optional = false
-python-versions = ">=3.5"
-
-[package.dependencies]
-django = ">=2.2"
-
-[[package]]
-name = "docutils"
-version = "0.16"
-description = "Docutils -- Python Documentation Utilities"
-category = "dev"
-optional = false
-python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
-
-[[package]]
-name = "dparse"
-version = "0.5.1"
-description = "A parser for Python dependency files"
-category = "dev"
-optional = false
-python-versions = ">=3.5"
-
-[package.dependencies]
-packaging = "*"
-pyyaml = "*"
-toml = "*"
-
-[package.extras]
-pipenv = ["pipenv"]
-
-[[package]]
-name = "dynaconf"
-version = "3.1.7"
-description = "The dynamic configurator for your Python Project"
-category = "main"
-optional = false
-python-versions = ">=3.7"
-
-[package.dependencies]
-configobj = {version = "*", optional = true, markers = "extra == \"ini\""}
-"ruamel.yaml" = {version = "*", optional = true, markers = "extra == \"yaml\""}
-toml = {version = "*", optional = true, markers = "extra == \"toml\""}
-
-[package.extras]
-all = ["redis", "ruamel.yaml", "configobj", "hvac"]
-configobj = ["configobj"]
-ini = ["configobj"]
-redis = ["redis"]
-toml = ["toml"]
-vault = ["hvac"]
-yaml = ["ruamel.yaml"]
-
-[[package]]
-name = "flake8"
-version = "3.9.2"
-description = "the modular source code checker: pep8 pyflakes and co"
-category = "dev"
-optional = false
-python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7"
-
-[package.dependencies]
-mccabe = ">=0.6.0,<0.7.0"
-pycodestyle = ">=2.7.0,<2.8.0"
-pyflakes = ">=2.3.0,<2.4.0"
-
-[[package]]
-name = "flake8-bandit"
-version = "2.1.2"
-description = "Automated security testing with bandit and flake8."
-category = "dev"
-optional = false
-python-versions = "*"
-
-[package.dependencies]
-bandit = "*"
-flake8 = "*"
-flake8-polyfill = "*"
-pycodestyle = "*"
-
-[[package]]
-name = "flake8-black"
-version = "0.2.3"
-description = "flake8 plugin to call black as a code style validator"
-category = "dev"
-optional = false
-python-versions = "*"
-
-[package.dependencies]
-black = "*"
-flake8 = ">=3.0.0"
-toml = "*"
-
-[[package]]
-name = "flake8-builtins"
-version = "1.5.3"
-description = "Check for python builtins being used as variables or parameters."
-category = "dev"
-optional = false
-python-versions = "*"
-
-[package.dependencies]
-flake8 = "*"
-
-[package.extras]
-test = ["coverage", "coveralls", "mock", "pytest", "pytest-cov"]
-
-[[package]]
-name = "flake8-django"
-version = "1.1.2"
-description = "Plugin to catch bad style specific to Django Projects."
-category = "dev"
-optional = false
-python-versions = ">=3.6,<4.0"
-
-[package.dependencies]
-flake8 = ">=3.8.4,<4.0.0"
-
-[[package]]
-name = "flake8-docstrings"
-version = "1.6.0"
-description = "Extension for flake8 which uses pydocstyle to check docstrings"
-category = "dev"
-optional = false
-python-versions = "*"
-
-[package.dependencies]
-flake8 = ">=3"
-pydocstyle = ">=2.1"
-
-[[package]]
-name = "flake8-fixme"
-version = "1.1.1"
-description = "Check for FIXME, TODO and other temporary developer notes. Plugin for flake8."
-category = "dev"
-optional = false
-python-versions = "*"
-
-[[package]]
-name = "flake8-isort"
-version = "4.1.1"
-description = "flake8 plugin that integrates isort ."
-category = "dev"
-optional = false
-python-versions = "*"
-
-[package.dependencies]
-flake8 = ">=3.2.1,<5"
-isort = ">=4.3.5,<6"
-testfixtures = ">=6.8.0,<7"
-
-[package.extras]
-test = ["pytest-cov"]
-
-[[package]]
-name = "flake8-mypy"
-version = "17.8.0"
-description = "A plugin for flake8 integrating mypy."
-category = "dev"
-optional = false
-python-versions = "*"
-
-[package.dependencies]
-attrs = "*"
-flake8 = ">=3.0.0"
-mypy = "*"
-
-[[package]]
-name = "flake8-polyfill"
-version = "1.0.2"
-description = "Polyfill package for Flake8 plugins"
-category = "dev"
-optional = false
-python-versions = "*"
-
-[package.dependencies]
-flake8 = "*"
-
-[[package]]
-name = "flake8-rst-docstrings"
-version = "0.2.3"
-description = "Python docstring reStructuredText (RST) validator"
-category = "dev"
-optional = false
-python-versions = ">=3.3"
-
-[package.dependencies]
-flake8 = ">=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"
-version = "4.0.9"
-description = "Git Object Database"
-category = "dev"
-optional = false
-python-versions = ">=3.6"
-
-[package.dependencies]
-smmap = ">=3.0.1,<6"
-
-[[package]]
-name = "gitpython"
-version = "3.1.24"
-description = "GitPython is a python library used to interact with Git repositories"
-category = "dev"
-optional = false
-python-versions = ">=3.7"
-
-[package.dependencies]
-gitdb = ">=4.0.1,<5"
-typing-extensions = {version = ">=3.7.4.3", markers = "python_version < \"3.10\""}
-
-[[package]]
-name = "haystack-redis"
-version = "0.0.1"
-description = "Use redis as a persistence layer for Whoosh and Haystack"
-category = "main"
-optional = false
-python-versions = "*"
-
-[package.dependencies]
-django-haystack = "*"
-redis = "*"
-whoosh = "*"
-
-[[package]]
-name = "html2text"
-version = "2020.1.16"
-description = "Turn HTML into equivalent Markdown-structured text."
-category = "main"
-optional = false
-python-versions = ">=3.5"
-
-[[package]]
-name = "idna"
-version = "3.3"
-description = "Internationalized Domain Names in Applications (IDNA)"
-category = "main"
-optional = false
-python-versions = ">=3.5"
-
-[[package]]
-name = "imagesize"
-version = "1.2.0"
-description = "Getting image size from png/jpeg/jpeg2000/gif file"
-category = "dev"
-optional = false
-python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
-
-[[package]]
-name = "iniconfig"
-version = "1.1.1"
-description = "iniconfig: brain-dead simple config-ini parsing"
-category = "dev"
-optional = false
-python-versions = "*"
-
-[[package]]
-name = "ipython"
-version = "7.28.0"
-description = "IPython: Productive Interactive Computing"
-category = "main"
-optional = false
-python-versions = ">=3.7"
-
-[package.dependencies]
-appnope = {version = "*", markers = "sys_platform == \"darwin\""}
-backcall = "*"
-colorama = {version = "*", markers = "sys_platform == \"win32\""}
-decorator = "*"
-jedi = ">=0.16"
-matplotlib-inline = "*"
-pexpect = {version = ">4.3", markers = "sys_platform != \"win32\""}
-pickleshare = "*"
-prompt-toolkit = ">=2.0.0,<3.0.0 || >3.0.0,<3.0.1 || >3.0.1,<3.1.0"
-pygments = "*"
-traitlets = ">=4.2"
-
-[package.extras]
-all = ["Sphinx (>=1.3)", "ipykernel", "ipyparallel", "ipywidgets", "nbconvert", "nbformat", "nose (>=0.10.1)", "notebook", "numpy (>=1.17)", "pygments", "qtconsole", "requests", "testpath"]
-doc = ["Sphinx (>=1.3)"]
-kernel = ["ipykernel"]
-nbconvert = ["nbconvert"]
-nbformat = ["nbformat"]
-notebook = ["notebook", "ipywidgets"]
-parallel = ["ipyparallel"]
-qtconsole = ["qtconsole"]
-test = ["nose (>=0.10.1)", "requests", "testpath", "pygments", "nbformat", "ipykernel", "numpy (>=1.17)"]
-
-[[package]]
-name = "isort"
-version = "5.9.3"
-description = "A Python utility / library to sort Python imports."
-category = "dev"
-optional = false
-python-versions = ">=3.6.1,<4.0"
-
-[package.extras]
-pipfile_deprecated_finder = ["pipreqs", "requirementslib"]
-requirements_deprecated_finder = ["pipreqs", "pip-api"]
-colors = ["colorama (>=0.4.3,<0.5.0)"]
-plugins = ["setuptools"]
-
-[[package]]
-name = "jedi"
-version = "0.18.0"
-description = "An autocompletion tool for Python that can be used for text editors."
-category = "main"
-optional = false
-python-versions = ">=3.6"
-
-[package.dependencies]
-parso = ">=0.8.0,<0.9.0"
-
-[package.extras]
-qa = ["flake8 (==3.8.3)", "mypy (==0.782)"]
-testing = ["Django (<3.1)", "colorama", "docopt", "pytest (<6.0.0)"]
-
-[[package]]
-name = "jinja2"
-version = "3.0.2"
-description = "A very fast and expressive template engine."
-category = "dev"
-optional = false
-python-versions = ">=3.6"
-
-[package.dependencies]
-MarkupSafe = ">=2.0"
-
-[package.extras]
-i18n = ["Babel (>=2.7)"]
-
-[[package]]
-name = "jmespath"
-version = "0.10.0"
-description = "JSON Matching Expressions"
-category = "main"
-optional = true
-python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*"
-
-[[package]]
-name = "jwcrypto"
-version = "1.0"
-description = "Implementation of JOSE Web standards"
-category = "main"
-optional = false
-python-versions = "*"
-
-[package.dependencies]
-cryptography = ">=2.3"
-deprecated = "*"
-
-[[package]]
-name = "kombu"
-version = "5.1.0"
-description = "Messaging library for Python."
-category = "main"
-optional = false
-python-versions = ">=3.6"
-
-[package.dependencies]
-amqp = ">=5.0.6,<6.0.0"
-vine = "*"
-
-[package.extras]
-azureservicebus = ["azure-servicebus (>=7.0.0)"]
-azurestoragequeues = ["azure-storage-queue"]
-consul = ["python-consul (>=0.6.0)"]
-librabbitmq = ["librabbitmq (>=1.5.2)"]
-mongodb = ["pymongo (>=3.3.0)"]
-msgpack = ["msgpack"]
-pyro = ["pyro4"]
-qpid = ["qpid-python (>=0.26)", "qpid-tools (>=0.26)"]
-redis = ["redis (>=3.3.11)"]
-slmq = ["softlayer-messaging (>=1.0.3)"]
-sqlalchemy = ["sqlalchemy"]
-sqs = ["boto3 (>=1.4.4)", "pycurl (==7.43.0.2)", "urllib3 (<1.26)"]
-yaml = ["PyYAML (>=3.10)"]
-zookeeper = ["kazoo (>=1.3.1)"]
-
-[[package]]
-name = "libsass"
-version = "0.21.0"
-description = "Sass for Python: A straightforward binding of libsass for Python."
-category = "main"
-optional = false
-python-versions = "*"
-
-[package.dependencies]
-six = "*"
-
-[[package]]
-name = "license-expression"
-version = "1.2"
-description = "license-expression is small utility library to parse, compare, simplify and normalize license expressions (such as SPDX license expressions) using boolean logic."
-category = "main"
-optional = false
-python-versions = "*"
-
-[package.dependencies]
-"boolean.py" = ">=3.6,<4.0.0"
-
-[[package]]
-name = "markupsafe"
-version = "2.0.1"
-description = "Safely add untrusted strings to HTML/XML markup."
-category = "dev"
-optional = false
-python-versions = ">=3.6"
-
-[[package]]
-name = "matplotlib-inline"
-version = "0.1.3"
-description = "Inline Matplotlib backend for Jupyter"
-category = "main"
-optional = false
-python-versions = ">=3.5"
-
-[package.dependencies]
-traitlets = "*"
-
-[[package]]
-name = "mccabe"
-version = "0.6.1"
-description = "McCabe checker, plugin for flake8"
-category = "dev"
-optional = false
-python-versions = "*"
-
-[[package]]
-name = "mypy"
-version = "0.910"
-description = "Optional static typing for Python"
-category = "dev"
-optional = false
-python-versions = ">=3.5"
-
-[package.dependencies]
-mypy-extensions = ">=0.4.3,<0.5.0"
-toml = "*"
-typing-extensions = ">=3.7.4"
-
-[package.extras]
-dmypy = ["psutil (>=4.0)"]
-python2 = ["typed-ast (>=1.4.0,<1.5.0)"]
-
-[[package]]
-name = "mypy-extensions"
-version = "0.4.3"
-description = "Experimental type system extensions for programs checked with the mypy typechecker."
-category = "dev"
-optional = false
-python-versions = "*"
-
-[[package]]
-name = "oauthlib"
-version = "3.1.1"
-description = "A generic, spec-compliant, thorough implementation of the OAuth request-signing logic"
-category = "main"
-optional = false
-python-versions = ">=3.6"
-
-[package.extras]
-rsa = ["cryptography (>=3.0.0,<4)"]
-signals = ["blinker (>=1.4.0)"]
-signedtoken = ["cryptography (>=3.0.0,<4)", "pyjwt (>=2.0.0,<3)"]
-
-[[package]]
-name = "packaging"
-version = "21.0"
-description = "Core utilities for Python packages"
-category = "main"
-optional = false
-python-versions = ">=3.6"
-
-[package.dependencies]
-pyparsing = ">=2.0.2"
-
-[[package]]
-name = "parso"
-version = "0.8.2"
-description = "A Python Parser"
-category = "main"
-optional = false
-python-versions = ">=3.6"
-
-[package.extras]
-qa = ["flake8 (==3.8.3)", "mypy (==0.782)"]
-testing = ["docopt", "pytest (<6.0.0)"]
-
-[[package]]
-name = "parsy"
-version = "1.1.0"
-description = "easy-to-use parser combinators, for parsing in pure Python"
-category = "dev"
-optional = false
-python-versions = "*"
-
-[[package]]
-name = "pathspec"
-version = "0.9.0"
-description = "Utility library for gitignore style pattern matching of file paths."
-category = "dev"
-optional = false
-python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7"
-
-[[package]]
-name = "pbr"
-version = "5.6.0"
-description = "Python Build Reasonableness"
-category = "dev"
-optional = false
-python-versions = ">=2.6"
-
-[[package]]
-name = "persisting-theory"
-version = "0.2.1"
-description = "Registries that can autodiscover values accross your project apps"
-category = "main"
-optional = false
-python-versions = "*"
-
-[[package]]
-name = "pexpect"
-version = "4.8.0"
-description = "Pexpect allows easy control of interactive console applications."
-category = "main"
-optional = false
-python-versions = "*"
-
-[package.dependencies]
-ptyprocess = ">=0.5"
-
-[[package]]
-name = "pg8000"
-version = "1.22.0"
-description = "PostgreSQL interface library"
-category = "dev"
-optional = false
-python-versions = ">=3.6"
-
-[package.dependencies]
-scramp = ">=1.4.1"
-
-[[package]]
-name = "phonenumbers"
-version = "8.12.35"
-description = "Python version of Google's common library for parsing, formatting, storing and validating international phone numbers."
-category = "main"
-optional = false
-python-versions = "*"
-
-[[package]]
-name = "pickleshare"
-version = "0.7.5"
-description = "Tiny 'shelve'-like database with concurrency support"
-category = "main"
-optional = false
-python-versions = "*"
-
-[[package]]
-name = "pillow"
-version = "8.4.0"
-description = "Python Imaging Library (Fork)"
-category = "main"
-optional = false
-python-versions = ">=3.6"
-
-[[package]]
-name = "pluggy"
-version = "1.0.0"
-description = "plugin and hook calling mechanisms for python"
-category = "dev"
-optional = false
-python-versions = ">=3.6"
-
-[package.extras]
-dev = ["pre-commit", "tox"]
-testing = ["pytest", "pytest-benchmark"]
-
-[[package]]
-name = "prometheus-client"
-version = "0.11.0"
-description = "Python client for the Prometheus monitoring system."
-category = "main"
-optional = false
-python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
-
-[package.extras]
-twisted = ["twisted"]
-
-[[package]]
-name = "prompt-toolkit"
-version = "3.0.21"
-description = "Library for building powerful interactive command lines in Python"
-category = "main"
-optional = false
-python-versions = ">=3.6.2"
-
-[package.dependencies]
-wcwidth = "*"
-
-[[package]]
-name = "psutil"
-version = "5.8.0"
-description = "Cross-platform lib for process and system monitoring in Python."
-category = "main"
-optional = false
-python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
-
-[package.extras]
-test = ["ipaddress", "mock", "unittest2", "enum34", "pywin32", "wmi"]
-
-[[package]]
-name = "psycopg2"
-version = "2.9.1"
-description = "psycopg2 - Python-PostgreSQL Database Adapter"
-category = "main"
-optional = false
-python-versions = ">=3.6"
-
-[[package]]
-name = "ptyprocess"
-version = "0.7.0"
-description = "Run a subprocess in a pseudo terminal"
-category = "main"
-optional = false
-python-versions = "*"
-
-[[package]]
-name = "py"
-version = "1.10.0"
-description = "library with cross-python path, ini-parsing, io, code, log facilities"
-category = "dev"
-optional = false
-python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
-
-[[package]]
-name = "pyasn1"
-version = "0.4.8"
-description = "ASN.1 types and codecs"
-category = "main"
-optional = true
-python-versions = "*"
-
-[[package]]
-name = "pyasn1-modules"
-version = "0.2.8"
-description = "A collection of ASN.1-based protocols modules."
-category = "main"
-optional = true
-python-versions = "*"
-
-[package.dependencies]
-pyasn1 = ">=0.4.6,<0.5.0"
-
-[[package]]
-name = "pycodestyle"
-version = "2.7.0"
-description = "Python style guide checker"
-category = "dev"
-optional = false
-python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
-
-[[package]]
-name = "pycparser"
-version = "2.20"
-description = "C parser in Python"
-category = "main"
-optional = false
-python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
-
-[[package]]
-name = "pycryptodome"
-version = "3.11.0"
-description = "Cryptographic library for Python"
-category = "main"
-optional = false
-python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
-
-[[package]]
-name = "pydocstyle"
-version = "6.1.1"
-description = "Python docstring style checker"
-category = "dev"
-optional = false
-python-versions = ">=3.6"
-
-[package.dependencies]
-snowballstemmer = "*"
-
-[package.extras]
-toml = ["toml"]
-
-[[package]]
-name = "pyflakes"
-version = "2.3.1"
-description = "passive checker of Python programs"
-category = "dev"
-optional = false
-python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
-
-[[package]]
-name = "pygments"
-version = "2.10.0"
-description = "Pygments is a syntax highlighting package written in Python."
-category = "main"
-optional = false
-python-versions = ">=3.5"
-
-[[package]]
-name = "pyjwt"
-version = "2.3.0"
-description = "JSON Web Token implementation in Python"
-category = "main"
-optional = false
-python-versions = ">=3.6"
-
-[package.dependencies]
-cryptography = {version = ">=3.3.1", optional = true, markers = "extra == \"crypto\""}
-
-[package.extras]
-crypto = ["cryptography (>=3.3.1)"]
-dev = ["sphinx", "sphinx-rtd-theme", "zope.interface", "cryptography (>=3.3.1)", "pytest (>=6.0.0,<7.0.0)", "coverage[toml] (==5.0.4)", "mypy", "pre-commit"]
-docs = ["sphinx", "sphinx-rtd-theme", "zope.interface"]
-tests = ["pytest (>=6.0.0,<7.0.0)", "coverage[toml] (==5.0.4)"]
-
-[[package]]
-name = "pyparsing"
-version = "3.0.1"
-description = "Python parsing module"
-category = "main"
-optional = false
-python-versions = ">=3.6"
-
-[package.extras]
-diagrams = ["jinja2", "railroad-diagrams"]
-
-[[package]]
-name = "pytest"
-version = "6.2.5"
-description = "pytest: simple powerful testing with Python"
-category = "dev"
-optional = false
-python-versions = ">=3.6"
-
-[package.dependencies]
-atomicwrites = {version = ">=1.0", markers = "sys_platform == \"win32\""}
-attrs = ">=19.2.0"
-colorama = {version = "*", markers = "sys_platform == \"win32\""}
-iniconfig = "*"
-packaging = "*"
-pluggy = ">=0.12,<2.0"
-py = ">=1.8.2"
-toml = "*"
-
-[package.extras]
-testing = ["argcomplete", "hypothesis (>=3.56)", "mock", "nose", "requests", "xmlschema"]
-
-[[package]]
-name = "pytest-cov"
-version = "2.12.1"
-description = "Pytest plugin for measuring coverage."
-category = "dev"
-optional = false
-python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
-
-[package.dependencies]
-coverage = ">=5.2.1"
-pytest = ">=4.6"
-toml = "*"
-
-[package.extras]
-testing = ["fields", "hunter", "process-tests", "six", "pytest-xdist", "virtualenv"]
-
-[[package]]
-name = "pytest-django"
-version = "4.4.0"
-description = "A Django plugin for pytest."
-category = "dev"
-optional = false
-python-versions = ">=3.5"
-
-[package.dependencies]
-pytest = ">=5.4.0"
-
-[package.extras]
-docs = ["sphinx", "sphinx-rtd-theme"]
-testing = ["django", "django-configurations (>=2.0)"]
-
-[[package]]
-name = "pytest-django-testing-postgresql"
-version = "0.1.post0"
-description = "Use a temporary PostgreSQL database with pytest-django"
-category = "dev"
-optional = false
-python-versions = "*"
-
-[package.dependencies]
-dj-database-url = "*"
-"testing.postgresql" = "*"
-
-[[package]]
-name = "pytest-sugar"
-version = "0.9.4"
-description = "pytest-sugar is a plugin for pytest that changes the default look and feel of pytest (e.g. progressbar, show tests that fail instantly)."
-category = "dev"
-optional = false
-python-versions = "*"
-
-[package.dependencies]
-packaging = ">=14.1"
-pytest = ">=2.9"
-termcolor = ">=1.1.0"
-
-[[package]]
-name = "python-crontab"
-version = "2.6.0"
-description = "Python Crontab API"
-category = "main"
-optional = false
-python-versions = "*"
-
-[package.dependencies]
-python-dateutil = "*"
-
-[package.extras]
-cron-description = ["cron-descriptor"]
-cron-schedule = ["croniter"]
-
-[[package]]
-name = "python-dateutil"
-version = "2.8.2"
-description = "Extensions to the standard Python datetime module"
-category = "main"
-optional = false
-python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7"
-
-[package.dependencies]
-six = ">=1.5"
-
-[[package]]
-name = "python-gnupg"
-version = "0.4.7"
-description = "A wrapper for the Gnu Privacy Guard (GPG or GnuPG)"
-category = "main"
-optional = false
-python-versions = "*"
-
-[[package]]
-name = "python-ldap"
-version = "3.3.1"
-description = "Python modules for implementing LDAP clients"
-category = "main"
-optional = true
-python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*"
-
-[package.dependencies]
-pyasn1 = ">=0.3.7"
-pyasn1_modules = ">=0.1.5"
-
-[[package]]
-name = "python3-openid"
-version = "3.2.0"
-description = "OpenID support for modern servers and consumers."
-category = "main"
-optional = false
-python-versions = "*"
-
-[package.dependencies]
-defusedxml = "*"
-
-[package.extras]
-mysql = ["mysql-connector-python"]
-postgresql = ["psycopg2"]
-
-[[package]]
-name = "pytz"
-version = "2021.3"
-description = "World timezone definitions, modern and historical"
-category = "main"
-optional = false
-python-versions = "*"
-
-[[package]]
-name = "pyyaml"
-version = "6.0"
-description = "YAML parser and emitter for Python"
-category = "dev"
-optional = false
-python-versions = ">=3.6"
-
-[[package]]
-name = "qrcode"
-version = "6.1"
-description = "QR Code image generator"
-category = "main"
-optional = false
-python-versions = "*"
-
-[package.dependencies]
-colorama = {version = "*", markers = "platform_system == \"Windows\""}
-six = "*"
-
-[package.extras]
-dev = ["tox", "pytest", "mock"]
-maintainer = ["zest.releaser"]
-pil = ["pillow"]
-test = ["pytest", "pytest-cov", "mock"]
-
-[[package]]
-name = "redis"
-version = "3.5.3"
-description = "Python client for Redis key-value store"
-category = "main"
-optional = false
-python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
-
-[package.extras]
-hiredis = ["hiredis (>=0.1.3)"]
-
-[[package]]
-name = "regex"
-version = "2021.10.23"
-description = "Alternative regular expression module, to replace re."
-category = "dev"
-optional = false
-python-versions = "*"
-
-[[package]]
-name = "requests"
-version = "2.26.0"
-description = "Python HTTP for Humans."
-category = "main"
-optional = false
-python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*"
-
-[package.dependencies]
-certifi = ">=2017.4.17"
-charset-normalizer = {version = ">=2.0.0,<2.1.0", markers = "python_version >= \"3\""}
-idna = {version = ">=2.5,<4", markers = "python_version >= \"3\""}
-urllib3 = ">=1.21.1,<1.27"
-
-[package.extras]
-socks = ["PySocks (>=1.5.6,!=1.5.7)", "win-inet-pton"]
-use_chardet_on_py3 = ["chardet (>=3.0.2,<5)"]
-
-[[package]]
-name = "requests-oauthlib"
-version = "1.3.0"
-description = "OAuthlib authentication support for Requests."
-category = "main"
-optional = false
-python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
-
-[package.dependencies]
-oauthlib = ">=3.0.0"
-requests = ">=2.0.0"
-
-[package.extras]
-rsa = ["oauthlib[signedtoken] (>=3.0.0)"]
-
-[[package]]
-name = "restructuredtext-lint"
-version = "1.3.2"
-description = "reStructuredText linter"
-category = "dev"
-optional = false
-python-versions = "*"
-
-[package.dependencies]
-docutils = ">=0.11,<1.0"
-
-[[package]]
-name = "ruamel.yaml"
-version = "0.17.16"
-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
-python-versions = ">=3"
-
-[package.dependencies]
-"ruamel.yaml.clib" = {version = ">=0.1.2", markers = "platform_python_implementation == \"CPython\" and python_version < \"3.10\""}
-
-[package.extras]
-docs = ["ryd"]
-jinja2 = ["ruamel.yaml.jinja2 (>=0.2)"]
-
-[[package]]
-name = "ruamel.yaml.clib"
-version = "0.2.6"
-description = "C version of reader, parser and emitter for ruamel.yaml derived from libyaml"
-category = "main"
-optional = false
-python-versions = ">=3.5"
-
-[[package]]
-name = "rules"
-version = "2.2"
-description = "Awesome Django authorization, without the database"
-category = "main"
-optional = false
-python-versions = "*"
-
-[[package]]
-name = "s3transfer"
-version = "0.5.0"
-description = "An Amazon S3 Transfer Manager"
-category = "main"
-optional = true
-python-versions = ">= 3.6"
-
-[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"
-description = "Checks installed dependencies for known vulnerabilities."
-category = "dev"
-optional = false
-python-versions = ">=3.5"
-
-[package.dependencies]
-Click = ">=6.0"
-dparse = ">=0.5.1"
-packaging = "*"
-requests = "*"
-
-[[package]]
-name = "scramp"
-version = "1.4.1"
-description = "An implementation of the SCRAM protocol."
-category = "dev"
-optional = false
-python-versions = ">=3.6"
-
-[package.dependencies]
-asn1crypto = ">=1.4.0"
-
-[[package]]
-name = "selenium"
-version = "3.141.0"
-description = "Python bindings for Selenium"
-category = "dev"
-optional = false
-python-versions = "*"
-
-[package.dependencies]
-urllib3 = "*"
-
-[[package]]
-name = "sentry-sdk"
-version = "1.4.3"
-description = "Python client for Sentry (https://sentry.io)"
-category = "main"
-optional = true
-python-versions = "*"
-
-[package.dependencies]
-certifi = "*"
-urllib3 = ">=1.10.0"
-
-[package.extras]
-aiohttp = ["aiohttp (>=3.5)"]
-beam = ["apache-beam (>=2.12)"]
-bottle = ["bottle (>=0.12.13)"]
-celery = ["celery (>=3)"]
-chalice = ["chalice (>=1.16.0)"]
-django = ["django (>=1.8)"]
-falcon = ["falcon (>=1.4)"]
-flask = ["flask (>=0.11)", "blinker (>=1.1)"]
-httpx = ["httpx (>=0.16.0)"]
-pure_eval = ["pure-eval", "executing", "asttokens"]
-pyspark = ["pyspark (>=2.4.4)"]
-rq = ["rq (>=0.6)"]
-sanic = ["sanic (>=0.8)"]
-sqlalchemy = ["sqlalchemy (>=1.2)"]
-tornado = ["tornado (>=5)"]
-
-[package.source]
-type = "legacy"
-url = "https://edugit.org/api/v4/projects/461/packages/pypi/simple"
-reference = "gitlab"
-
-[[package]]
-name = "six"
-version = "1.16.0"
-description = "Python 2 and 3 compatibility utilities"
-category = "main"
-optional = false
-python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*"
-
-[[package]]
-name = "smmap"
-version = "5.0.0"
-description = "A pure Python implementation of a sliding window memory map manager"
-category = "dev"
-optional = false
-python-versions = ">=3.6"
-
-[[package]]
-name = "snowballstemmer"
-version = "2.1.0"
-description = "This package provides 29 stemmers for 28 languages generated from Snowball algorithms."
-category = "dev"
-optional = false
-python-versions = "*"
-
-[[package]]
-name = "soupsieve"
-version = "2.2.1"
-description = "A modern CSS selector implementation for Beautiful Soup."
-category = "main"
-optional = false
-python-versions = ">=3.6"
-
-[[package]]
-name = "spdx-license-list"
-version = "0.5.2"
-description = "A simple tool/library for working with SPDX license definitions."
-category = "main"
-optional = false
-python-versions = "*"
-
-[[package]]
-name = "sphinx"
-version = "3.5.4"
-description = "Python documentation generator"
-category = "dev"
-optional = false
-python-versions = ">=3.5"
-
-[package.dependencies]
-alabaster = ">=0.7,<0.8"
-babel = ">=1.3"
-colorama = {version = ">=0.3.5", markers = "sys_platform == \"win32\""}
-docutils = ">=0.12,<0.17"
-imagesize = "*"
-Jinja2 = ">=2.3"
-packaging = "*"
-Pygments = ">=2.0"
-requests = ">=2.5.0"
-snowballstemmer = ">=1.1"
-sphinxcontrib-applehelp = "*"
-sphinxcontrib-devhelp = "*"
-sphinxcontrib-htmlhelp = "*"
-sphinxcontrib-jsmath = "*"
-sphinxcontrib-qthelp = "*"
-sphinxcontrib-serializinghtml = "*"
-
-[package.extras]
-docs = ["sphinxcontrib-websupport"]
-lint = ["flake8 (>=3.5.0)", "isort", "mypy (>=0.800)", "docutils-stubs"]
-test = ["pytest", "pytest-cov", "html5lib", "cython", "typed-ast"]
-
-[[package]]
-name = "sphinx-autodoc-typehints"
-version = "1.12.0"
-description = "Type hints (PEP 484) support for the Sphinx autodoc extension"
-category = "dev"
-optional = false
-python-versions = ">=3.6"
-
-[package.dependencies]
-Sphinx = ">=3.0"
-
-[package.extras]
-test = ["pytest (>=3.1.0)", "typing-extensions (>=3.5)", "sphobjinv (>=2.0)", "Sphinx (>=3.2.0)", "dataclasses"]
-type_comments = ["typed-ast (>=1.4.0)"]
-
-[[package]]
-name = "sphinx-materialdesign-theme"
-version = "0.1.11"
-description = "Sphinx Material Design Theme"
-category = "dev"
-optional = false
-python-versions = "*"
-
-[[package]]
-name = "sphinxcontrib-applehelp"
-version = "1.0.2"
-description = "sphinxcontrib-applehelp is a sphinx extension which outputs Apple help books"
-category = "dev"
-optional = false
-python-versions = ">=3.5"
-
-[package.extras]
-lint = ["flake8", "mypy", "docutils-stubs"]
-test = ["pytest"]
-
-[[package]]
-name = "sphinxcontrib-devhelp"
-version = "1.0.2"
-description = "sphinxcontrib-devhelp is a sphinx extension which outputs Devhelp document."
-category = "dev"
-optional = false
-python-versions = ">=3.5"
-
-[package.extras]
-lint = ["flake8", "mypy", "docutils-stubs"]
-test = ["pytest"]
-
-[[package]]
-name = "sphinxcontrib-django"
-version = "0.5.1"
-description = "Improve the Sphinx autodoc for Django classes."
-category = "dev"
-optional = false
-python-versions = "*"
-
-[[package]]
-name = "sphinxcontrib-htmlhelp"
-version = "2.0.0"
-description = "sphinxcontrib-htmlhelp is a sphinx extension which renders HTML help files"
-category = "dev"
-optional = false
-python-versions = ">=3.6"
-
-[package.extras]
-lint = ["flake8", "mypy", "docutils-stubs"]
-test = ["pytest", "html5lib"]
-
-[[package]]
-name = "sphinxcontrib-jsmath"
-version = "1.0.1"
-description = "A sphinx extension which renders display math in HTML via JavaScript"
-category = "dev"
-optional = false
-python-versions = ">=3.5"
-
-[package.extras]
-test = ["pytest", "flake8", "mypy"]
-
-[[package]]
-name = "sphinxcontrib-qthelp"
-version = "1.0.3"
-description = "sphinxcontrib-qthelp is a sphinx extension which outputs QtHelp document."
-category = "dev"
-optional = false
-python-versions = ">=3.5"
-
-[package.extras]
-lint = ["flake8", "mypy", "docutils-stubs"]
-test = ["pytest"]
-
-[[package]]
-name = "sphinxcontrib-serializinghtml"
-version = "1.1.5"
-description = "sphinxcontrib-serializinghtml is a sphinx extension which outputs \"serialized\" HTML files (json and pickle)."
-category = "dev"
-optional = false
-python-versions = ">=3.5"
-
-[package.extras]
-lint = ["flake8", "mypy", "docutils-stubs"]
-test = ["pytest"]
-
-[[package]]
-name = "sqlparse"
-version = "0.4.2"
-description = "A non-validating SQL parser."
-category = "main"
-optional = false
-python-versions = ">=3.5"
-
-[[package]]
-name = "stevedore"
-version = "3.5.0"
-description = "Manage dynamic plugins for Python applications"
-category = "dev"
-optional = false
-python-versions = ">=3.6"
-
-[package.dependencies]
-pbr = ">=2.0.0,<2.1.0 || >2.1.0"
-
-[[package]]
-name = "termcolor"
-version = "1.1.0"
-description = "ANSII Color formatting for output in terminal."
-category = "dev"
-optional = false
-python-versions = "*"
-
-[[package]]
-name = "testfixtures"
-version = "6.18.3"
-description = "A collection of helpers and mock objects for unit tests and doc tests."
-category = "dev"
-optional = false
-python-versions = "*"
-
-[package.extras]
-build = ["setuptools-git", "wheel", "twine"]
-docs = ["sphinx", "zope.component", "sybil", "twisted", "mock", "django (<2)", "django"]
-test = ["pytest (>=3.6)", "pytest-cov", "pytest-django", "zope.component", "sybil", "twisted", "mock", "django (<2)", "django"]
-
-[[package]]
-name = "testing.common.database"
-version = "2.0.3"
-description = "utilities for testing.* packages"
-category = "dev"
-optional = false
-python-versions = "*"
-
-[package.extras]
-testing = ["nose"]
-
-[[package]]
-name = "testing.postgresql"
-version = "1.3.0"
-description = "automatically setups a postgresql instance in a temporary directory, and destroys it after testing"
-category = "dev"
-optional = false
-python-versions = "*"
-
-[package.dependencies]
-pg8000 = ">=1.10"
-"testing.common.database" = "*"
-
-[package.extras]
-testing = ["sqlalchemy", "nose", "psycopg2"]
-
-[[package]]
-name = "toml"
-version = "0.10.2"
-description = "Python Library for Tom's Obvious, Minimal Language"
-category = "main"
-optional = false
-python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*"
-
-[[package]]
-name = "traitlets"
-version = "5.1.1"
-description = "Traitlets Python configuration system"
-category = "main"
-optional = false
-python-versions = ">=3.7"
-
-[package.extras]
-test = ["pytest"]
-
-[[package]]
-name = "twilio"
-version = "7.2.0"
-description = "Twilio API client and TwiML generator"
-category = "main"
-optional = false
-python-versions = ">=3.6.0"
-
-[package.dependencies]
-PyJWT = ">=2.0.0,<3.0.0"
-pytz = "*"
-requests = ">=2.0.0"
-
-[[package]]
-name = "typed-ast"
-version = "1.4.3"
-description = "a fork of Python 2 and 3 ast modules with type comment support"
-category = "dev"
-optional = false
-python-versions = "*"
-
-[[package]]
-name = "types-pytz"
-version = "2021.3.0"
-description = "Typing stubs for pytz"
-category = "dev"
-optional = false
-python-versions = "*"
-
-[[package]]
-name = "types-pyyaml"
-version = "6.0.0"
-description = "Typing stubs for PyYAML"
-category = "dev"
-optional = false
-python-versions = "*"
-
-[[package]]
-name = "typing-extensions"
-version = "3.10.0.2"
-description = "Backported and Experimental Type Hints for Python 3.5+"
-category = "dev"
-optional = false
-python-versions = "*"
-
-[[package]]
-name = "urllib3"
-version = "1.26.7"
-description = "HTTP library with thread-safe connection pooling, file post, and more."
-category = "main"
-optional = false
-python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, <4"
-
-[package.extras]
-brotli = ["brotlipy (>=0.6.0)"]
-secure = ["pyOpenSSL (>=0.14)", "cryptography (>=1.3.4)", "idna (>=2.0.0)", "certifi", "ipaddress"]
-socks = ["PySocks (>=1.5.6,!=1.5.7,<2.0)"]
-
-[[package]]
-name = "uwsgi"
-version = "2.0.20"
-description = "The uWSGI server"
-category = "dev"
-optional = false
-python-versions = "*"
-
-[[package]]
-name = "vine"
-version = "5.0.0"
-description = "Promises, promises, promises."
-category = "main"
-optional = false
-python-versions = ">=3.6"
-
-[[package]]
-name = "wcwidth"
-version = "0.2.5"
-description = "Measures the displayed width of unicode strings in a terminal"
-category = "main"
-optional = false
-python-versions = "*"
-
-[[package]]
-name = "webencodings"
-version = "0.5.1"
-description = "Character encoding aliases for legacy web content"
-category = "main"
-optional = false
-python-versions = "*"
-
-[[package]]
-name = "whoosh"
-version = "2.7.4"
-description = "Fast, pure-Python full text indexing, search, and spell checking library."
-category = "main"
-optional = false
-python-versions = "*"
-
-[[package]]
-name = "wrapt"
-version = "1.13.2"
-description = "Module for decorators, wrappers and monkey patching."
-category = "main"
-optional = false
-python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7"
-
-[[package]]
-name = "yubiotp"
-version = "1.0.0.post1"
-description = "A library for verifying YubiKey OTP tokens, both locally and through a Yubico web service."
-category = "main"
-optional = false
-python-versions = "*"
-
-[package.dependencies]
-pycryptodome = "*"
-
-[extras]
-ldap = ["django-auth-ldap"]
-s3 = ["boto3", "django-storages"]
-sentry = []
-
-[metadata]
-lock-version = "1.1"
-python-versions = "^3.9"
-content-hash = "d15f5a57e0a3e887cfe411829b19459af4ebfc9aadc6e2a0468b11f4fc03e6b4"
-
-[metadata.files]
-alabaster = [
-    {file = "alabaster-0.7.12-py2.py3-none-any.whl", hash = "sha256:446438bdcca0e05bd45ea2de1668c1d9b032e1a9154c2c259092d77031ddd359"},
-    {file = "alabaster-0.7.12.tar.gz", hash = "sha256:a661d72d58e6ea8a57f7a86e37d86716863ee5e92788398526d58b26a4e4dc02"},
-]
-aleksis-builddeps = [
-    {file = "AlekSIS-Builddeps-4.tar.gz", hash = "sha256:aaaa22965228b9b9b7de812e3e7fa9cbfdbf8635bb22d6f3a201dc0cc6d8d307"},
-    {file = "AlekSIS_Builddeps-4-py3-none-any.whl", hash = "sha256:02a93e503f5810e6c93a8bc829bc8cbda735ccbf78b91954d2f6507dfea3d01f"},
-]
-amqp = [
-    {file = "amqp-5.0.6-py3-none-any.whl", hash = "sha256:493a2ac6788ce270a2f6a765b017299f60c1998f5a8617908ee9be082f7300fb"},
-    {file = "amqp-5.0.6.tar.gz", hash = "sha256:03e16e94f2b34c31f8bf1206d8ddd3ccaa4c315f7f6a1879b7b1210d229568c2"},
-]
-appdirs = [
-    {file = "appdirs-1.4.4-py2.py3-none-any.whl", hash = "sha256:a841dacd6b99318a741b166adb07e19ee71a274450e68237b4650ca1055ab128"},
-    {file = "appdirs-1.4.4.tar.gz", hash = "sha256:7d5d0167b2b1ba821647616af46a749d1c653740dd0d2415100fe26e27afdf41"},
-]
-appnope = [
-    {file = "appnope-0.1.2-py2.py3-none-any.whl", hash = "sha256:93aa393e9d6c54c5cd570ccadd8edad61ea0c4b9ea7a01409020c9aa019eb442"},
-    {file = "appnope-0.1.2.tar.gz", hash = "sha256:dd83cd4b5b460958838f6eb3000c660b1f9caf2a5b1de4264e941512f603258a"},
-]
-asgiref = [
-    {file = "asgiref-3.4.1-py3-none-any.whl", hash = "sha256:ffc141aa908e6f175673e7b1b3b7af4fdb0ecb738fc5c8b88f69f055c2415214"},
-    {file = "asgiref-3.4.1.tar.gz", hash = "sha256:4ef1ab46b484e3c706329cedeff284a5d40824200638503f5768edb6de7d58e9"},
-]
-asn1crypto = [
-    {file = "asn1crypto-1.4.0-py2.py3-none-any.whl", hash = "sha256:4bcdf33c861c7d40bdcd74d8e4dd7661aac320fcdf40b9a3f95b4ee12fde2fa8"},
-    {file = "asn1crypto-1.4.0.tar.gz", hash = "sha256:f4f6e119474e58e04a2b1af817eb585b4fd72bdd89b998624712b5c99be7641c"},
-]
-atomicwrites = [
-    {file = "atomicwrites-1.4.0-py2.py3-none-any.whl", hash = "sha256:6d1784dea7c0c8d4a5172b6c620f40b6e4cbfdf96d783691f2e1302a7b88e197"},
-    {file = "atomicwrites-1.4.0.tar.gz", hash = "sha256:ae70396ad1a434f9c7046fd2dd196fc04b12f9e91ffb859164193be8b6168a7a"},
-]
-attrs = [
-    {file = "attrs-21.2.0-py2.py3-none-any.whl", hash = "sha256:149e90d6d8ac20db7a955ad60cf0e6881a3f20d37096140088356da6c716b0b1"},
-    {file = "attrs-21.2.0.tar.gz", hash = "sha256:ef6aaac3ca6cd92904cdd0d83f629a15f18053ec84e6432106f7a4d04ae4f5fb"},
-]
-babel = [
-    {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"},
-    {file = "backcall-0.2.0.tar.gz", hash = "sha256:5cbdbf27be5e7cfadb448baf0aa95508f91f2bbc6c6437cd9cd06e2a4c215e1e"},
-]
-bandit = [
-    {file = "bandit-1.7.0-py3-none-any.whl", hash = "sha256:216be4d044209fa06cf2a3e51b319769a51be8318140659719aa7a115c35ed07"},
-    {file = "bandit-1.7.0.tar.gz", hash = "sha256:8a4c7415254d75df8ff3c3b15cfe9042ecee628a1e40b44c15a98890fbfc2608"},
-]
-beautifulsoup4 = [
-    {file = "beautifulsoup4-4.10.0-py3-none-any.whl", hash = "sha256:9a315ce70049920ea4572a4055bc4bd700c940521d36fc858205ad4fcde149bf"},
-    {file = "beautifulsoup4-4.10.0.tar.gz", hash = "sha256:c23ad23c521d818955a4151a67d81580319d4bf548d3d49f4223ae041ff98891"},
-]
-billiard = [
-    {file = "billiard-3.6.4.0-py3-none-any.whl", hash = "sha256:87103ea78fa6ab4d5c751c4909bcff74617d985de7fa8b672cf8618afd5a875b"},
-    {file = "billiard-3.6.4.0.tar.gz", hash = "sha256:299de5a8da28a783d51b197d496bef4f1595dd023a93a4f59dde1886ae905547"},
-]
-black = [
-    {file = "black-19.10b0-py36-none-any.whl", hash = "sha256:1b30e59be925fafc1ee4565e5e08abef6b03fe455102883820fe5ee2e4734e0b"},
-    {file = "black-19.10b0.tar.gz", hash = "sha256:c2edb73a08e9e0e6f65a0e6af18b059b8b1cdd5bef997d7a0b181df93dc81539"},
-]
-bleach = [
-    {file = "bleach-4.1.0-py2.py3-none-any.whl", hash = "sha256:4d2651ab93271d1129ac9cbc679f524565cc8a1b791909c4a51eac4446a15994"},
-    {file = "bleach-4.1.0.tar.gz", hash = "sha256:0900d8b37eba61a802ee40ac0061f8c2b5dee29c1927dd1d233e075ebf5a71da"},
-]
-"boolean.py" = [
-    {file = "boolean.py-3.8-py2.py3-none-any.whl", hash = "sha256:d75da0fd0354425fa64f6bbc6cec6ae1485d0eec3447b73187ff8cbf9b572e26"},
-    {file = "boolean.py-3.8.tar.gz", hash = "sha256:cc24e20f985d60cd4a3a5a1c0956dd12611159d32a75081dabd0c9ab981acaa4"},
-]
-boto3 = [
-    {file = "boto3-1.19.3-py3-none-any.whl", hash = "sha256:2dfc8cf34d6dfbdfca4c88e8fddf9fe95cde489fb83144fe35f989ec6790e325"},
-    {file = "boto3-1.19.3.tar.gz", hash = "sha256:e36ffaf9969648e2f435aa1f0029956fea3aac52466eef3bcb43bde498a182dd"},
-]
-botocore = [
-    {file = "botocore-1.22.3-py3-none-any.whl", hash = "sha256:aacdb9b8e09e356515966251d1e08d9929575a76af504992bfb941553dee59c2"},
-    {file = "botocore-1.22.3.tar.gz", hash = "sha256:53ca22aeac9b53fe5ec1f40b8ca9620ffe8b054458abfeb9ab74bbe9e0b0ecfa"},
-]
-bs4 = [
-    {file = "bs4-0.0.1.tar.gz", hash = "sha256:36ecea1fd7cc5c0c6e4a1ff075df26d50da647b75376626cc186e2212886dd3a"},
-]
-calendarweek = [
-    {file = "calendarweek-0.5.0-py3-none-any.whl", hash = "sha256:f2003e6e0264d3d1320fc99ae6d70e60174c2664e5640c6aa31ad38e229d942d"},
-    {file = "calendarweek-0.5.0.tar.gz", hash = "sha256:32f5c8663799a2f5a0b8909976c7a3ae77397acd7e7c31d1456ece5b452988a5"},
-]
-celery = [
-    {file = "celery-5.1.2-py3-none-any.whl", hash = "sha256:9dab2170b4038f7bf10ef2861dbf486ddf1d20592290a1040f7b7a1259705d42"},
-    {file = "celery-5.1.2.tar.gz", hash = "sha256:8d9a3de9162965e97f8e8cc584c67aad83b3f7a267584fa47701ed11c3e0d4b0"},
-]
-celery-haystack-ng = [
-    {file = "celery-haystack-ng-0.20.post2.tar.gz", hash = "sha256:d2e077851f13dddc36fc86134c7c8a937e46ae75e576eb8e77e03b03977fc7bb"},
-    {file = "celery_haystack_ng-0.20.post2-py2.py3-none-any.whl", hash = "sha256:a13e00f2c29411b06c6cdf59ad6a90b6c158e3384e7ec6d6d64f6a69e8ff299a"},
-]
-celery-progress = [
-    {file = "celery-progress-0.1.1.tar.gz", hash = "sha256:b2622d1b410a763412810f0293153c984f4a0220b76769bd701b5b45e583ddad"},
-    {file = "celery_progress-0.1.1-py3-none-any.whl", hash = "sha256:36a1e58b4408c9bf6aa63908204b50960b005db8e13f3c94ce6f8d6a2a4d4a6c"},
-]
-certifi = [
-    {file = "certifi-2021.10.8-py2.py3-none-any.whl", hash = "sha256:d62a0163eb4c2344ac042ab2bdf75399a71a2d8c7d47eac2e2ee91b9d6339569"},
-    {file = "certifi-2021.10.8.tar.gz", hash = "sha256:78884e7c1d4b00ce3cea67b44566851c4343c120abd683433ce934a68ea58872"},
-]
-cffi = [
-    {file = "cffi-1.15.0-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:c2502a1a03b6312837279c8c1bd3ebedf6c12c4228ddbad40912d671ccc8a962"},
-    {file = "cffi-1.15.0-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:23cfe892bd5dd8941608f93348c0737e369e51c100d03718f108bf1add7bd6d0"},
-    {file = "cffi-1.15.0-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:41d45de54cd277a7878919867c0f08b0cf817605e4eb94093e7516505d3c8d14"},
-    {file = "cffi-1.15.0-cp27-cp27m-win32.whl", hash = "sha256:4a306fa632e8f0928956a41fa8e1d6243c71e7eb59ffbd165fc0b41e316b2474"},
-    {file = "cffi-1.15.0-cp27-cp27m-win_amd64.whl", hash = "sha256:e7022a66d9b55e93e1a845d8c9eba2a1bebd4966cd8bfc25d9cd07d515b33fa6"},
-    {file = "cffi-1.15.0-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:14cd121ea63ecdae71efa69c15c5543a4b5fbcd0bbe2aad864baca0063cecf27"},
-    {file = "cffi-1.15.0-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:d4d692a89c5cf08a8557fdeb329b82e7bf609aadfaed6c0d79f5a449a3c7c023"},
-    {file = "cffi-1.15.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0104fb5ae2391d46a4cb082abdd5c69ea4eab79d8d44eaaf79f1b1fd806ee4c2"},
-    {file = "cffi-1.15.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:91ec59c33514b7c7559a6acda53bbfe1b283949c34fe7440bcf917f96ac0723e"},
-    {file = "cffi-1.15.0-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:f5c7150ad32ba43a07c4479f40241756145a1f03b43480e058cfd862bf5041c7"},
-    {file = "cffi-1.15.0-cp310-cp310-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:00c878c90cb53ccfaae6b8bc18ad05d2036553e6d9d1d9dbcf323bbe83854ca3"},
-    {file = "cffi-1.15.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:abb9a20a72ac4e0fdb50dae135ba5e77880518e742077ced47eb1499e29a443c"},
-    {file = "cffi-1.15.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a5263e363c27b653a90078143adb3d076c1a748ec9ecc78ea2fb916f9b861962"},
-    {file = "cffi-1.15.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f54a64f8b0c8ff0b64d18aa76675262e1700f3995182267998c31ae974fbc382"},
-    {file = "cffi-1.15.0-cp310-cp310-win32.whl", hash = "sha256:c21c9e3896c23007803a875460fb786118f0cdd4434359577ea25eb556e34c55"},
-    {file = "cffi-1.15.0-cp310-cp310-win_amd64.whl", hash = "sha256:5e069f72d497312b24fcc02073d70cb989045d1c91cbd53979366077959933e0"},
-    {file = "cffi-1.15.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:64d4ec9f448dfe041705426000cc13e34e6e5bb13736e9fd62e34a0b0c41566e"},
-    {file = "cffi-1.15.0-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2756c88cbb94231c7a147402476be2c4df2f6078099a6f4a480d239a8817ae39"},
-    {file = "cffi-1.15.0-cp36-cp36m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3b96a311ac60a3f6be21d2572e46ce67f09abcf4d09344c49274eb9e0bf345fc"},
-    {file = "cffi-1.15.0-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:75e4024375654472cc27e91cbe9eaa08567f7fbdf822638be2814ce059f58032"},
-    {file = "cffi-1.15.0-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:59888172256cac5629e60e72e86598027aca6bf01fa2465bdb676d37636573e8"},
-    {file = "cffi-1.15.0-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:27c219baf94952ae9d50ec19651a687b826792055353d07648a5695413e0c605"},
-    {file = "cffi-1.15.0-cp36-cp36m-win32.whl", hash = "sha256:4958391dbd6249d7ad855b9ca88fae690783a6be9e86df65865058ed81fc860e"},
-    {file = "cffi-1.15.0-cp36-cp36m-win_amd64.whl", hash = "sha256:f6f824dc3bce0edab5f427efcfb1d63ee75b6fcb7282900ccaf925be84efb0fc"},
-    {file = "cffi-1.15.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:06c48159c1abed75c2e721b1715c379fa3200c7784271b3c46df01383b593636"},
-    {file = "cffi-1.15.0-cp37-cp37m-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:c2051981a968d7de9dd2d7b87bcb9c939c74a34626a6e2f8181455dd49ed69e4"},
-    {file = "cffi-1.15.0-cp37-cp37m-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:fd8a250edc26254fe5b33be00402e6d287f562b6a5b2152dec302fa15bb3e997"},
-    {file = "cffi-1.15.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:91d77d2a782be4274da750752bb1650a97bfd8f291022b379bb8e01c66b4e96b"},
-    {file = "cffi-1.15.0-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:45db3a33139e9c8f7c09234b5784a5e33d31fd6907800b316decad50af323ff2"},
-    {file = "cffi-1.15.0-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:263cc3d821c4ab2213cbe8cd8b355a7f72a8324577dc865ef98487c1aeee2bc7"},
-    {file = "cffi-1.15.0-cp37-cp37m-win32.whl", hash = "sha256:17771976e82e9f94976180f76468546834d22a7cc404b17c22df2a2c81db0c66"},
-    {file = "cffi-1.15.0-cp37-cp37m-win_amd64.whl", hash = "sha256:3415c89f9204ee60cd09b235810be700e993e343a408693e80ce7f6a40108029"},
-    {file = "cffi-1.15.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:4238e6dab5d6a8ba812de994bbb0a79bddbdf80994e4ce802b6f6f3142fcc880"},
-    {file = "cffi-1.15.0-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:0808014eb713677ec1292301ea4c81ad277b6cdf2fdd90fd540af98c0b101d20"},
-    {file = "cffi-1.15.0-cp38-cp38-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:57e9ac9ccc3101fac9d6014fba037473e4358ef4e89f8e181f8951a2c0162024"},
-    {file = "cffi-1.15.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8b6c2ea03845c9f501ed1313e78de148cd3f6cad741a75d43a29b43da27f2e1e"},
-    {file = "cffi-1.15.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:10dffb601ccfb65262a27233ac273d552ddc4d8ae1bf93b21c94b8511bffe728"},
-    {file = "cffi-1.15.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:786902fb9ba7433aae840e0ed609f45c7bcd4e225ebb9c753aa39725bb3e6ad6"},
-    {file = "cffi-1.15.0-cp38-cp38-win32.whl", hash = "sha256:da5db4e883f1ce37f55c667e5c0de439df76ac4cb55964655906306918e7363c"},
-    {file = "cffi-1.15.0-cp38-cp38-win_amd64.whl", hash = "sha256:181dee03b1170ff1969489acf1c26533710231c58f95534e3edac87fff06c443"},
-    {file = "cffi-1.15.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:45e8636704eacc432a206ac7345a5d3d2c62d95a507ec70d62f23cd91770482a"},
-    {file = "cffi-1.15.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:31fb708d9d7c3f49a60f04cf5b119aeefe5644daba1cd2a0fe389b674fd1de37"},
-    {file = "cffi-1.15.0-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:6dc2737a3674b3e344847c8686cf29e500584ccad76204efea14f451d4cc669a"},
-    {file = "cffi-1.15.0-cp39-cp39-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:74fdfdbfdc48d3f47148976f49fab3251e550a8720bebc99bf1483f5bfb5db3e"},
-    {file = "cffi-1.15.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ffaa5c925128e29efbde7301d8ecaf35c8c60ffbcd6a1ffd3a552177c8e5e796"},
-    {file = "cffi-1.15.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3f7d084648d77af029acb79a0ff49a0ad7e9d09057a9bf46596dac9514dc07df"},
-    {file = "cffi-1.15.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ef1f279350da2c586a69d32fc8733092fd32cc8ac95139a00377841f59a3f8d8"},
-    {file = "cffi-1.15.0-cp39-cp39-win32.whl", hash = "sha256:2a23af14f408d53d5e6cd4e3d9a24ff9e05906ad574822a10563efcef137979a"},
-    {file = "cffi-1.15.0-cp39-cp39-win_amd64.whl", hash = "sha256:3773c4d81e6e818df2efbc7dd77325ca0dcb688116050fb2b3011218eda36139"},
-    {file = "cffi-1.15.0.tar.gz", hash = "sha256:920f0d66a896c2d99f0adbb391f990a84091179542c205fa53ce5787aff87954"},
-]
-charset-normalizer = [
-    {file = "charset-normalizer-2.0.7.tar.gz", hash = "sha256:e019de665e2bcf9c2b64e2e5aa025fa991da8720daa3c1138cadd2fd1856aed0"},
-    {file = "charset_normalizer-2.0.7-py3-none-any.whl", hash = "sha256:f7af805c321bfa1ce6714c51f254e0d5bb5e5834039bc17db7ebe3a4cec9492b"},
-]
-click = [
-    {file = "click-7.1.2-py2.py3-none-any.whl", hash = "sha256:dacca89f4bfadd5de3d7489b7c8a566eee0d3676333fbb50030263894c38c0dc"},
-    {file = "click-7.1.2.tar.gz", hash = "sha256:d2b5255c7c6349bc1bd1e59e08cd12acbbd63ce649f2588755783aa94dfb6b1a"},
-]
-click-didyoumean = [
-    {file = "click-didyoumean-0.3.0.tar.gz", hash = "sha256:f184f0d851d96b6d29297354ed981b7dd71df7ff500d82fa6d11f0856bee8035"},
-    {file = "click_didyoumean-0.3.0-py3-none-any.whl", hash = "sha256:a0713dc7a1de3f06bc0df5a9567ad19ead2d3d5689b434768a6145bff77c0667"},
-]
-click-plugins = [
-    {file = "click-plugins-1.1.1.tar.gz", hash = "sha256:46ab999744a9d831159c3411bb0c79346d94a444df9a3a3742e9ed63645f264b"},
-    {file = "click_plugins-1.1.1-py2.py3-none-any.whl", hash = "sha256:5d262006d3222f5057fd81e1623d4443e41dcda5dc815c06b442aa3c02889fc8"},
-]
-click-repl = [
-    {file = "click-repl-0.2.0.tar.gz", hash = "sha256:cd12f68d745bf6151210790540b4cb064c7b13e571bc64b6957d98d120dacfd8"},
-    {file = "click_repl-0.2.0-py3-none-any.whl", hash = "sha256:94b3fbbc9406a236f176e0506524b2937e4b23b6f4c0c0b2a0a83f8a64e9194b"},
-]
-colorama = [
-    {file = "colorama-0.4.4-py2.py3-none-any.whl", hash = "sha256:9f47eda37229f68eee03b24b9748937c7dc3868f906e8ba69fbcbdd3bc5dc3e2"},
-    {file = "colorama-0.4.4.tar.gz", hash = "sha256:5941b2b48a20143d2267e95b1c2a7603ce057ee39fd88e7329b0c292aa16869b"},
-]
-colour = [
-    {file = "colour-0.1.5-py2.py3-none-any.whl", hash = "sha256:33f6db9d564fadc16e59921a56999b79571160ce09916303d35346dddc17978c"},
-    {file = "colour-0.1.5.tar.gz", hash = "sha256:af20120fefd2afede8b001fbef2ea9da70ad7d49fafdb6489025dae8745c3aee"},
-]
-configobj = [
-    {file = "configobj-5.0.6.tar.gz", hash = "sha256:a2f5650770e1c87fb335af19a9b7eb73fc05ccf22144eb68db7d00cd2bcb0902"},
-]
-coverage = [
-    {file = "coverage-6.0.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:1549e1d08ce38259de2bc3e9a0d5f3642ff4a8f500ffc1b2df73fd621a6cdfc0"},
-    {file = "coverage-6.0.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bcae10fccb27ca2a5f456bf64d84110a5a74144be3136a5e598f9d9fb48c0caa"},
-    {file = "coverage-6.0.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:53a294dc53cfb39c74758edaa6305193fb4258a30b1f6af24b360a6c8bd0ffa7"},
-    {file = "coverage-6.0.2-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:8251b37be1f2cd9c0e5ccd9ae0380909c24d2a5ed2162a41fcdbafaf59a85ebd"},
-    {file = "coverage-6.0.2-cp310-cp310-win32.whl", hash = "sha256:db42baa892cba723326284490283a68d4de516bfb5aaba369b4e3b2787a778b7"},
-    {file = "coverage-6.0.2-cp310-cp310-win_amd64.whl", hash = "sha256:bbffde2a68398682623d9dd8c0ca3f46fda074709b26fcf08ae7a4c431a6ab2d"},
-    {file = "coverage-6.0.2-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:60e51a3dd55540bec686d7fff61b05048ca31e804c1f32cbb44533e6372d9cc3"},
-    {file = "coverage-6.0.2-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6a6a9409223a27d5ef3cca57dd7cd4dfcb64aadf2fad5c3b787830ac9223e01a"},
-    {file = "coverage-6.0.2-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:4b34ae4f51bbfa5f96b758b55a163d502be3dcb24f505d0227858c2b3f94f5b9"},
-    {file = "coverage-6.0.2-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:3bbda1b550e70fa6ac40533d3f23acd4f4e9cb4e6e77251ce77fdf41b3309fb2"},
-    {file = "coverage-6.0.2-cp36-cp36m-win32.whl", hash = "sha256:4e28d2a195c533b58fc94a12826f4431726d8eb029ac21d874345f943530c122"},
-    {file = "coverage-6.0.2-cp36-cp36m-win_amd64.whl", hash = "sha256:a82d79586a0a4f5fd1cf153e647464ced402938fbccb3ffc358c7babd4da1dd9"},
-    {file = "coverage-6.0.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:3be1206dc09fb6298de3fce70593e27436862331a85daee36270b6d0e1c251c4"},
-    {file = "coverage-6.0.2-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c9cd3828bbe1a40070c11fe16a51df733fd2f0cb0d745fb83b7b5c1f05967df7"},
-    {file = "coverage-6.0.2-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:d036dc1ed8e1388e995833c62325df3f996675779541f682677efc6af71e96cc"},
-    {file = "coverage-6.0.2-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:04560539c19ec26995ecfb3d9307ff154fbb9a172cb57e3b3cfc4ced673103d1"},
-    {file = "coverage-6.0.2-cp37-cp37m-win32.whl", hash = "sha256:e4fb7ced4d9dec77d6cf533acfbf8e1415fe799430366affb18d69ee8a3c6330"},
-    {file = "coverage-6.0.2-cp37-cp37m-win_amd64.whl", hash = "sha256:77b1da5767ed2f44611bc9bc019bc93c03fa495728ec389759b6e9e5039ac6b1"},
-    {file = "coverage-6.0.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:61b598cbdbaae22d9e34e3f675997194342f866bb1d781da5d0be54783dce1ff"},
-    {file = "coverage-6.0.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:36e9040a43d2017f2787b28d365a4bb33fcd792c7ff46a047a04094dc0e2a30d"},
-    {file = "coverage-6.0.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:9f1627e162e3864a596486774876415a7410021f4b67fd2d9efdf93ade681afc"},
-    {file = "coverage-6.0.2-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:e7a0b42db2a47ecb488cde14e0f6c7679a2c5a9f44814393b162ff6397fcdfbb"},
-    {file = "coverage-6.0.2-cp38-cp38-win32.whl", hash = "sha256:a1b73c7c4d2a42b9d37dd43199c5711d91424ff3c6c22681bc132db4a4afec6f"},
-    {file = "coverage-6.0.2-cp38-cp38-win_amd64.whl", hash = "sha256:1db67c497688fd4ba85b373b37cc52c50d437fd7267520ecd77bddbd89ea22c9"},
-    {file = "coverage-6.0.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:f2f184bf38e74f152eed7f87e345b51f3ab0b703842f447c22efe35e59942c24"},
-    {file = "coverage-6.0.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cd1cf1deb3d5544bd942356364a2fdc8959bad2b6cf6eb17f47d301ea34ae822"},
-    {file = "coverage-6.0.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:ad9b8c1206ae41d46ec7380b78ba735ebb77758a650643e841dd3894966c31d0"},
-    {file = "coverage-6.0.2-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:381d773d896cc7f8ba4ff3b92dee4ed740fb88dfe33b6e42efc5e8ab6dfa1cfe"},
-    {file = "coverage-6.0.2-cp39-cp39-win32.whl", hash = "sha256:424c44f65e8be58b54e2b0bd1515e434b940679624b1b72726147cfc6a9fc7ce"},
-    {file = "coverage-6.0.2-cp39-cp39-win_amd64.whl", hash = "sha256:abbff240f77347d17306d3201e14431519bf64495648ca5a49571f988f88dee9"},
-    {file = "coverage-6.0.2-pp36-none-any.whl", hash = "sha256:7092eab374346121805fb637572483270324407bf150c30a3b161fc0c4ca5164"},
-    {file = "coverage-6.0.2-pp37-none-any.whl", hash = "sha256:30922626ce6f7a5a30bdba984ad21021529d3d05a68b4f71ea3b16bda35b8895"},
-    {file = "coverage-6.0.2.tar.gz", hash = "sha256:6807947a09510dc31fa86f43595bf3a14017cd60bf633cc746d52141bfa6b149"},
-]
-cryptography = [
-    {file = "cryptography-35.0.0-cp36-abi3-macosx_10_10_x86_64.whl", hash = "sha256:d57e0cdc1b44b6cdf8af1d01807db06886f10177469312fbde8f44ccbb284bc9"},
-    {file = "cryptography-35.0.0-cp36-abi3-macosx_11_0_arm64.whl", hash = "sha256:ced40344e811d6abba00295ced98c01aecf0c2de39481792d87af4fa58b7b4d6"},
-    {file = "cryptography-35.0.0-cp36-abi3-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:54b2605e5475944e2213258e0ab8696f4f357a31371e538ef21e8d61c843c28d"},
-    {file = "cryptography-35.0.0-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:7b7ceeff114c31f285528ba8b390d3e9cfa2da17b56f11d366769a807f17cbaa"},
-    {file = "cryptography-35.0.0-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2d69645f535f4b2c722cfb07a8eab916265545b3475fdb34e0be2f4ee8b0b15e"},
-    {file = "cryptography-35.0.0-cp36-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4a2d0e0acc20ede0f06ef7aa58546eee96d2592c00f450c9acb89c5879b61992"},
-    {file = "cryptography-35.0.0-cp36-abi3-manylinux_2_24_x86_64.whl", hash = "sha256:07bb7fbfb5de0980590ddfc7f13081520def06dc9ed214000ad4372fb4e3c7f6"},
-    {file = "cryptography-35.0.0-cp36-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:7eba2cebca600a7806b893cb1d541a6e910afa87e97acf2021a22b32da1df52d"},
-    {file = "cryptography-35.0.0-cp36-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:18d90f4711bf63e2fb21e8c8e51ed8189438e6b35a6d996201ebd98a26abbbe6"},
-    {file = "cryptography-35.0.0-cp36-abi3-win32.whl", hash = "sha256:c10c797ac89c746e488d2ee92bd4abd593615694ee17b2500578b63cad6b93a8"},
-    {file = "cryptography-35.0.0-cp36-abi3-win_amd64.whl", hash = "sha256:7075b304cd567694dc692ffc9747f3e9cb393cc4aa4fb7b9f3abd6f5c4e43588"},
-    {file = "cryptography-35.0.0-pp36-pypy36_pp73-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:a688ebcd08250eab5bb5bca318cc05a8c66de5e4171a65ca51db6bd753ff8953"},
-    {file = "cryptography-35.0.0-pp36-pypy36_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d99915d6ab265c22873f1b4d6ea5ef462ef797b4140be4c9d8b179915e0985c6"},
-    {file = "cryptography-35.0.0-pp36-pypy36_pp73-manylinux_2_24_x86_64.whl", hash = "sha256:928185a6d1ccdb816e883f56ebe92e975a262d31cc536429041921f8cb5a62fd"},
-    {file = "cryptography-35.0.0-pp37-pypy37_pp73-macosx_10_10_x86_64.whl", hash = "sha256:ebeddd119f526bcf323a89f853afb12e225902a24d29b55fe18dd6fcb2838a76"},
-    {file = "cryptography-35.0.0-pp37-pypy37_pp73-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:22a38e96118a4ce3b97509443feace1d1011d0571fae81fc3ad35f25ba3ea999"},
-    {file = "cryptography-35.0.0-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eb80e8a1f91e4b7ef8b33041591e6d89b2b8e122d787e87eeb2b08da71bb16ad"},
-    {file = "cryptography-35.0.0-pp37-pypy37_pp73-manylinux_2_24_x86_64.whl", hash = "sha256:abb5a361d2585bb95012a19ed9b2c8f412c5d723a9836418fab7aaa0243e67d2"},
-    {file = "cryptography-35.0.0-pp37-pypy37_pp73-win_amd64.whl", hash = "sha256:1ed82abf16df40a60942a8c211251ae72858b25b7421ce2497c2eb7a1cee817c"},
-    {file = "cryptography-35.0.0.tar.gz", hash = "sha256:9933f28f70d0517686bd7de36166dda42094eac49415459d9bdf5e7df3e0086d"},
-]
-curlylint = [
-    {file = "curlylint-0.12.2-py3-none-any.whl", hash = "sha256:98bc15609ce858387dd70a28c7ddda96e82d0f1cb8bf51b8902532ce0fc1a97e"},
-    {file = "curlylint-0.12.2.tar.gz", hash = "sha256:76b557cf8d007bd92df2dae61a02e65f8aa2ff3e05c6398b1314d92692fbb0d8"},
-]
-decorator = [
-    {file = "decorator-5.1.0-py3-none-any.whl", hash = "sha256:7b12e7c3c6ab203a29e157335e9122cb03de9ab7264b137594103fd4a683b374"},
-    {file = "decorator-5.1.0.tar.gz", hash = "sha256:e59913af105b9860aa2c8d3272d9de5a56a4e608db9a2f167a8480b323d529a7"},
-]
-defusedxml = [
-    {file = "defusedxml-0.7.1-py2.py3-none-any.whl", hash = "sha256:a352e7e428770286cc899e2542b6cdaedb2b4953ff269a210103ec58f6198a61"},
-    {file = "defusedxml-0.7.1.tar.gz", hash = "sha256:1bb3032db185915b62d7c6209c5a8792be6a32ab2fedacc84e01b52c51aa3e69"},
-]
-deprecated = [
-    {file = "Deprecated-1.2.13-py2.py3-none-any.whl", hash = "sha256:64756e3e14c8c5eea9795d93c524551432a0be75629f8f29e67ab8caf076c76d"},
-    {file = "Deprecated-1.2.13.tar.gz", hash = "sha256:43ac5335da90c31c24ba028af536a91d41d53f9e6901ddb021bcc572ce44e38d"},
-]
-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.2.8-py3-none-any.whl", hash = "sha256:42573831292029639b798fe4d3812996bfe4ff3275f04566da90764daec011a5"},
-    {file = "Django-3.2.8.tar.gz", hash = "sha256:f6d2c4069c9b9bfac03bedff927ea1f9e0d29e34525cec8a68fd28eb2a8df7af"},
-]
-django-allauth = [
-    {file = "django-allauth-0.45.0.tar.gz", hash = "sha256:6d46be0e1480316ccd45476db3aefb39db70e038d2a543112d314b76bb999a4e"},
-]
-django-any-js = [
-    {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.5.tar.gz", hash = "sha256:be3db0be6c81fa84742000b89a81c016d70ae66a7ccb620cdef592b1f1a6aaa4"},
-    {file = "django_appconf-1.0.5-py3-none-any.whl", hash = "sha256:ae9f864ee1958c815a965ed63b3fba4874eec13de10236ba063a788f9a17389d"},
-]
-django-auth-ldap = [
-    {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.8.0.tar.gz", hash = "sha256:42a83ad8a3306e081facb7ef405883035ea23a35204aea7c2502391f3911f821"},
-    {file = "django_bleach-0.8.0-py2.py3-none-any.whl", hash = "sha256:2c978b4a7516a9e166fa1d8eba950dcfadcd6b68c80971b527fd36f03050f4c2"},
-]
-django-cachalot = [
-    {file = "django-cachalot-2.4.3.tar.gz", hash = "sha256:2c81390f53d8c2e0ae6f266cff170b5681dad2416e09266ca7ca25f50e892a53"},
-    {file = "django_cachalot-2.4.3-py3-none-any.whl", hash = "sha256:d56ffa280da8317019959801703659616adcf60e1bf6513bd7e154b0b0851014"},
-]
-django-cache-memoize = [
-    {file = "django-cache-memoize-0.1.10.tar.gz", hash = "sha256:63e8faa245a41c0dbad843807e9f21a6e59eba8e6e50df310fdf6485a6749843"},
-    {file = "django_cache_memoize-0.1.10-py3-none-any.whl", hash = "sha256:676299313079cde9242ae84db0160e80b1d44e8dd6bc9b1f4f1247e11b30c9e0"},
-]
-django-celery-beat = [
-    {file = "django-celery-beat-2.2.1.tar.gz", hash = "sha256:97ae5eb309541551bdb07bf60cc57cadacf42a74287560ced2d2c06298620234"},
-    {file = "django_celery_beat-2.2.1-py2.py3-none-any.whl", hash = "sha256:ab43049634fd18dc037927d7c2c7d5f67f95283a20ebbda55f42f8606412e66c"},
-]
-django-celery-email = [
-    {file = "django-celery-email-3.0.0.tar.gz", hash = "sha256:5546cbba80952cc3b8a0ffa4206ce90a4a996a7ffd1c385a2bdb65903ca18ece"},
-    {file = "django_celery_email-3.0.0-py2.py3-none-any.whl", hash = "sha256:0f72da39cb2ea83c69440566e87f27cd72f68f247f98ce99fb29889fcf329406"},
-]
-django-celery-results = [
-    {file = "django_celery_results-2.2.0-py2.py3-none-any.whl", hash = "sha256:d5f83fad9091e52cd6dbb3ca80632153ad14b6cdac4d73258e040f92717237cb"},
-    {file = "django_celery_results-2.2.0.tar.gz", hash = "sha256:cc0285090a306f97f1d4b7929ed98af0475bf6db2568976b3387de4fbe812edc"},
-]
-django-ckeditor = [
-    {file = "django-ckeditor-6.1.0.tar.gz", hash = "sha256:f0d108f67a81a04e26d8de11255fe314f51026eaf8eb0534a807512ae3c21620"},
-    {file = "django_ckeditor-6.1.0-py2.py3-none-any.whl", hash = "sha256:346b26b9d60dc8a88524d0eaaf406f4e91a4b3c22d208ae87aa032bf500b251c"},
-]
-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.5.tar.gz", hash = "sha256:66d7cb628d05c0eb09e25b0923e36bf6fbd3e339c568a199e66b34463119ca13"},
-    {file = "django_colorfield-0.4.5-py3-none-any.whl", hash = "sha256:579128d008b2d15e4df64f102b1ad58432de1b6f1017221c316d630e5c62960a"},
-]
-django-dbbackup = [
-    {file = "django-dbbackup-3.3.0.tar.gz", hash = "sha256:bb109735cae98b64ad084e5b461b7aca2d7b39992f10c9ed9435e3ebb6fb76c8"},
-]
-django-debug-toolbar = [
-    {file = "django-debug-toolbar-3.2.2.tar.gz", hash = "sha256:8c5b13795d4040008ee69ba82dcdd259c49db346cf7d0de6e561a49d191f0860"},
-    {file = "django_debug_toolbar-3.2.2-py3-none-any.whl", hash = "sha256:d7bab7573fab35b0fd029163371b7182f5826c13da69734beb675c761d06a4d3"},
-]
-django-dynamic-preferences = [
-    {file = "django-dynamic-preferences-1.11.0.tar.gz", hash = "sha256:f214c938b5872a17647e2b2ccfd9ad00a90a3c6c4aa83fa65d3c5c446e7a66c7"},
-    {file = "django_dynamic_preferences-1.11.0-py2.py3-none-any.whl", hash = "sha256:31aecebcbfcfb970b78cfa3e5f8cc9f77638efe8e7c90f205a48b01c45ee5002"},
-]
-django-extensions = [
-    {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.1.3.tar.gz", hash = "sha256:36c2a1cefc201df8bd132492e2440ccdc3d9ceb8e421b2ca181a4704ebacd190"},
-    {file = "django_favicon_plus_reloaded-1.1.3-py3-none-any.whl", hash = "sha256:a60b438360e82bf7075b856ff6a80bae20c825373a58deac627810e478c42be3"},
-]
-django-filter = [
-    {file = "django-filter-2.4.0.tar.gz", hash = "sha256:84e9d5bb93f237e451db814ed422a3a625751cbc9968b484ecc74964a8696b06"},
-    {file = "django_filter-2.4.0-py3-none-any.whl", hash = "sha256:e00d32cebdb3d54273c48f4f878f898dced8d5dfaad009438fe61ebdf535ace1"},
-]
-django-formtools = [
-    {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.4.0.tar.gz", hash = "sha256:c58a68ae76922d33e6bdc0e69af1892097838de56e93e78a8361090bcd9f89a0"},
-    {file = "django_guardian-2.4.0-py3-none-any.whl", hash = "sha256:440ca61358427e575323648b25f8384739e54c38b3d655c81d75e0cd0d61b697"},
-]
-django-haystack = [
-    {file = "django-haystack-3.0.tar.gz", hash = "sha256:d490f920afa85471dd1fa5000bc8eff4b704daacbe09aee1a64e75cbc426f3be"},
-]
-django-health-check = [
-    {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"},
-]
-django-ipware = [
-    {file = "django-ipware-4.0.0.tar.gz", hash = "sha256:1294f916f3b3475e40e1b0ec1bd320aa2397978eae672721c81cbc2ed517e9ee"},
-    {file = "django_ipware-4.0.0-py2.py3-none-any.whl", hash = "sha256:116bd0d7940f09bf7ffd465943992e23d87e772a9d6c0d3a57b74040589a383b"},
-]
-django-js-asset = [
-    {file = "django-js-asset-1.2.2.tar.gz", hash = "sha256:c163ae80d2e0b22d8fb598047cd0dcef31f81830e127cfecae278ad574167260"},
-    {file = "django_js_asset-1.2.2-py2.py3-none-any.whl", hash = "sha256:8ec12017f26eec524cab436c64ae73033368a372970af4cf42d9354fcb166bdd"},
-]
-django-js-reverse = [
-    {file = "django-js-reverse-0.9.1.tar.gz", hash = "sha256:2a392d169f44e30b883c30dfcfd917a14167ce8fe196c99d2385b31c90d77aa0"},
-    {file = "django_js_reverse-0.9.1-py2.py3-none-any.whl", hash = "sha256:8134c2ab6307c945edfa90671ca65e85d6c1754d48566bdd6464be259cc80c30"},
-]
-django-jsonstore = [
-    {file = "django-jsonstore-0.5.0.tar.gz", hash = "sha256:896dc10b08f59807eda1c6cebf43cd26e50d0db29d13495c027dc31e464be3c3"},
-    {file = "django_jsonstore-0.5.0-py2-none-any.whl", hash = "sha256:9630c1fb43ae9f8e32733c5cf7d4c3775ba6f08532f517c64025053352d72844"},
-]
-django-maintenance-mode = [
-    {file = "django-maintenance-mode-0.16.1.tar.gz", hash = "sha256:da1f77f431ab5c55913459adb3c259e091f783ffc72de701690826aaaccce4ca"},
-    {file = "django_maintenance_mode-0.16.1-py3-none-any.whl", hash = "sha256:1bfac0b34429a9f6dbb0db169fb753f49f24ca155ae148d7526ff395303c158d"},
-]
-django-material = [
-    {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-model-utils = [
-    {file = "django-model-utils-4.2.0.tar.gz", hash = "sha256:e7a95e102f9c9653427eadab980d5d59e1dea972913b9c9e01ac37f86bba0ddf"},
-    {file = "django_model_utils-4.2.0-py3-none-any.whl", hash = "sha256:a768a25c80514e0ad4e4a6f9c02c44498985f36c5dfdea47b5b1e8cf994beba6"},
-]
-django-oauth-toolkit = [
-    {file = "django-oauth-toolkit-1.5.0.tar.gz", hash = "sha256:650e5ef2244d1d8db8f507137e0d1e8b8aad1f4086a4a610526e8851f9a38308"},
-    {file = "django_oauth_toolkit-1.5.0-py3-none-any.whl", hash = "sha256:b2e346a7c1e222774bfb370f21b556b92b408395b4c23914e2d1b241b2e5376a"},
-]
-django-otp = [
-    {file = "django-otp-1.1.1.tar.gz", hash = "sha256:4c90cdaed683d736b0efafc034a3c6b410e1be2a53c24da287165b1f371d8776"},
-    {file = "django_otp-1.1.1-py3-none-any.whl", hash = "sha256:0c03a471db9e876f3671314bc9a65bd56a5c3c108ee0562c473701310bba4a77"},
-]
-django-otp-yubikey = [
-    {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.2.0.tar.gz", hash = "sha256:52b2e5970133ec5ab701218b802f7ab237229854dc95fd239b7e9e77dc43731d"},
-    {file = "django_phonenumber_field-5.2.0-py3-none-any.whl", hash = "sha256:5547fb2b2cc690a306ba77a5038419afc8fa8298a486fb7895008e9067cc7e75"},
-]
-django-polymorphic = [
-    {file = "django-polymorphic-3.0.0.tar.gz", hash = "sha256:9d886f19f031d26bb1391c055ed9be06fb226a04a4cec1842b372c58873b3caa"},
-    {file = "django_polymorphic-3.0.0-py2.py3-none-any.whl", hash = "sha256:73b75eb44ea302bd32820f8661e469509d245ce7f7ff09cd2ad149e5c42034ff"},
-]
-django-prometheus = [
-    {file = "django-prometheus-2.1.0.tar.gz", hash = "sha256:dd3f8da1399140fbef5c00d1526a23d1ade286b144281c325f8e409a781643f2"},
-    {file = "django_prometheus-2.1.0-py2.py3-none-any.whl", hash = "sha256:c338d6efde1ca336e90c540b5e87afe9287d7bcc82d651a778f302b0be17a933"},
-]
-django-redis = [
-    {file = "django-redis-5.0.0.tar.gz", hash = "sha256:048f665bbe27f8ff2edebae6aa9c534ab137f1e8fa7234147ef470df3f3aa9b8"},
-    {file = "django_redis-5.0.0-py3-none-any.whl", hash = "sha256:97739ca9de3f964c51412d1d7d8aecdfd86737bb197fce6e1ff12620c63c97ee"},
-]
-django-render-block = [
-    {file = "django-render-block-0.8.1.tar.gz", hash = "sha256:edbc5d444cc50f3eb3387cf17f6f1014bf19d6018f680861cdeae9e0306003fa"},
-    {file = "django_render_block-0.8.1-py3-none-any.whl", hash = "sha256:903969efd0949f750c5fe71affe6e6b1ea66d03005c102a67fda36d5b9f4e1e1"},
-]
-django-reversion = [
-    {file = "django-reversion-4.0.0.tar.gz", hash = "sha256:ad6d714b4b9b824e22b88d47201cc0f74b5c4294c8d4e1f8d7ac7c3631ef3188"},
-    {file = "django_reversion-4.0.0-py3-none-any.whl", hash = "sha256:f059c654e38c0dd8dccd7f0990aa2f6d9ad22dab55c5e095f9596aeda8079dcd"},
-]
-django-sass-processor = [
-    {file = "django-sass-processor-1.0.0.tar.gz", hash = "sha256:cb90efee38cd7b0fe727c78d8993ad7804de33f40328200dfc1a481307ef0466"},
-]
-django-select2 = [
-    {file = "django-select2-7.7.3.tar.gz", hash = "sha256:063d29df5598ec6549a4d49cc4beb85d585987a4e470224592034aff2d4f0a3f"},
-    {file = "django_select2-7.7.3-py2.py3-none-any.whl", hash = "sha256:7bf5b35595624a3318f46d69f639e38dafb123fa4a6f82d63a6a1ac6d532317d"},
-]
-django-storages = [
-    {file = "django-storages-1.12.2.tar.gz", hash = "sha256:0013ebe4904521e2fa28f33591a03a7210304d73363e7eadd7cdcf81c12ba003"},
-    {file = "django_storages-1.12.2-py3-none-any.whl", hash = "sha256:683b70617f4be9baa17b2d6d7df46cf6afe31b5eb17ece3ca82f773f555592a7"},
-]
-django-stubs = [
-    {file = "django-stubs-1.9.0.tar.gz", hash = "sha256:664843091636a917faf5256d028476559dc360fdef9050b6df87ab61b21607bf"},
-    {file = "django_stubs-1.9.0-py3-none-any.whl", hash = "sha256:59c9f81af64d214b1954eaf90f037778c8d2b9c2de946a3cda177fefcf588fbd"},
-]
-django-stubs-ext = [
-    {file = "django-stubs-ext-0.3.1.tar.gz", hash = "sha256:783c198d7e39a41be0b90fd843fa2770243a642922af679be4b19e03b82c8c28"},
-    {file = "django_stubs_ext-0.3.1-py3-none-any.whl", hash = "sha256:a51a3e9e844d4e1cacaaedbb33bf3def78a3956eed5d9575a640bd97ccd99cec"},
-]
-django-tables2 = [
-    {file = "django-tables2-2.4.1.tar.gz", hash = "sha256:6c72dd208358539e789e4c0efd7d151e43283a4aa4093a35f44c43489e7ddeaa"},
-    {file = "django_tables2-2.4.1-py2.py3-none-any.whl", hash = "sha256:50762bf3d7c61a4eb70e763c3e278650d7266bb78d0497fc8fafcf4e507c9a64"},
-]
-django-templated-email = [
-    {file = "django-templated-email-3.0.0.tar.gz", hash = "sha256:49d61840ec551e640adaf341146e94d6f9058ae01df964480850bf988046e5eb"},
-    {file = "django_templated_email-3.0.0-py3-none-any.whl", hash = "sha256:bf1b68ffe6c8794c0c50e2ce20e3a166c6d511b3879abbd3cf059a3fc2fe2e60"},
-]
-django-timezone-field = [
-    {file = "django-timezone-field-4.2.1.tar.gz", hash = "sha256:97780cde658daa5094ae515bb55ca97c1352928ab554041207ad515dee3fe971"},
-    {file = "django_timezone_field-4.2.1-py3-none-any.whl", hash = "sha256:6dc782e31036a58da35b553bd00c70f112d794700025270d8a6a4c1d2e5b26c6"},
-]
-django-titofisto = [
-    {file = "django-titofisto-0.1.2.post1.tar.gz", hash = "sha256:e3b0783142d075aadda1c041061f84affdbe767ffaeebd0f615359723339c208"},
-    {file = "django_titofisto-0.1.2.post1-py3-none-any.whl", hash = "sha256:abebb5db39562bde9999ffd4afe67d43eefae37c8a7ec7abbe611420e6f8aca0"},
-]
-django-two-factor-auth = [
-    {file = "django-two-factor-auth-1.13.1.tar.gz", hash = "sha256:a20e03d256fd9fd668988545f052cedcc47e5a981888562e5e27d0bb83deae89"},
-    {file = "django_two_factor_auth-1.13.1-py2.py3-none-any.whl", hash = "sha256:d270d4288731233621a9462a89a8dfed2dcb86fa354125c816a89772d55f9e29"},
-]
-django-uwsgi-ng = [
-    {file = "django-uwsgi-ng-1.1.2.tar.gz", hash = "sha256:1df2ffa642f7a831bd8d7f7e459f7b0821113b37174ccf4bf977e4467d45d9b3"},
-    {file = "django_uwsgi_ng-1.1.2-py3-none-any.whl", hash = "sha256:8b1a489a1ed9e56da0efadfa86ec306b532e5cd953fe34b234aaefc26898c649"},
-]
-django-widget-tweaks = [
-    {file = "django-widget-tweaks-1.4.9.tar.gz", hash = "sha256:19bcb66a4a9e68493ced04e7124882d753c5be517ed001556f9e35a40147f545"},
-    {file = "django_widget_tweaks-1.4.9-py2.py3-none-any.whl", hash = "sha256:d6c64fbf92cd2df9031f597c1374982233c05a1190d295c39d1c57ce007569c7"},
-]
-django-yarnpkg = [
-    {file = "django-yarnpkg-6.0.1.tar.gz", hash = "sha256:aa059347b246c6f242401581d2c129bdcb45aa726be59fe2f288762a9843348a"},
-]
-djangorestframework = [
-    {file = "djangorestframework-3.12.4-py3-none-any.whl", hash = "sha256:6d1d59f623a5ad0509fe0d6bfe93cbdfe17b8116ebc8eda86d45f6e16e819aaf"},
-    {file = "djangorestframework-3.12.4.tar.gz", hash = "sha256:f747949a8ddac876e879190df194b925c177cdeb725a099db1460872f7c0a7f2"},
-]
-docutils = [
-    {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"},
-    {file = "dparse-0.5.1.tar.gz", hash = "sha256:a1b5f169102e1c894f9a7d5ccf6f9402a836a5d24be80a986c7ce9eaed78f367"},
-]
-dynaconf = [
-    {file = "dynaconf-3.1.7-py2.py3-none-any.whl", hash = "sha256:f52fe5db7622da56a552275e8f64e4df46e3b4ae11158831b042e8ba2f6d1c96"},
-    {file = "dynaconf-3.1.7.tar.gz", hash = "sha256:e9d80b46ba4d9372f2f40c812594c963f74178140c0b596e57f2881001fc4d35"},
-]
-flake8 = [
-    {file = "flake8-3.9.2-py2.py3-none-any.whl", hash = "sha256:bf8fd333346d844f616e8d47905ef3a3384edae6b4e9beb0c5101e25e3110907"},
-    {file = "flake8-3.9.2.tar.gz", hash = "sha256:07528381786f2a6237b061f6e96610a4167b226cb926e2aa2b6b1d78057c576b"},
-]
-flake8-bandit = [
-    {file = "flake8_bandit-2.1.2.tar.gz", hash = "sha256:687fc8da2e4a239b206af2e54a90093572a60d0954f3054e23690739b0b0de3b"},
-]
-flake8-black = [
-    {file = "flake8-black-0.2.3.tar.gz", hash = "sha256:c199844bc1b559d91195ebe8620216f21ed67f2cc1ff6884294c91a0d2492684"},
-    {file = "flake8_black-0.2.3-py3-none-any.whl", hash = "sha256:cc080ba5b3773b69ba102b6617a00cc4ecbad8914109690cfda4d565ea435d96"},
-]
-flake8-builtins = [
-    {file = "flake8-builtins-1.5.3.tar.gz", hash = "sha256:09998853b2405e98e61d2ff3027c47033adbdc17f9fe44ca58443d876eb00f3b"},
-    {file = "flake8_builtins-1.5.3-py2.py3-none-any.whl", hash = "sha256:7706babee43879320376861897e5d1468e396a40b8918ed7bccf70e5f90b8687"},
-]
-flake8-django = [
-    {file = "flake8-django-1.1.2.tar.gz", hash = "sha256:b4314abb5bacda450d2eae564a0604447111b1b98188e46bca41682ad2ab59d6"},
-    {file = "flake8_django-1.1.2-py3-none-any.whl", hash = "sha256:f8bfdbe8352c2c5f3788c2a2f6652dd2604af24af07a5aa112206d63ae228fdc"},
-]
-flake8-docstrings = [
-    {file = "flake8-docstrings-1.6.0.tar.gz", hash = "sha256:9fe7c6a306064af8e62a055c2f61e9eb1da55f84bb39caef2b84ce53708ac34b"},
-    {file = "flake8_docstrings-1.6.0-py2.py3-none-any.whl", hash = "sha256:99cac583d6c7e32dd28bbfbef120a7c0d1b6dde4adb5a9fd441c4227a6534bde"},
-]
-flake8-fixme = [
-    {file = "flake8-fixme-1.1.1.tar.gz", hash = "sha256:50cade07d27a4c30d4f12351478df87339e67640c83041b664724bda6d16f33a"},
-    {file = "flake8_fixme-1.1.1-py2.py3-none-any.whl", hash = "sha256:226a6f2ef916730899f29ac140bed5d4a17e5aba79f00a0e3ae1eff1997cb1ac"},
-]
-flake8-isort = [
-    {file = "flake8-isort-4.1.1.tar.gz", hash = "sha256:d814304ab70e6e58859bc5c3e221e2e6e71c958e7005239202fee19c24f82717"},
-    {file = "flake8_isort-4.1.1-py3-none-any.whl", hash = "sha256:c4e8b6dcb7be9b71a02e6e5d4196cefcef0f3447be51e82730fb336fff164949"},
-]
-flake8-mypy = [
-    {file = "flake8-mypy-17.8.0.tar.gz", hash = "sha256:47120db63aff631ee1f84bac6fe8e64731dc66da3efc1c51f85e15ade4a3ba18"},
-    {file = "flake8_mypy-17.8.0-py35.py36-none-any.whl", hash = "sha256:cff009f4250e8391bf48990093cff85802778c345c8449d6498b62efefeebcbc"},
-]
-flake8-polyfill = [
-    {file = "flake8-polyfill-1.0.2.tar.gz", hash = "sha256:e44b087597f6da52ec6393a709e7108b2905317d0c0b744cdca6208e670d8eda"},
-    {file = "flake8_polyfill-1.0.2-py2.py3-none-any.whl", hash = "sha256:12be6a34ee3ab795b19ca73505e7b55826d5f6ad7230d31b18e106400169b9e9"},
-]
-flake8-rst-docstrings = [
-    {file = "flake8-rst-docstrings-0.2.3.tar.gz", hash = "sha256:3045794e1c8467fba33aaea5c246b8369efc9c44ef8b0b20199bb6df7a4bd47b"},
-    {file = "flake8_rst_docstrings-0.2.3-py3-none-any.whl", hash = "sha256:565bbb391d7e4d0042924102221e9857ad72929cdd305b26501736ec22c1451a"},
-]
-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.9-py3-none-any.whl", hash = "sha256:8033ad4e853066ba6ca92050b9df2f89301b8fc8bf7e9324d412a63f8bf1a8fd"},
-    {file = "gitdb-4.0.9.tar.gz", hash = "sha256:bac2fd45c0a1c9cf619e63a90d62bdc63892ef92387424b855792a6cabe789aa"},
-]
-gitpython = [
-    {file = "GitPython-3.1.24-py3-none-any.whl", hash = "sha256:dc0a7f2f697657acc8d7f89033e8b1ea94dd90356b2983bca89dc8d2ab3cc647"},
-    {file = "GitPython-3.1.24.tar.gz", hash = "sha256:df83fdf5e684fef7c6ee2c02fc68a5ceb7e7e759d08b694088d0cacb4eba59e5"},
-]
-haystack-redis = [
-    {file = "haystack-redis-0.0.1.tar.gz", hash = "sha256:ccfea88bdc1387c9f7f6f19e9bc062a3612039ef94cfd3e78cf59a96ddd269b2"},
-    {file = "haystack_redis-0.0.1-py3-none-any.whl", hash = "sha256:4fdeee5a9d8daadb1fed4584fd2ffbb25b1ed2315dacb97b53093756d6b54467"},
-]
-html2text = [
-    {file = "html2text-2020.1.16-py3-none-any.whl", hash = "sha256:c7c629882da0cf377d66f073329ccf34a12ed2adf0169b9285ae4e63ef54c82b"},
-    {file = "html2text-2020.1.16.tar.gz", hash = "sha256:e296318e16b059ddb97f7a8a1d6a5c1d7af4544049a01e261731d2d5cc277bbb"},
-]
-idna = [
-    {file = "idna-3.3-py3-none-any.whl", hash = "sha256:84d9dd047ffa80596e0f246e2eab0b391788b0503584e8945f2368256d2735ff"},
-    {file = "idna-3.3.tar.gz", hash = "sha256:9d643ff0a55b762d5cdb124b8eaa99c66322e2157b69160bc32796e824360e6d"},
-]
-imagesize = [
-    {file = "imagesize-1.2.0-py2.py3-none-any.whl", hash = "sha256:6965f19a6a2039c7d48bca7dba2473069ff854c36ae6f19d2cde309d998228a1"},
-    {file = "imagesize-1.2.0.tar.gz", hash = "sha256:b1f6b5a4eab1f73479a50fb79fcf729514a900c341d8503d62a62dbc4127a2b1"},
-]
-iniconfig = [
-    {file = "iniconfig-1.1.1-py2.py3-none-any.whl", hash = "sha256:011e24c64b7f47f6ebd835bb12a743f2fbe9a26d4cecaa7f53bc4f35ee9da8b3"},
-    {file = "iniconfig-1.1.1.tar.gz", hash = "sha256:bc3af051d7d14b2ee5ef9969666def0cd1a000e121eaea580d4a313df4b37f32"},
-]
-ipython = [
-    {file = "ipython-7.28.0-py3-none-any.whl", hash = "sha256:f16148f9163e1e526f1008d7c8d966d9c15600ca20d1a754287cf96d00ba6f1d"},
-    {file = "ipython-7.28.0.tar.gz", hash = "sha256:2097be5c814d1b974aea57673176a924c4c8c9583890e7a5f082f547b9975b11"},
-]
-isort = [
-    {file = "isort-5.9.3-py3-none-any.whl", hash = "sha256:e17d6e2b81095c9db0a03a8025a957f334d6ea30b26f9ec70805411e5c7c81f2"},
-    {file = "isort-5.9.3.tar.gz", hash = "sha256:9c2ea1e62d871267b78307fe511c0838ba0da28698c5732d54e2790bf3ba9899"},
-]
-jedi = [
-    {file = "jedi-0.18.0-py2.py3-none-any.whl", hash = "sha256:18456d83f65f400ab0c2d3319e48520420ef43b23a086fdc05dff34132f0fb93"},
-    {file = "jedi-0.18.0.tar.gz", hash = "sha256:92550a404bad8afed881a137ec9a461fed49eca661414be45059329614ed0707"},
-]
-jinja2 = [
-    {file = "Jinja2-3.0.2-py3-none-any.whl", hash = "sha256:8569982d3f0889eed11dd620c706d39b60c36d6d25843961f33f77fb6bc6b20c"},
-    {file = "Jinja2-3.0.2.tar.gz", hash = "sha256:827a0e32839ab1600d4eb1c4c33ec5a8edfbc5cb42dafa13b81f182f97784b45"},
-]
-jmespath = [
-    {file = "jmespath-0.10.0-py2.py3-none-any.whl", hash = "sha256:cdf6525904cc597730141d61b36f2e4b8ecc257c420fa2f4549bac2c2d0cb72f"},
-    {file = "jmespath-0.10.0.tar.gz", hash = "sha256:b85d0567b8666149a93172712e68920734333c0ce7e89b78b3e987f71e5ed4f9"},
-]
-jwcrypto = [
-    {file = "jwcrypto-1.0-py2.py3-none-any.whl", hash = "sha256:db93a656d9a7a35dda5a68deb5c9f301f4e60507d8aef1559e0637b9ac497137"},
-    {file = "jwcrypto-1.0.tar.gz", hash = "sha256:f88816eb0a41b8f006af978ced5f171f33782525006cdb055b536a40f4d46ac9"},
-]
-kombu = [
-    {file = "kombu-5.1.0-py3-none-any.whl", hash = "sha256:e2dedd8a86c9077c350555153825a31e456a0dc20c15d5751f00137ec9c75f0a"},
-    {file = "kombu-5.1.0.tar.gz", hash = "sha256:01481d99f4606f6939cdc9b637264ed353ee9e3e4f62cfb582324142c41a572d"},
-]
-libsass = [
-    {file = "libsass-0.21.0-cp27-cp27m-macosx_10_14_x86_64.whl", hash = "sha256:06c8776417fe930714bdc930a3d7e795ae3d72be6ac883ff72a1b8f7c49e5ffb"},
-    {file = "libsass-0.21.0-cp27-cp27m-win32.whl", hash = "sha256:a005f298f64624f313a3ac618ab03f844c71d84ae4f4a4aec4b68d2a4ffe75eb"},
-    {file = "libsass-0.21.0-cp27-cp27m-win_amd64.whl", hash = "sha256:6b984510ed94993708c0d697b4fef2d118929bbfffc3b90037be0f5ccadf55e7"},
-    {file = "libsass-0.21.0-cp27-cp27mu-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:1e25dd9047a9392d3c59a0b869e0404f2b325a03871ee45285ee33b3664f5613"},
-    {file = "libsass-0.21.0-cp36-abi3-macosx_10_14_x86_64.whl", hash = "sha256:12f39712de38689a8b785b7db41d3ba2ea1d46f9379d81ea4595802d91fa6529"},
-    {file = "libsass-0.21.0-cp36-abi3-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:e2b1a7d093f2e76dc694c17c0c285e846d0b0deb0e8b21dc852ba1a3a4e2f1d6"},
-    {file = "libsass-0.21.0-cp36-abi3-win32.whl", hash = "sha256:abc29357ee540849faf1383e1746d40d69ed5cb6d4c346df276b258f5aa8977a"},
-    {file = "libsass-0.21.0-cp36-abi3-win_amd64.whl", hash = "sha256:659ae41af8708681fa3ec73f47b9735a6725e71c3b66ff570bfce78952f2314e"},
-    {file = "libsass-0.21.0.tar.gz", hash = "sha256:d5ba529d9ce668be9380563279f3ffe988f27bc5b299c5a28453df2e0b0fbaf2"},
-]
-license-expression = [
-    {file = "license-expression-1.2.tar.gz", hash = "sha256:7960e1dfdf20d127e75ead931476f2b5c7556df05b117a73880b22ade17d1abc"},
-    {file = "license_expression-1.2-py2.py3-none-any.whl", hash = "sha256:6d97906380cecfc758a77f6d38c6760f2afade7e83d2b8295e234fe21f486fb8"},
-]
-markupsafe = [
-    {file = "MarkupSafe-2.0.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:d8446c54dc28c01e5a2dbac5a25f071f6653e6e40f3a8818e8b45d790fe6ef53"},
-    {file = "MarkupSafe-2.0.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:36bc903cbb393720fad60fc28c10de6acf10dc6cc883f3e24ee4012371399a38"},
-    {file = "MarkupSafe-2.0.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2d7d807855b419fc2ed3e631034685db6079889a1f01d5d9dac950f764da3dad"},
-    {file = "MarkupSafe-2.0.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:add36cb2dbb8b736611303cd3bfcee00afd96471b09cda130da3581cbdc56a6d"},
-    {file = "MarkupSafe-2.0.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:168cd0a3642de83558a5153c8bd34f175a9a6e7f6dc6384b9655d2697312a646"},
-    {file = "MarkupSafe-2.0.1-cp310-cp310-win32.whl", hash = "sha256:99df47edb6bda1249d3e80fdabb1dab8c08ef3975f69aed437cb69d0a5de1e28"},
-    {file = "MarkupSafe-2.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:e0f138900af21926a02425cf736db95be9f4af72ba1bb21453432a07f6082134"},
-    {file = "MarkupSafe-2.0.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:f9081981fe268bd86831e5c75f7de206ef275defcb82bc70740ae6dc507aee51"},
-    {file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:0955295dd5eec6cb6cc2fe1698f4c6d84af2e92de33fbcac4111913cd100a6ff"},
-    {file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:0446679737af14f45767963a1a9ef7620189912317d095f2d9ffa183a4d25d2b"},
-    {file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux2010_i686.whl", hash = "sha256:f826e31d18b516f653fe296d967d700fddad5901ae07c622bb3705955e1faa94"},
-    {file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:fa130dd50c57d53368c9d59395cb5526eda596d3ffe36666cd81a44d56e48872"},
-    {file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:905fec760bd2fa1388bb5b489ee8ee5f7291d692638ea5f67982d968366bef9f"},
-    {file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bf5d821ffabf0ef3533c39c518f3357b171a1651c1ff6827325e4489b0e46c3c"},
-    {file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:0d4b31cc67ab36e3392bbf3862cfbadac3db12bdd8b02a2731f509ed5b829724"},
-    {file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:baa1a4e8f868845af802979fcdbf0bb11f94f1cb7ced4c4b8a351bb60d108145"},
-    {file = "MarkupSafe-2.0.1-cp36-cp36m-win32.whl", hash = "sha256:6c4ca60fa24e85fe25b912b01e62cb969d69a23a5d5867682dd3e80b5b02581d"},
-    {file = "MarkupSafe-2.0.1-cp36-cp36m-win_amd64.whl", hash = "sha256:b2f4bf27480f5e5e8ce285a8c8fd176c0b03e93dcc6646477d4630e83440c6a9"},
-    {file = "MarkupSafe-2.0.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:0717a7390a68be14b8c793ba258e075c6f4ca819f15edfc2a3a027c823718567"},
-    {file = "MarkupSafe-2.0.1-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:6557b31b5e2c9ddf0de32a691f2312a32f77cd7681d8af66c2692efdbef84c18"},
-    {file = "MarkupSafe-2.0.1-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:49e3ceeabbfb9d66c3aef5af3a60cc43b85c33df25ce03d0031a608b0a8b2e3f"},
-    {file = "MarkupSafe-2.0.1-cp37-cp37m-manylinux2010_i686.whl", hash = "sha256:d7f9850398e85aba693bb640262d3611788b1f29a79f0c93c565694658f4071f"},
-    {file = "MarkupSafe-2.0.1-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:6a7fae0dd14cf60ad5ff42baa2e95727c3d81ded453457771d02b7d2b3f9c0c2"},
-    {file = "MarkupSafe-2.0.1-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:b7f2d075102dc8c794cbde1947378051c4e5180d52d276987b8d28a3bd58c17d"},
-    {file = "MarkupSafe-2.0.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e9936f0b261d4df76ad22f8fee3ae83b60d7c3e871292cd42f40b81b70afae85"},
-    {file = "MarkupSafe-2.0.1-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:2a7d351cbd8cfeb19ca00de495e224dea7e7d919659c2841bbb7f420ad03e2d6"},
-    {file = "MarkupSafe-2.0.1-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:60bf42e36abfaf9aff1f50f52644b336d4f0a3fd6d8a60ca0d054ac9f713a864"},
-    {file = "MarkupSafe-2.0.1-cp37-cp37m-win32.whl", hash = "sha256:a30e67a65b53ea0a5e62fe23682cfe22712e01f453b95233b25502f7c61cb415"},
-    {file = "MarkupSafe-2.0.1-cp37-cp37m-win_amd64.whl", hash = "sha256:611d1ad9a4288cf3e3c16014564df047fe08410e628f89805e475368bd304914"},
-    {file = "MarkupSafe-2.0.1-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:5bb28c636d87e840583ee3adeb78172efc47c8b26127267f54a9c0ec251d41a9"},
-    {file = "MarkupSafe-2.0.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:be98f628055368795d818ebf93da628541e10b75b41c559fdf36d104c5787066"},
-    {file = "MarkupSafe-2.0.1-cp38-cp38-manylinux1_i686.whl", hash = "sha256:1d609f577dc6e1aa17d746f8bd3c31aa4d258f4070d61b2aa5c4166c1539de35"},
-    {file = "MarkupSafe-2.0.1-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:7d91275b0245b1da4d4cfa07e0faedd5b0812efc15b702576d103293e252af1b"},
-    {file = "MarkupSafe-2.0.1-cp38-cp38-manylinux2010_i686.whl", hash = "sha256:01a9b8ea66f1658938f65b93a85ebe8bc016e6769611be228d797c9d998dd298"},
-    {file = "MarkupSafe-2.0.1-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:47ab1e7b91c098ab893b828deafa1203de86d0bc6ab587b160f78fe6c4011f75"},
-    {file = "MarkupSafe-2.0.1-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:97383d78eb34da7e1fa37dd273c20ad4320929af65d156e35a5e2d89566d9dfb"},
-    {file = "MarkupSafe-2.0.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6fcf051089389abe060c9cd7caa212c707e58153afa2c649f00346ce6d260f1b"},
-    {file = "MarkupSafe-2.0.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:5855f8438a7d1d458206a2466bf82b0f104a3724bf96a1c781ab731e4201731a"},
-    {file = "MarkupSafe-2.0.1-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:3dd007d54ee88b46be476e293f48c85048603f5f516008bee124ddd891398ed6"},
-    {file = "MarkupSafe-2.0.1-cp38-cp38-win32.whl", hash = "sha256:023cb26ec21ece8dc3907c0e8320058b2e0cb3c55cf9564da612bc325bed5e64"},
-    {file = "MarkupSafe-2.0.1-cp38-cp38-win_amd64.whl", hash = "sha256:984d76483eb32f1bcb536dc27e4ad56bba4baa70be32fa87152832cdd9db0833"},
-    {file = "MarkupSafe-2.0.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:2ef54abee730b502252bcdf31b10dacb0a416229b72c18b19e24a4509f273d26"},
-    {file = "MarkupSafe-2.0.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:3c112550557578c26af18a1ccc9e090bfe03832ae994343cfdacd287db6a6ae7"},
-    {file = "MarkupSafe-2.0.1-cp39-cp39-manylinux1_i686.whl", hash = "sha256:53edb4da6925ad13c07b6d26c2a852bd81e364f95301c66e930ab2aef5b5ddd8"},
-    {file = "MarkupSafe-2.0.1-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:f5653a225f31e113b152e56f154ccbe59eeb1c7487b39b9d9f9cdb58e6c79dc5"},
-    {file = "MarkupSafe-2.0.1-cp39-cp39-manylinux2010_i686.whl", hash = "sha256:4efca8f86c54b22348a5467704e3fec767b2db12fc39c6d963168ab1d3fc9135"},
-    {file = "MarkupSafe-2.0.1-cp39-cp39-manylinux2010_x86_64.whl", hash = "sha256:ab3ef638ace319fa26553db0624c4699e31a28bb2a835c5faca8f8acf6a5a902"},
-    {file = "MarkupSafe-2.0.1-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:f8ba0e8349a38d3001fae7eadded3f6606f0da5d748ee53cc1dab1d6527b9509"},
-    {file = "MarkupSafe-2.0.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c47adbc92fc1bb2b3274c4b3a43ae0e4573d9fbff4f54cd484555edbf030baf1"},
-    {file = "MarkupSafe-2.0.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:37205cac2a79194e3750b0af2a5720d95f786a55ce7df90c3af697bfa100eaac"},
-    {file = "MarkupSafe-2.0.1-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:1f2ade76b9903f39aa442b4aadd2177decb66525062db244b35d71d0ee8599b6"},
-    {file = "MarkupSafe-2.0.1-cp39-cp39-win32.whl", hash = "sha256:10f82115e21dc0dfec9ab5c0223652f7197feb168c940f3ef61563fc2d6beb74"},
-    {file = "MarkupSafe-2.0.1-cp39-cp39-win_amd64.whl", hash = "sha256:693ce3f9e70a6cf7d2fb9e6c9d8b204b6b39897a2c4a1aa65728d5ac97dcc1d8"},
-    {file = "MarkupSafe-2.0.1.tar.gz", hash = "sha256:594c67807fb16238b30c44bdf74f36c02cdf22d1c8cda91ef8a0ed8dabf5620a"},
-]
-matplotlib-inline = [
-    {file = "matplotlib-inline-0.1.3.tar.gz", hash = "sha256:a04bfba22e0d1395479f866853ec1ee28eea1485c1d69a6faf00dc3e24ff34ee"},
-    {file = "matplotlib_inline-0.1.3-py3-none-any.whl", hash = "sha256:aed605ba3b72462d64d475a21a9296f400a19c4f74a31b59103d2a99ffd5aa5c"},
-]
-mccabe = [
-    {file = "mccabe-0.6.1-py2.py3-none-any.whl", hash = "sha256:ab8a6258860da4b6677da4bd2fe5dc2c659cff31b3ee4f7f5d64e79735b80d42"},
-    {file = "mccabe-0.6.1.tar.gz", hash = "sha256:dd8d182285a0fe56bace7f45b5e7d1a6ebcbf524e8f3bd87eb0f125271b8831f"},
-]
-mypy = [
-    {file = "mypy-0.910-cp35-cp35m-macosx_10_9_x86_64.whl", hash = "sha256:a155d80ea6cee511a3694b108c4494a39f42de11ee4e61e72bc424c490e46457"},
-    {file = "mypy-0.910-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:b94e4b785e304a04ea0828759172a15add27088520dc7e49ceade7834275bedb"},
-    {file = "mypy-0.910-cp35-cp35m-manylinux2010_x86_64.whl", hash = "sha256:088cd9c7904b4ad80bec811053272986611b84221835e079be5bcad029e79dd9"},
-    {file = "mypy-0.910-cp35-cp35m-win_amd64.whl", hash = "sha256:adaeee09bfde366d2c13fe6093a7df5df83c9a2ba98638c7d76b010694db760e"},
-    {file = "mypy-0.910-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:ecd2c3fe726758037234c93df7e98deb257fd15c24c9180dacf1ef829da5f921"},
-    {file = "mypy-0.910-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:d9dd839eb0dc1bbe866a288ba3c1afc33a202015d2ad83b31e875b5905a079b6"},
-    {file = "mypy-0.910-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:3e382b29f8e0ccf19a2df2b29a167591245df90c0b5a2542249873b5c1d78212"},
-    {file = "mypy-0.910-cp36-cp36m-win_amd64.whl", hash = "sha256:53fd2eb27a8ee2892614370896956af2ff61254c275aaee4c230ae771cadd885"},
-    {file = "mypy-0.910-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:b6fb13123aeef4a3abbcfd7e71773ff3ff1526a7d3dc538f3929a49b42be03f0"},
-    {file = "mypy-0.910-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:e4dab234478e3bd3ce83bac4193b2ecd9cf94e720ddd95ce69840273bf44f6de"},
-    {file = "mypy-0.910-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:7df1ead20c81371ccd6091fa3e2878559b5c4d4caadaf1a484cf88d93ca06703"},
-    {file = "mypy-0.910-cp37-cp37m-win_amd64.whl", hash = "sha256:0aadfb2d3935988ec3815952e44058a3100499f5be5b28c34ac9d79f002a4a9a"},
-    {file = "mypy-0.910-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:ec4e0cd079db280b6bdabdc807047ff3e199f334050db5cbb91ba3e959a67504"},
-    {file = "mypy-0.910-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:119bed3832d961f3a880787bf621634ba042cb8dc850a7429f643508eeac97b9"},
-    {file = "mypy-0.910-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:866c41f28cee548475f146aa4d39a51cf3b6a84246969f3759cb3e9c742fc072"},
-    {file = "mypy-0.910-cp38-cp38-win_amd64.whl", hash = "sha256:ceb6e0a6e27fb364fb3853389607cf7eb3a126ad335790fa1e14ed02fba50811"},
-    {file = "mypy-0.910-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:1a85e280d4d217150ce8cb1a6dddffd14e753a4e0c3cf90baabb32cefa41b59e"},
-    {file = "mypy-0.910-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:42c266ced41b65ed40a282c575705325fa7991af370036d3f134518336636f5b"},
-    {file = "mypy-0.910-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:3c4b8ca36877fc75339253721f69603a9c7fdb5d4d5a95a1a1b899d8b86a4de2"},
-    {file = "mypy-0.910-cp39-cp39-manylinux2010_x86_64.whl", hash = "sha256:c0df2d30ed496a08de5daed2a9ea807d07c21ae0ab23acf541ab88c24b26ab97"},
-    {file = "mypy-0.910-cp39-cp39-win_amd64.whl", hash = "sha256:c6c2602dffb74867498f86e6129fd52a2770c48b7cd3ece77ada4fa38f94eba8"},
-    {file = "mypy-0.910-py3-none-any.whl", hash = "sha256:ef565033fa5a958e62796867b1df10c40263ea9ded87164d67572834e57a174d"},
-    {file = "mypy-0.910.tar.gz", hash = "sha256:704098302473cb31a218f1775a873b376b30b4c18229421e9e9dc8916fd16150"},
-]
-mypy-extensions = [
-    {file = "mypy_extensions-0.4.3-py2.py3-none-any.whl", hash = "sha256:090fedd75945a69ae91ce1303b5824f428daf5a028d2f6ab8a299250a846f15d"},
-    {file = "mypy_extensions-0.4.3.tar.gz", hash = "sha256:2d82818f5bb3e369420cb3c4060a7970edba416647068eb4c5343488a6c604a8"},
-]
-oauthlib = [
-    {file = "oauthlib-3.1.1-py2.py3-none-any.whl", hash = "sha256:42bf6354c2ed8c6acb54d971fce6f88193d97297e18602a3a886603f9d7730cc"},
-    {file = "oauthlib-3.1.1.tar.gz", hash = "sha256:8f0215fcc533dd8dd1bee6f4c412d4f0cd7297307d43ac61666389e3bc3198a3"},
-]
-packaging = [
-    {file = "packaging-21.0-py3-none-any.whl", hash = "sha256:c86254f9220d55e31cc94d69bade760f0847da8000def4dfe1c6b872fd14ff14"},
-    {file = "packaging-21.0.tar.gz", hash = "sha256:7dc96269f53a4ccec5c0670940a4281106dd0bb343f47b7471f779df49c2fbe7"},
-]
-parso = [
-    {file = "parso-0.8.2-py2.py3-none-any.whl", hash = "sha256:a8c4922db71e4fdb90e0d0bc6e50f9b273d3397925e5e60a717e719201778d22"},
-    {file = "parso-0.8.2.tar.gz", hash = "sha256:12b83492c6239ce32ff5eed6d3639d6a536170723c6f3f1506869f1ace413398"},
-]
-parsy = [
-    {file = "parsy-1.1.0-py3-none-any.whl", hash = "sha256:25bd5cea2954950ebbfdf71f8bdaf7fd45a5df5325fd36a1064be2204d9d4c94"},
-    {file = "parsy-1.1.0.tar.gz", hash = "sha256:36173ba01a5372c7a1b32352cc73a279a49198f52252adf1c8c1ed41d1f94e8d"},
-]
-pathspec = [
-    {file = "pathspec-0.9.0-py2.py3-none-any.whl", hash = "sha256:7d15c4ddb0b5c802d161efc417ec1a2558ea2653c2e8ad9c19098201dc1c993a"},
-    {file = "pathspec-0.9.0.tar.gz", hash = "sha256:e564499435a2673d586f6b2130bb5b95f04a3ba06f81b8f895b651a3c76aabb1"},
-]
-pbr = [
-    {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"},
-]
-pexpect = [
-    {file = "pexpect-4.8.0-py2.py3-none-any.whl", hash = "sha256:0b48a55dcb3c05f3329815901ea4fc1537514d6ba867a152b581d69ae3710937"},
-    {file = "pexpect-4.8.0.tar.gz", hash = "sha256:fc65a43959d153d0114afe13997d439c22823a27cefceb5ff35c2178c6784c0c"},
-]
-pg8000 = [
-    {file = "pg8000-1.22.0-py3-none-any.whl", hash = "sha256:a0e82542f4a56b2139c41ff09c1aeff294c10b6500bb6c57890c0c1c551cbc03"},
-    {file = "pg8000-1.22.0.tar.gz", hash = "sha256:c5172252fc92142ec104cd5e7231be4580a1a0a814403707bafbf7bb8383a29a"},
-]
-phonenumbers = [
-    {file = "phonenumbers-8.12.35-py2.py3-none-any.whl", hash = "sha256:16c8f6d682ab5be550af2f4a2f81f0a90c8743e37babc7465edcefc106d0a1eb"},
-    {file = "phonenumbers-8.12.35.tar.gz", hash = "sha256:f426d419aabf6366c27ef1193918cc55217ef0e8be8f09cbf0667131037ca229"},
-]
-pickleshare = [
-    {file = "pickleshare-0.7.5-py2.py3-none-any.whl", hash = "sha256:9649af414d74d4df115d5d718f82acb59c9d418196b7b4290ed47a12ce62df56"},
-    {file = "pickleshare-0.7.5.tar.gz", hash = "sha256:87683d47965c1da65cdacaf31c8441d12b8044cdec9aca500cd78fc2c683afca"},
-]
-pillow = [
-    {file = "Pillow-8.4.0-cp310-cp310-macosx_10_10_universal2.whl", hash = "sha256:81f8d5c81e483a9442d72d182e1fb6dcb9723f289a57e8030811bac9ea3fef8d"},
-    {file = "Pillow-8.4.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:3f97cfb1e5a392d75dd8b9fd274d205404729923840ca94ca45a0af57e13dbe6"},
-    {file = "Pillow-8.4.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:eb9fc393f3c61f9054e1ed26e6fe912c7321af2f41ff49d3f83d05bacf22cc78"},
-    {file = "Pillow-8.4.0-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d82cdb63100ef5eedb8391732375e6d05993b765f72cb34311fab92103314649"},
-    {file = "Pillow-8.4.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:62cc1afda735a8d109007164714e73771b499768b9bb5afcbbee9d0ff374b43f"},
-    {file = "Pillow-8.4.0-cp310-cp310-win32.whl", hash = "sha256:e3dacecfbeec9a33e932f00c6cd7996e62f53ad46fbe677577394aaa90ee419a"},
-    {file = "Pillow-8.4.0-cp310-cp310-win_amd64.whl", hash = "sha256:620582db2a85b2df5f8a82ddeb52116560d7e5e6b055095f04ad828d1b0baa39"},
-    {file = "Pillow-8.4.0-cp36-cp36m-macosx_10_10_x86_64.whl", hash = "sha256:1bc723b434fbc4ab50bb68e11e93ce5fb69866ad621e3c2c9bdb0cd70e345f55"},
-    {file = "Pillow-8.4.0-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:72cbcfd54df6caf85cc35264c77ede902452d6df41166010262374155947460c"},
-    {file = "Pillow-8.4.0-cp36-cp36m-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:70ad9e5c6cb9b8487280a02c0ad8a51581dcbbe8484ce058477692a27c151c0a"},
-    {file = "Pillow-8.4.0-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:25a49dc2e2f74e65efaa32b153527fc5ac98508d502fa46e74fa4fd678ed6645"},
-    {file = "Pillow-8.4.0-cp36-cp36m-win32.whl", hash = "sha256:93ce9e955cc95959df98505e4608ad98281fff037350d8c2671c9aa86bcf10a9"},
-    {file = "Pillow-8.4.0-cp36-cp36m-win_amd64.whl", hash = "sha256:2e4440b8f00f504ee4b53fe30f4e381aae30b0568193be305256b1462216feff"},
-    {file = "Pillow-8.4.0-cp37-cp37m-macosx_10_10_x86_64.whl", hash = "sha256:8c803ac3c28bbc53763e6825746f05cc407b20e4a69d0122e526a582e3b5e153"},
-    {file = "Pillow-8.4.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c8a17b5d948f4ceeceb66384727dde11b240736fddeda54ca740b9b8b1556b29"},
-    {file = "Pillow-8.4.0-cp37-cp37m-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1394a6ad5abc838c5cd8a92c5a07535648cdf6d09e8e2d6df916dfa9ea86ead8"},
-    {file = "Pillow-8.4.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:792e5c12376594bfcb986ebf3855aa4b7c225754e9a9521298e460e92fb4a488"},
-    {file = "Pillow-8.4.0-cp37-cp37m-win32.whl", hash = "sha256:d99ec152570e4196772e7a8e4ba5320d2d27bf22fdf11743dd882936ed64305b"},
-    {file = "Pillow-8.4.0-cp37-cp37m-win_amd64.whl", hash = "sha256:7b7017b61bbcdd7f6363aeceb881e23c46583739cb69a3ab39cb384f6ec82e5b"},
-    {file = "Pillow-8.4.0-cp38-cp38-macosx_10_10_x86_64.whl", hash = "sha256:d89363f02658e253dbd171f7c3716a5d340a24ee82d38aab9183f7fdf0cdca49"},
-    {file = "Pillow-8.4.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:0a0956fdc5defc34462bb1c765ee88d933239f9a94bc37d132004775241a7585"},
-    {file = "Pillow-8.4.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5b7bb9de00197fb4261825c15551adf7605cf14a80badf1761d61e59da347779"},
-    {file = "Pillow-8.4.0-cp38-cp38-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:72b9e656e340447f827885b8d7a15fc8c4e68d410dc2297ef6787eec0f0ea409"},
-    {file = "Pillow-8.4.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a5a4532a12314149d8b4e4ad8ff09dde7427731fcfa5917ff16d0291f13609df"},
-    {file = "Pillow-8.4.0-cp38-cp38-win32.whl", hash = "sha256:82aafa8d5eb68c8463b6e9baeb4f19043bb31fefc03eb7b216b51e6a9981ae09"},
-    {file = "Pillow-8.4.0-cp38-cp38-win_amd64.whl", hash = "sha256:066f3999cb3b070a95c3652712cffa1a748cd02d60ad7b4e485c3748a04d9d76"},
-    {file = "Pillow-8.4.0-cp39-cp39-macosx_10_10_x86_64.whl", hash = "sha256:5503c86916d27c2e101b7f71c2ae2cddba01a2cf55b8395b0255fd33fa4d1f1a"},
-    {file = "Pillow-8.4.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:4acc0985ddf39d1bc969a9220b51d94ed51695d455c228d8ac29fcdb25810e6e"},
-    {file = "Pillow-8.4.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0b052a619a8bfcf26bd8b3f48f45283f9e977890263e4571f2393ed8898d331b"},
-    {file = "Pillow-8.4.0-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:493cb4e415f44cd601fcec11c99836f707bb714ab03f5ed46ac25713baf0ff20"},
-    {file = "Pillow-8.4.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b8831cb7332eda5dc89b21a7bce7ef6ad305548820595033a4b03cf3091235ed"},
-    {file = "Pillow-8.4.0-cp39-cp39-win32.whl", hash = "sha256:5e9ac5f66616b87d4da618a20ab0a38324dbe88d8a39b55be8964eb520021e02"},
-    {file = "Pillow-8.4.0-cp39-cp39-win_amd64.whl", hash = "sha256:3eb1ce5f65908556c2d8685a8f0a6e989d887ec4057326f6c22b24e8a172c66b"},
-    {file = "Pillow-8.4.0-pp36-pypy36_pp73-macosx_10_10_x86_64.whl", hash = "sha256:ddc4d832a0f0b4c52fff973a0d44b6c99839a9d016fe4e6a1cb8f3eea96479c2"},
-    {file = "Pillow-8.4.0-pp36-pypy36_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9a3e5ddc44c14042f0844b8cf7d2cd455f6cc80fd7f5eefbe657292cf601d9ad"},
-    {file = "Pillow-8.4.0-pp36-pypy36_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c70e94281588ef053ae8998039610dbd71bc509e4acbc77ab59d7d2937b10698"},
-    {file = "Pillow-8.4.0-pp37-pypy37_pp73-macosx_10_10_x86_64.whl", hash = "sha256:3862b7256046fcd950618ed22d1d60b842e3a40a48236a5498746f21189afbbc"},
-    {file = "Pillow-8.4.0-pp37-pypy37_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a4901622493f88b1a29bd30ec1a2f683782e57c3c16a2dbc7f2595ba01f639df"},
-    {file = "Pillow-8.4.0-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:84c471a734240653a0ec91dec0996696eea227eafe72a33bd06c92697728046b"},
-    {file = "Pillow-8.4.0-pp37-pypy37_pp73-win_amd64.whl", hash = "sha256:244cf3b97802c34c41905d22810846802a3329ddcb93ccc432870243211c79fc"},
-    {file = "Pillow-8.4.0.tar.gz", hash = "sha256:b8e2f83c56e141920c39464b852de3719dfbfb6e3c99a2d8da0edf4fb33176ed"},
-]
-pluggy = [
-    {file = "pluggy-1.0.0-py2.py3-none-any.whl", hash = "sha256:74134bbf457f031a36d68416e1509f34bd5ccc019f0bcc952c7b909d06b37bd3"},
-    {file = "pluggy-1.0.0.tar.gz", hash = "sha256:4224373bacce55f955a878bf9cfa763c1e360858e330072059e10bad68531159"},
-]
-prometheus-client = [
-    {file = "prometheus_client-0.11.0-py2.py3-none-any.whl", hash = "sha256:b014bc76815eb1399da8ce5fc84b7717a3e63652b0c0f8804092c9363acab1b2"},
-    {file = "prometheus_client-0.11.0.tar.gz", hash = "sha256:3a8baade6cb80bcfe43297e33e7623f3118d660d41387593758e2fb1ea173a86"},
-]
-prompt-toolkit = [
-    {file = "prompt_toolkit-3.0.21-py3-none-any.whl", hash = "sha256:62b3d3ea5a3ccee94dc1aac018279cf64866a76837156ebe159b981c42dd20a8"},
-    {file = "prompt_toolkit-3.0.21.tar.gz", hash = "sha256:27f13ff4e4850fe8f860b77414c7880f67c6158076a7b099062cc8570f1562e5"},
-]
-psutil = [
-    {file = "psutil-5.8.0-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:0066a82f7b1b37d334e68697faba68e5ad5e858279fd6351c8ca6024e8d6ba64"},
-    {file = "psutil-5.8.0-cp27-cp27m-manylinux2010_i686.whl", hash = "sha256:0ae6f386d8d297177fd288be6e8d1afc05966878704dad9847719650e44fc49c"},
-    {file = "psutil-5.8.0-cp27-cp27m-manylinux2010_x86_64.whl", hash = "sha256:12d844996d6c2b1d3881cfa6fa201fd635971869a9da945cf6756105af73d2df"},
-    {file = "psutil-5.8.0-cp27-cp27mu-manylinux2010_i686.whl", hash = "sha256:02b8292609b1f7fcb34173b25e48d0da8667bc85f81d7476584d889c6e0f2131"},
-    {file = "psutil-5.8.0-cp27-cp27mu-manylinux2010_x86_64.whl", hash = "sha256:6ffe81843131ee0ffa02c317186ed1e759a145267d54fdef1bc4ea5f5931ab60"},
-    {file = "psutil-5.8.0-cp27-none-win32.whl", hash = "sha256:ea313bb02e5e25224e518e4352af4bf5e062755160f77e4b1767dd5ccb65f876"},
-    {file = "psutil-5.8.0-cp27-none-win_amd64.whl", hash = "sha256:5da29e394bdedd9144c7331192e20c1f79283fb03b06e6abd3a8ae45ffecee65"},
-    {file = "psutil-5.8.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:74fb2557d1430fff18ff0d72613c5ca30c45cdbfcddd6a5773e9fc1fe9364be8"},
-    {file = "psutil-5.8.0-cp36-cp36m-manylinux2010_i686.whl", hash = "sha256:74f2d0be88db96ada78756cb3a3e1b107ce8ab79f65aa885f76d7664e56928f6"},
-    {file = "psutil-5.8.0-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:99de3e8739258b3c3e8669cb9757c9a861b2a25ad0955f8e53ac662d66de61ac"},
-    {file = "psutil-5.8.0-cp36-cp36m-win32.whl", hash = "sha256:36b3b6c9e2a34b7d7fbae330a85bf72c30b1c827a4366a07443fc4b6270449e2"},
-    {file = "psutil-5.8.0-cp36-cp36m-win_amd64.whl", hash = "sha256:52de075468cd394ac98c66f9ca33b2f54ae1d9bff1ef6b67a212ee8f639ec06d"},
-    {file = "psutil-5.8.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:c6a5fd10ce6b6344e616cf01cc5b849fa8103fbb5ba507b6b2dee4c11e84c935"},
-    {file = "psutil-5.8.0-cp37-cp37m-manylinux2010_i686.whl", hash = "sha256:61f05864b42fedc0771d6d8e49c35f07efd209ade09a5afe6a5059e7bb7bf83d"},
-    {file = "psutil-5.8.0-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:0dd4465a039d343925cdc29023bb6960ccf4e74a65ad53e768403746a9207023"},
-    {file = "psutil-5.8.0-cp37-cp37m-win32.whl", hash = "sha256:1bff0d07e76114ec24ee32e7f7f8d0c4b0514b3fae93e3d2aaafd65d22502394"},
-    {file = "psutil-5.8.0-cp37-cp37m-win_amd64.whl", hash = "sha256:fcc01e900c1d7bee2a37e5d6e4f9194760a93597c97fee89c4ae51701de03563"},
-    {file = "psutil-5.8.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:6223d07a1ae93f86451d0198a0c361032c4c93ebd4bf6d25e2fb3edfad9571ef"},
-    {file = "psutil-5.8.0-cp38-cp38-manylinux2010_i686.whl", hash = "sha256:d225cd8319aa1d3c85bf195c4e07d17d3cd68636b8fc97e6cf198f782f99af28"},
-    {file = "psutil-5.8.0-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:28ff7c95293ae74bf1ca1a79e8805fcde005c18a122ca983abf676ea3466362b"},
-    {file = "psutil-5.8.0-cp38-cp38-win32.whl", hash = "sha256:ce8b867423291cb65cfc6d9c4955ee9bfc1e21fe03bb50e177f2b957f1c2469d"},
-    {file = "psutil-5.8.0-cp38-cp38-win_amd64.whl", hash = "sha256:90f31c34d25b1b3ed6c40cdd34ff122b1887a825297c017e4cbd6796dd8b672d"},
-    {file = "psutil-5.8.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:6323d5d845c2785efb20aded4726636546b26d3b577aded22492908f7c1bdda7"},
-    {file = "psutil-5.8.0-cp39-cp39-manylinux2010_i686.whl", hash = "sha256:245b5509968ac0bd179287d91210cd3f37add77dad385ef238b275bad35fa1c4"},
-    {file = "psutil-5.8.0-cp39-cp39-manylinux2010_x86_64.whl", hash = "sha256:90d4091c2d30ddd0a03e0b97e6a33a48628469b99585e2ad6bf21f17423b112b"},
-    {file = "psutil-5.8.0-cp39-cp39-win32.whl", hash = "sha256:ea372bcc129394485824ae3e3ddabe67dc0b118d262c568b4d2602a7070afdb0"},
-    {file = "psutil-5.8.0-cp39-cp39-win_amd64.whl", hash = "sha256:f4634b033faf0d968bb9220dd1c793b897ab7f1189956e1aa9eae752527127d3"},
-    {file = "psutil-5.8.0.tar.gz", hash = "sha256:0c9ccb99ab76025f2f0bbecf341d4656e9c1351db8cc8a03ccd62e318ab4b5c6"},
-]
-psycopg2 = [
-    {file = "psycopg2-2.9.1-cp36-cp36m-win32.whl", hash = "sha256:7f91312f065df517187134cce8e395ab37f5b601a42446bdc0f0d51773621854"},
-    {file = "psycopg2-2.9.1-cp36-cp36m-win_amd64.whl", hash = "sha256:830c8e8dddab6b6716a4bf73a09910c7954a92f40cf1d1e702fb93c8a919cc56"},
-    {file = "psycopg2-2.9.1-cp37-cp37m-win32.whl", hash = "sha256:89409d369f4882c47f7ea20c42c5046879ce22c1e4ea20ef3b00a4dfc0a7f188"},
-    {file = "psycopg2-2.9.1-cp37-cp37m-win_amd64.whl", hash = "sha256:7640e1e4d72444ef012e275e7b53204d7fab341fb22bc76057ede22fe6860b25"},
-    {file = "psycopg2-2.9.1-cp38-cp38-win32.whl", hash = "sha256:079d97fc22de90da1d370c90583659a9f9a6ee4007355f5825e5f1c70dffc1fa"},
-    {file = "psycopg2-2.9.1-cp38-cp38-win_amd64.whl", hash = "sha256:2c992196719fadda59f72d44603ee1a2fdcc67de097eea38d41c7ad9ad246e62"},
-    {file = "psycopg2-2.9.1-cp39-cp39-win32.whl", hash = "sha256:2087013c159a73e09713294a44d0c8008204d06326006b7f652bef5ace66eebb"},
-    {file = "psycopg2-2.9.1-cp39-cp39-win_amd64.whl", hash = "sha256:bf35a25f1aaa8a3781195595577fcbb59934856ee46b4f252f56ad12b8043bcf"},
-    {file = "psycopg2-2.9.1.tar.gz", hash = "sha256:de5303a6f1d0a7a34b9d40e4d3bef684ccc44a49bbe3eb85e3c0bffb4a131b7c"},
-]
-ptyprocess = [
-    {file = "ptyprocess-0.7.0-py2.py3-none-any.whl", hash = "sha256:4b41f3967fce3af57cc7e94b888626c18bf37a083e3651ca8feeb66d492fef35"},
-    {file = "ptyprocess-0.7.0.tar.gz", hash = "sha256:5c5d0a3b48ceee0b48485e0c26037c0acd7d29765ca3fbb5cb3831d347423220"},
-]
-py = [
-    {file = "py-1.10.0-py2.py3-none-any.whl", hash = "sha256:3b80836aa6d1feeaa108e046da6423ab8f6ceda6468545ae8d02d9d58d18818a"},
-    {file = "py-1.10.0.tar.gz", hash = "sha256:21b81bda15b66ef5e1a777a21c4dcd9c20ad3efd0b3f817e7a809035269e1bd3"},
-]
-pyasn1 = [
-    {file = "pyasn1-0.4.8-py2.4.egg", hash = "sha256:fec3e9d8e36808a28efb59b489e4528c10ad0f480e57dcc32b4de5c9d8c9fdf3"},
-    {file = "pyasn1-0.4.8-py2.5.egg", hash = "sha256:0458773cfe65b153891ac249bcf1b5f8f320b7c2ce462151f8fa74de8934becf"},
-    {file = "pyasn1-0.4.8-py2.6.egg", hash = "sha256:5c9414dcfede6e441f7e8f81b43b34e834731003427e5b09e4e00e3172a10f00"},
-    {file = "pyasn1-0.4.8-py2.7.egg", hash = "sha256:6e7545f1a61025a4e58bb336952c5061697da694db1cae97b116e9c46abcf7c8"},
-    {file = "pyasn1-0.4.8-py2.py3-none-any.whl", hash = "sha256:39c7e2ec30515947ff4e87fb6f456dfc6e84857d34be479c9d4a4ba4bf46aa5d"},
-    {file = "pyasn1-0.4.8-py3.1.egg", hash = "sha256:78fa6da68ed2727915c4767bb386ab32cdba863caa7dbe473eaae45f9959da86"},
-    {file = "pyasn1-0.4.8-py3.2.egg", hash = "sha256:08c3c53b75eaa48d71cf8c710312316392ed40899cb34710d092e96745a358b7"},
-    {file = "pyasn1-0.4.8-py3.3.egg", hash = "sha256:03840c999ba71680a131cfaee6fab142e1ed9bbd9c693e285cc6aca0d555e576"},
-    {file = "pyasn1-0.4.8-py3.4.egg", hash = "sha256:7ab8a544af125fb704feadb008c99a88805126fb525280b2270bb25cc1d78a12"},
-    {file = "pyasn1-0.4.8-py3.5.egg", hash = "sha256:e89bf84b5437b532b0803ba5c9a5e054d21fec423a89952a74f87fa2c9b7bce2"},
-    {file = "pyasn1-0.4.8-py3.6.egg", hash = "sha256:014c0e9976956a08139dc0712ae195324a75e142284d5f87f1a87ee1b068a359"},
-    {file = "pyasn1-0.4.8-py3.7.egg", hash = "sha256:99fcc3c8d804d1bc6d9a099921e39d827026409a58f2a720dcdb89374ea0c776"},
-    {file = "pyasn1-0.4.8.tar.gz", hash = "sha256:aef77c9fb94a3ac588e87841208bdec464471d9871bd5050a287cc9a475cd0ba"},
-]
-pyasn1-modules = [
-    {file = "pyasn1-modules-0.2.8.tar.gz", hash = "sha256:905f84c712230b2c592c19470d3ca8d552de726050d1d1716282a1f6146be65e"},
-    {file = "pyasn1_modules-0.2.8-py2.4.egg", hash = "sha256:0fe1b68d1e486a1ed5473f1302bd991c1611d319bba158e98b106ff86e1d7199"},
-    {file = "pyasn1_modules-0.2.8-py2.5.egg", hash = "sha256:fe0644d9ab041506b62782e92b06b8c68cca799e1a9636ec398675459e031405"},
-    {file = "pyasn1_modules-0.2.8-py2.6.egg", hash = "sha256:a99324196732f53093a84c4369c996713eb8c89d360a496b599fb1a9c47fc3eb"},
-    {file = "pyasn1_modules-0.2.8-py2.7.egg", hash = "sha256:0845a5582f6a02bb3e1bde9ecfc4bfcae6ec3210dd270522fee602365430c3f8"},
-    {file = "pyasn1_modules-0.2.8-py2.py3-none-any.whl", hash = "sha256:a50b808ffeb97cb3601dd25981f6b016cbb3d31fbf57a8b8a87428e6158d0c74"},
-    {file = "pyasn1_modules-0.2.8-py3.1.egg", hash = "sha256:f39edd8c4ecaa4556e989147ebf219227e2cd2e8a43c7e7fcb1f1c18c5fd6a3d"},
-    {file = "pyasn1_modules-0.2.8-py3.2.egg", hash = "sha256:b80486a6c77252ea3a3e9b1e360bc9cf28eaac41263d173c032581ad2f20fe45"},
-    {file = "pyasn1_modules-0.2.8-py3.3.egg", hash = "sha256:65cebbaffc913f4fe9e4808735c95ea22d7a7775646ab690518c056784bc21b4"},
-    {file = "pyasn1_modules-0.2.8-py3.4.egg", hash = "sha256:15b7c67fabc7fc240d87fb9aabf999cf82311a6d6fb2c70d00d3d0604878c811"},
-    {file = "pyasn1_modules-0.2.8-py3.5.egg", hash = "sha256:426edb7a5e8879f1ec54a1864f16b882c2837bfd06eee62f2c982315ee2473ed"},
-    {file = "pyasn1_modules-0.2.8-py3.6.egg", hash = "sha256:cbac4bc38d117f2a49aeedec4407d23e8866ea4ac27ff2cf7fb3e5b570df19e0"},
-    {file = "pyasn1_modules-0.2.8-py3.7.egg", hash = "sha256:c29a5e5cc7a3f05926aff34e097e84f8589cd790ce0ed41b67aed6857b26aafd"},
-]
-pycodestyle = [
-    {file = "pycodestyle-2.7.0-py2.py3-none-any.whl", hash = "sha256:514f76d918fcc0b55c6680472f0a37970994e07bbb80725808c17089be302068"},
-    {file = "pycodestyle-2.7.0.tar.gz", hash = "sha256:c389c1d06bf7904078ca03399a4816f974a1d590090fecea0c63ec26ebaf1cef"},
-]
-pycparser = [
-    {file = "pycparser-2.20-py2.py3-none-any.whl", hash = "sha256:7582ad22678f0fcd81102833f60ef8d0e57288b6b5fb00323d101be910e35705"},
-    {file = "pycparser-2.20.tar.gz", hash = "sha256:2d475327684562c3a96cc71adf7dc8c4f0565175cf86b6d7a404ff4c771f15f0"},
-]
-pycryptodome = [
-    {file = "pycryptodome-3.11.0-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:ffd0cac13ff41f2d15ed39dc6ba1d2ad88dd2905d656c33d8235852f5d6151fd"},
-    {file = "pycryptodome-3.11.0-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:ead516e03dfe062aefeafe4a29445a6449b0fc43bc8cb30194b2754917a63798"},
-    {file = "pycryptodome-3.11.0-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:4ce6b09547bf2c7cede3a017f79502eaed3e819c13cdb3cb357aea1b004e4cc6"},
-    {file = "pycryptodome-3.11.0-cp27-cp27m-manylinux2010_i686.whl", hash = "sha256:014c758af7fa38cab85b357a496b76f4fc9dda1f731eb28358d66fef7ad4a3e1"},
-    {file = "pycryptodome-3.11.0-cp27-cp27m-manylinux2010_x86_64.whl", hash = "sha256:a843350d08c3d22f6c09c2f17f020d8dcfa59496165d7425a3fba0045543dda7"},
-    {file = "pycryptodome-3.11.0-cp27-cp27m-manylinux2014_aarch64.whl", hash = "sha256:53989477044be41fa4a63da09d5038c2a34b2f4554cfea2e3933b17186ee9e19"},
-    {file = "pycryptodome-3.11.0-cp27-cp27m-win32.whl", hash = "sha256:f9bad2220b80b4ed74f089db012ab5ab5419143a33fad6c8aedcc2a9341eac70"},
-    {file = "pycryptodome-3.11.0-cp27-cp27m-win_amd64.whl", hash = "sha256:3c7ed5b07274535979c730daf5817db5e983ea80b04c22579eee8da4ca3ae4f8"},
-    {file = "pycryptodome-3.11.0-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:8f3a60926be78422e662b0d0b18351b426ce27657101c8a50bad80300de6a701"},
-    {file = "pycryptodome-3.11.0-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:fce7e22d96030b35345637c563246c24d4513bd3b413e1c40293114837ab8912"},
-    {file = "pycryptodome-3.11.0-cp27-cp27mu-manylinux2010_i686.whl", hash = "sha256:bc3c61ff92efdcc14af4a7b81da71d849c9acee51d8fd8ac9841a7620140d6c6"},
-    {file = "pycryptodome-3.11.0-cp27-cp27mu-manylinux2010_x86_64.whl", hash = "sha256:b33c9b3d1327d821e28e9cc3a6512c14f8b17570ddb4cfb9a52247ed0fcc5d8b"},
-    {file = "pycryptodome-3.11.0-cp27-cp27mu-manylinux2014_aarch64.whl", hash = "sha256:75e78360d1dd6d02eb288fd8275bb4d147d6e3f5337935c096d11dba1fa84748"},
-    {file = "pycryptodome-3.11.0-cp35-abi3-macosx_10_9_x86_64.whl", hash = "sha256:621a90147a5e255fdc2a0fec2d56626b76b5d72ea9e60164c9a5a8976d45b0c9"},
-    {file = "pycryptodome-3.11.0-cp35-abi3-manylinux1_i686.whl", hash = "sha256:0ca7a6b4fc1f9fafe990b95c8cda89099797e2cfbf40e55607f2f2f5a3355dcb"},
-    {file = "pycryptodome-3.11.0-cp35-abi3-manylinux1_x86_64.whl", hash = "sha256:b59bf823cfafde8ef1105d8984f26d1694dff165adb7198b12e3e068d7999b15"},
-    {file = "pycryptodome-3.11.0-cp35-abi3-manylinux2010_i686.whl", hash = "sha256:ce81b9c6aaa0f920e2ab05eb2b9f4ccd102e3016b2f37125593b16a83a4b0cc2"},
-    {file = "pycryptodome-3.11.0-cp35-abi3-manylinux2010_x86_64.whl", hash = "sha256:ae29fcd56152f417bfba50a36a56a7a5f9fb74ff80bab98704cac704de6568ab"},
-    {file = "pycryptodome-3.11.0-cp35-abi3-manylinux2014_aarch64.whl", hash = "sha256:ae31cb874f6f0cedbed457c6374e7e54d7ed45c1a4e11a65a9c80968da90a650"},
-    {file = "pycryptodome-3.11.0-cp35-abi3-win32.whl", hash = "sha256:6db1f9fa1f52226621905f004278ce7bd90c8f5363ffd5d7ab3755363d98549a"},
-    {file = "pycryptodome-3.11.0-cp35-abi3-win_amd64.whl", hash = "sha256:d7e5f6f692421e5219aa3b545eb0cffd832cd589a4b9dcd4a5eb4260e2c0d68a"},
-    {file = "pycryptodome-3.11.0-pp27-pypy_73-macosx_10_9_x86_64.whl", hash = "sha256:da796e9221dda61a0019d01742337eb8a322de8598b678a4344ca0a436380315"},
-    {file = "pycryptodome-3.11.0-pp27-pypy_73-manylinux1_x86_64.whl", hash = "sha256:ed45ef92d21db33685b789de2c015e9d9a18a74760a8df1fc152faee88cdf741"},
-    {file = "pycryptodome-3.11.0-pp27-pypy_73-manylinux2010_x86_64.whl", hash = "sha256:4169ed515742425ff21e4bd3fabbb6994ffb64434472fb72230019bdfa36b939"},
-    {file = "pycryptodome-3.11.0-pp27-pypy_73-win32.whl", hash = "sha256:f19edd42368e9057c39492947bb99570dc927123e210008f2af7cf9b505c6892"},
-    {file = "pycryptodome-3.11.0-pp36-pypy36_pp73-macosx_10_9_x86_64.whl", hash = "sha256:06162fcfed2f9deee8383fd59eaeabc7b7ffc3af50d3fad4000032deb8f700b0"},
-    {file = "pycryptodome-3.11.0-pp36-pypy36_pp73-manylinux1_x86_64.whl", hash = "sha256:6eda8a3157c91ba60b26a07bedd6c44ab8bda6cd79b6b5ea9744ba62c39b7b1e"},
-    {file = "pycryptodome-3.11.0-pp36-pypy36_pp73-manylinux2010_x86_64.whl", hash = "sha256:7ff701fc283412e651eaab4319b3cd4eaa0827e94569cd37ee9075d5c05fe655"},
-    {file = "pycryptodome-3.11.0-pp36-pypy36_pp73-win32.whl", hash = "sha256:2a4bcc8a9977fee0979079cd33a9e9f0d3ddba5660d35ffe874cf84f1dd399d2"},
-    {file = "pycryptodome-3.11.0.tar.gz", hash = "sha256:428096bbf7a77e207f418dfd4d7c284df8ade81d2dc80f010e92753a3e406ad0"},
-]
-pydocstyle = [
-    {file = "pydocstyle-6.1.1-py3-none-any.whl", hash = "sha256:6987826d6775056839940041beef5c08cc7e3d71d63149b48e36727f70144dc4"},
-    {file = "pydocstyle-6.1.1.tar.gz", hash = "sha256:1d41b7c459ba0ee6c345f2eb9ae827cab14a7533a88c5c6f7e94923f72df92dc"},
-]
-pyflakes = [
-    {file = "pyflakes-2.3.1-py2.py3-none-any.whl", hash = "sha256:7893783d01b8a89811dd72d7dfd4d84ff098e5eed95cfa8905b22bbffe52efc3"},
-    {file = "pyflakes-2.3.1.tar.gz", hash = "sha256:f5bc8ecabc05bb9d291eb5203d6810b49040f6ff446a756326104746cc00c1db"},
-]
-pygments = [
-    {file = "Pygments-2.10.0-py3-none-any.whl", hash = "sha256:b8e67fe6af78f492b3c4b3e2970c0624cbf08beb1e493b2c99b9fa1b67a20380"},
-    {file = "Pygments-2.10.0.tar.gz", hash = "sha256:f398865f7eb6874156579fdf36bc840a03cab64d1cde9e93d68f46a425ec52c6"},
-]
-pyjwt = [
-    {file = "PyJWT-2.3.0-py3-none-any.whl", hash = "sha256:e0c4bb8d9f0af0c7f5b1ec4c5036309617d03d56932877f2f7a0beeb5318322f"},
-    {file = "PyJWT-2.3.0.tar.gz", hash = "sha256:b888b4d56f06f6dcd777210c334e69c737be74755d3e5e9ee3fe67dc18a0ee41"},
-]
-pyparsing = [
-    {file = "pyparsing-3.0.1-py3-none-any.whl", hash = "sha256:fd93fc45c47893c300bd98f5dd1b41c0e783eaeb727e7cea210dcc09d64ce7c3"},
-    {file = "pyparsing-3.0.1.tar.gz", hash = "sha256:84196357aa3566d64ad123d7a3c67b0e597a115c4934b097580e5ce220b91531"},
-]
-pytest = [
-    {file = "pytest-6.2.5-py3-none-any.whl", hash = "sha256:7310f8d27bc79ced999e760ca304d69f6ba6c6649c0b60fb0e04a4a77cacc134"},
-    {file = "pytest-6.2.5.tar.gz", hash = "sha256:131b36680866a76e6781d13f101efb86cf674ebb9762eb70d3082b6f29889e89"},
-]
-pytest-cov = [
-    {file = "pytest-cov-2.12.1.tar.gz", hash = "sha256:261ceeb8c227b726249b376b8526b600f38667ee314f910353fa318caa01f4d7"},
-    {file = "pytest_cov-2.12.1-py2.py3-none-any.whl", hash = "sha256:261bb9e47e65bd099c89c3edf92972865210c36813f80ede5277dceb77a4a62a"},
-]
-pytest-django = [
-    {file = "pytest-django-4.4.0.tar.gz", hash = "sha256:b5171e3798bf7e3fc5ea7072fe87324db67a4dd9f1192b037fed4cc3c1b7f455"},
-    {file = "pytest_django-4.4.0-py3-none-any.whl", hash = "sha256:65783e78382456528bd9d79a35843adde9e6a47347b20464eb2c885cb0f1f606"},
-]
-pytest-django-testing-postgresql = [
-    {file = "pytest-django-testing-postgresql-0.1.post0.tar.gz", hash = "sha256:78b0c58930084cb4393407b2e5a2a3b8734c627b841ecef7d62d39bbfb8e8a45"},
-    {file = "pytest_django_testing_postgresql-0.1.post0-py3-none-any.whl", hash = "sha256:78e52e3d1b0ef5f906d5d69247dd6ac7dfb10d840bd81abab92f3f8c30872cd3"},
-]
-pytest-sugar = [
-    {file = "pytest-sugar-0.9.4.tar.gz", hash = "sha256:b1b2186b0a72aada6859bea2a5764145e3aaa2c1cfbb23c3a19b5f7b697563d3"},
-]
-python-crontab = [
-    {file = "python-crontab-2.6.0.tar.gz", hash = "sha256:1e35ed7a3cdc3100545b43e196d34754e6551e7f95e4caebbe0e1c0ca41c2f1b"},
-]
-python-dateutil = [
-    {file = "python-dateutil-2.8.2.tar.gz", hash = "sha256:0123cacc1627ae19ddf3c27a5de5bd67ee4586fbdd6440d9748f8abb483d3e86"},
-    {file = "python_dateutil-2.8.2-py2.py3-none-any.whl", hash = "sha256:961d03dc3453ebbc59dbdea9e4e11c5651520a876d0f4db161e8674aae935da9"},
-]
-python-gnupg = [
-    {file = "python-gnupg-0.4.7.tar.gz", hash = "sha256:2061f56b1942c29b92727bf9aecbd3cea3893acc9cccbdc7eb4604285efe4ac7"},
-    {file = "python_gnupg-0.4.7-py2.py3-none-any.whl", hash = "sha256:3ff5b1bf5e397de6e1fe41a7c0f403dad4e242ac92b345f440eaecfb72a7ebae"},
-]
-python-ldap = [
-    {file = "python-ldap-3.3.1.tar.gz", hash = "sha256:4711cacf013e298754abd70058ccc995758177fb425f1c2d30e71adfc1d00aa5"},
-]
-python3-openid = [
-    {file = "python3-openid-3.2.0.tar.gz", hash = "sha256:33fbf6928f401e0b790151ed2b5290b02545e8775f982485205a066f874aaeaf"},
-    {file = "python3_openid-3.2.0-py3-none-any.whl", hash = "sha256:6626f771e0417486701e0b4daff762e7212e820ca5b29fcc0d05f6f8736dfa6b"},
-]
-pytz = [
-    {file = "pytz-2021.3-py2.py3-none-any.whl", hash = "sha256:3672058bc3453457b622aab7a1c3bfd5ab0bdae451512f6cf25f64ed37f5b87c"},
-    {file = "pytz-2021.3.tar.gz", hash = "sha256:acad2d8b20a1af07d4e4c9d2e9285c5ed9104354062f275f3fcd88dcef4f1326"},
-]
-pyyaml = [
-    {file = "PyYAML-6.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d4db7c7aef085872ef65a8fd7d6d09a14ae91f691dec3e87ee5ee0539d516f53"},
-    {file = "PyYAML-6.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9df7ed3b3d2e0ecfe09e14741b857df43adb5a3ddadc919a2d94fbdf78fea53c"},
-    {file = "PyYAML-6.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:77f396e6ef4c73fdc33a9157446466f1cff553d979bd00ecb64385760c6babdc"},
-    {file = "PyYAML-6.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a80a78046a72361de73f8f395f1f1e49f956c6be882eed58505a15f3e430962b"},
-    {file = "PyYAML-6.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:f84fbc98b019fef2ee9a1cb3ce93e3187a6df0b2538a651bfb890254ba9f90b5"},
-    {file = "PyYAML-6.0-cp310-cp310-win32.whl", hash = "sha256:2cd5df3de48857ed0544b34e2d40e9fac445930039f3cfe4bcc592a1f836d513"},
-    {file = "PyYAML-6.0-cp310-cp310-win_amd64.whl", hash = "sha256:daf496c58a8c52083df09b80c860005194014c3698698d1a57cbcfa182142a3a"},
-    {file = "PyYAML-6.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:897b80890765f037df3403d22bab41627ca8811ae55e9a722fd0392850ec4d86"},
-    {file = "PyYAML-6.0-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:50602afada6d6cbfad699b0c7bb50d5ccffa7e46a3d738092afddc1f9758427f"},
-    {file = "PyYAML-6.0-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:48c346915c114f5fdb3ead70312bd042a953a8ce5c7106d5bfb1a5254e47da92"},
-    {file = "PyYAML-6.0-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:98c4d36e99714e55cfbaaee6dd5badbc9a1ec339ebfc3b1f52e293aee6bb71a4"},
-    {file = "PyYAML-6.0-cp36-cp36m-win32.whl", hash = "sha256:0283c35a6a9fbf047493e3a0ce8d79ef5030852c51e9d911a27badfde0605293"},
-    {file = "PyYAML-6.0-cp36-cp36m-win_amd64.whl", hash = "sha256:07751360502caac1c067a8132d150cf3d61339af5691fe9e87803040dbc5db57"},
-    {file = "PyYAML-6.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:819b3830a1543db06c4d4b865e70ded25be52a2e0631ccd2f6a47a2822f2fd7c"},
-    {file = "PyYAML-6.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:473f9edb243cb1935ab5a084eb238d842fb8f404ed2193a915d1784b5a6b5fc0"},
-    {file = "PyYAML-6.0-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0ce82d761c532fe4ec3f87fc45688bdd3a4c1dc5e0b4a19814b9009a29baefd4"},
-    {file = "PyYAML-6.0-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:231710d57adfd809ef5d34183b8ed1eeae3f76459c18fb4a0b373ad56bedcdd9"},
-    {file = "PyYAML-6.0-cp37-cp37m-win32.whl", hash = "sha256:c5687b8d43cf58545ade1fe3e055f70eac7a5a1a0bf42824308d868289a95737"},
-    {file = "PyYAML-6.0-cp37-cp37m-win_amd64.whl", hash = "sha256:d15a181d1ecd0d4270dc32edb46f7cb7733c7c508857278d3d378d14d606db2d"},
-    {file = "PyYAML-6.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:0b4624f379dab24d3725ffde76559cff63d9ec94e1736b556dacdfebe5ab6d4b"},
-    {file = "PyYAML-6.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:213c60cd50106436cc818accf5baa1aba61c0189ff610f64f4a3e8c6726218ba"},
-    {file = "PyYAML-6.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9fa600030013c4de8165339db93d182b9431076eb98eb40ee068700c9c813e34"},
-    {file = "PyYAML-6.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:277a0ef2981ca40581a47093e9e2d13b3f1fbbeffae064c1d21bfceba2030287"},
-    {file = "PyYAML-6.0-cp38-cp38-win32.whl", hash = "sha256:d4eccecf9adf6fbcc6861a38015c2a64f38b9d94838ac1810a9023a0609e1b78"},
-    {file = "PyYAML-6.0-cp38-cp38-win_amd64.whl", hash = "sha256:1e4747bc279b4f613a09eb64bba2ba602d8a6664c6ce6396a4d0cd413a50ce07"},
-    {file = "PyYAML-6.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:055d937d65826939cb044fc8c9b08889e8c743fdc6a32b33e2390f66013e449b"},
-    {file = "PyYAML-6.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:e61ceaab6f49fb8bdfaa0f92c4b57bcfbea54c09277b1b4f7ac376bfb7a7c174"},
-    {file = "PyYAML-6.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d67d839ede4ed1b28a4e8909735fc992a923cdb84e618544973d7dfc71540803"},
-    {file = "PyYAML-6.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cba8c411ef271aa037d7357a2bc8f9ee8b58b9965831d9e51baf703280dc73d3"},
-    {file = "PyYAML-6.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:40527857252b61eacd1d9af500c3337ba8deb8fc298940291486c465c8b46ec0"},
-    {file = "PyYAML-6.0-cp39-cp39-win32.whl", hash = "sha256:b5b9eccad747aabaaffbc6064800670f0c297e52c12754eb1d976c57e4f74dcb"},
-    {file = "PyYAML-6.0-cp39-cp39-win_amd64.whl", hash = "sha256:b3d267842bf12586ba6c734f89d1f5b871df0273157918b0ccefa29deb05c21c"},
-    {file = "PyYAML-6.0.tar.gz", hash = "sha256:68fb519c14306fec9720a2a5b45bc9f0c8d1b9c72adf45c37baedfcd949c35a2"},
-]
-qrcode = [
-    {file = "qrcode-6.1-py2.py3-none-any.whl", hash = "sha256:3996ee560fc39532910603704c82980ff6d4d5d629f9c3f25f34174ce8606cf5"},
-    {file = "qrcode-6.1.tar.gz", hash = "sha256:505253854f607f2abf4d16092c61d4e9d511a3b4392e60bff957a68592b04369"},
-]
-redis = [
-    {file = "redis-3.5.3-py2.py3-none-any.whl", hash = "sha256:432b788c4530cfe16d8d943a09d40ca6c16149727e4afe8c2c9d5580c59d9f24"},
-    {file = "redis-3.5.3.tar.gz", hash = "sha256:0e7e0cfca8660dea8b7d5cd8c4f6c5e29e11f31158c0b0ae91a397f00e5a05a2"},
-]
-regex = [
-    {file = "regex-2021.10.23-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:45b65d6a275a478ac2cbd7fdbf7cc93c1982d613de4574b56fd6972ceadb8395"},
-    {file = "regex-2021.10.23-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:74d071dbe4b53c602edd87a7476ab23015a991374ddb228d941929ad7c8c922e"},
-    {file = "regex-2021.10.23-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:34d870f9f27f2161709054d73646fc9aca49480617a65533fc2b4611c518e455"},
-    {file = "regex-2021.10.23-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2fb698037c35109d3c2e30f2beb499e5ebae6e4bb8ff2e60c50b9a805a716f79"},
-    {file = "regex-2021.10.23-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:cb46b542133999580ffb691baf67410306833ee1e4f58ed06b6a7aaf4e046952"},
-    {file = "regex-2021.10.23-cp310-cp310-win32.whl", hash = "sha256:5e9c9e0ce92f27cef79e28e877c6b6988c48b16942258f3bc55d39b5f911df4f"},
-    {file = "regex-2021.10.23-cp310-cp310-win_amd64.whl", hash = "sha256:ab7c5684ff3538b67df3f93d66bd3369b749087871ae3786e70ef39e601345b0"},
-    {file = "regex-2021.10.23-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:de557502c3bec8e634246588a94e82f1ee1b9dfcfdc453267c4fb652ff531570"},
-    {file = "regex-2021.10.23-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ee684f139c91e69fe09b8e83d18b4d63bf87d9440c1eb2eeb52ee851883b1b29"},
-    {file = "regex-2021.10.23-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:5095a411c8479e715784a0c9236568ae72509450ee2226b649083730f3fadfc6"},
-    {file = "regex-2021.10.23-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7b568809dca44cb75c8ebb260844ea98252c8c88396f9d203f5094e50a70355f"},
-    {file = "regex-2021.10.23-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:eb672217f7bd640411cfc69756ce721d00ae600814708d35c930930f18e8029f"},
-    {file = "regex-2021.10.23-cp36-cp36m-win32.whl", hash = "sha256:a7a986c45d1099a5de766a15de7bee3840b1e0e1a344430926af08e5297cf666"},
-    {file = "regex-2021.10.23-cp36-cp36m-win_amd64.whl", hash = "sha256:6d7722136c6ed75caf84e1788df36397efdc5dbadab95e59c2bba82d4d808a4c"},
-    {file = "regex-2021.10.23-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:9f665677e46c5a4d288ece12fdedf4f4204a422bb28ff05f0e6b08b7447796d1"},
-    {file = "regex-2021.10.23-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:450dc27483548214314640c89a0f275dbc557968ed088da40bde7ef8fb52829e"},
-    {file = "regex-2021.10.23-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:129472cd06062fb13e7b4670a102951a3e655e9b91634432cfbdb7810af9d710"},
-    {file = "regex-2021.10.23-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a940ca7e7189d23da2bfbb38973832813eab6bd83f3bf89a977668c2f813deae"},
-    {file = "regex-2021.10.23-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:530fc2bbb3dc1ebb17f70f7b234f90a1dd43b1b489ea38cea7be95fb21cdb5c7"},
-    {file = "regex-2021.10.23-cp37-cp37m-win32.whl", hash = "sha256:ded0c4a3eee56b57fcb2315e40812b173cafe79d2f992d50015f4387445737fa"},
-    {file = "regex-2021.10.23-cp37-cp37m-win_amd64.whl", hash = "sha256:391703a2abf8013d95bae39145d26b4e21531ab82e22f26cd3a181ee2644c234"},
-    {file = "regex-2021.10.23-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:be04739a27be55631069b348dda0c81d8ea9822b5da10b8019b789e42d1fe452"},
-    {file = "regex-2021.10.23-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:13ec99df95003f56edcd307db44f06fbeb708c4ccdcf940478067dd62353181e"},
-    {file = "regex-2021.10.23-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:8d1cdcda6bd16268316d5db1038965acf948f2a6f43acc2e0b1641ceab443623"},
-    {file = "regex-2021.10.23-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0c186691a7995ef1db61205e00545bf161fb7b59cdb8c1201c89b333141c438a"},
-    {file = "regex-2021.10.23-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:2b20f544cbbeffe171911f6ce90388ad36fe3fad26b7c7a35d4762817e9ea69c"},
-    {file = "regex-2021.10.23-cp38-cp38-win32.whl", hash = "sha256:c0938ddd60cc04e8f1faf7a14a166ac939aac703745bfcd8e8f20322a7373019"},
-    {file = "regex-2021.10.23-cp38-cp38-win_amd64.whl", hash = "sha256:56f0c81c44638dfd0e2367df1a331b4ddf2e771366c4b9c5d9a473de75e3e1c7"},
-    {file = "regex-2021.10.23-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:80bb5d2e92b2258188e7dcae5b188c7bf868eafdf800ea6edd0fbfc029984a88"},
-    {file = "regex-2021.10.23-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e1dae12321b31059a1a72aaa0e6ba30156fe7e633355e445451e4021b8e122b6"},
-    {file = "regex-2021.10.23-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:1f2b59c28afc53973d22e7bc18428721ee8ca6079becf1b36571c42627321c65"},
-    {file = "regex-2021.10.23-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d134757a37d8640f3c0abb41f5e68b7cf66c644f54ef1cb0573b7ea1c63e1509"},
-    {file = "regex-2021.10.23-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:0dcc0e71118be8c69252c207630faf13ca5e1b8583d57012aae191e7d6d28b84"},
-    {file = "regex-2021.10.23-cp39-cp39-win32.whl", hash = "sha256:a30513828180264294953cecd942202dfda64e85195ae36c265daf4052af0464"},
-    {file = "regex-2021.10.23-cp39-cp39-win_amd64.whl", hash = "sha256:0f7552429dd39f70057ac5d0e897e5bfe211629652399a21671e53f2a9693a4e"},
-    {file = "regex-2021.10.23.tar.gz", hash = "sha256:f3f9a91d3cc5e5b0ddf1043c0ae5fa4852f18a1c0050318baf5fc7930ecc1f9c"},
-]
-requests = [
-    {file = "requests-2.26.0-py2.py3-none-any.whl", hash = "sha256:6c1246513ecd5ecd4528a0906f910e8f0f9c6b8ec72030dc9fd154dc1a6efd24"},
-    {file = "requests-2.26.0.tar.gz", hash = "sha256:b8aa58f8cf793ffd8782d3d8cb19e66ef36f7aba4353eec859e74678b01b07a7"},
-]
-requests-oauthlib = [
-    {file = "requests-oauthlib-1.3.0.tar.gz", hash = "sha256:b4261601a71fd721a8bd6d7aa1cc1d6a8a93b4a9f5e96626f8e4d91e8beeaa6a"},
-    {file = "requests_oauthlib-1.3.0-py2.py3-none-any.whl", hash = "sha256:7f71572defaecd16372f9006f33c2ec8c077c3cfa6f5911a9a90202beb513f3d"},
-    {file = "requests_oauthlib-1.3.0-py3.7.egg", hash = "sha256:fa6c47b933f01060936d87ae9327fead68768b69c6c9ea2109c48be30f2d4dbc"},
-]
-restructuredtext-lint = [
-    {file = "restructuredtext_lint-1.3.2.tar.gz", hash = "sha256:d3b10a1fe2ecac537e51ae6d151b223b78de9fafdd50e5eb6b08c243df173c80"},
-]
-"ruamel.yaml" = [
-    {file = "ruamel.yaml-0.17.16-py3-none-any.whl", hash = "sha256:ea21da1198c4b41b8e7a259301cc9710d3b972bf8ba52f06218478e6802dd1f1"},
-    {file = "ruamel.yaml-0.17.16.tar.gz", hash = "sha256:1a771fc92d3823682b7f0893ad56cb5a5c87c48e62b5399d6f42c8759a583b33"},
-]
-"ruamel.yaml.clib" = [
-    {file = "ruamel.yaml.clib-0.2.6-cp35-cp35m-macosx_10_6_intel.whl", hash = "sha256:cfdb9389d888c5b74af297e51ce357b800dd844898af9d4a547ffc143fa56751"},
-    {file = "ruamel.yaml.clib-0.2.6-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:7b2927e92feb51d830f531de4ccb11b320255ee95e791022555971c466af4527"},
-    {file = "ruamel.yaml.clib-0.2.6-cp35-cp35m-win32.whl", hash = "sha256:ada3f400d9923a190ea8b59c8f60680c4ef8a4b0dfae134d2f2ff68429adfab5"},
-    {file = "ruamel.yaml.clib-0.2.6-cp35-cp35m-win_amd64.whl", hash = "sha256:de9c6b8a1ba52919ae919f3ae96abb72b994dd0350226e28f3686cb4f142165c"},
-    {file = "ruamel.yaml.clib-0.2.6-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:d67f273097c368265a7b81e152e07fb90ed395df6e552b9fa858c6d2c9f42502"},
-    {file = "ruamel.yaml.clib-0.2.6-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:72a2b8b2ff0a627496aad76f37a652bcef400fd861721744201ef1b45199ab78"},
-    {file = "ruamel.yaml.clib-0.2.6-cp36-cp36m-win32.whl", hash = "sha256:9efef4aab5353387b07f6b22ace0867032b900d8e91674b5d8ea9150db5cae94"},
-    {file = "ruamel.yaml.clib-0.2.6-cp36-cp36m-win_amd64.whl", hash = "sha256:846fc8336443106fe23f9b6d6b8c14a53d38cef9a375149d61f99d78782ea468"},
-    {file = "ruamel.yaml.clib-0.2.6-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:0847201b767447fc33b9c235780d3aa90357d20dd6108b92be544427bea197dd"},
-    {file = "ruamel.yaml.clib-0.2.6-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:78988ed190206672da0f5d50c61afef8f67daa718d614377dcd5e3ed85ab4a99"},
-    {file = "ruamel.yaml.clib-0.2.6-cp37-cp37m-win32.whl", hash = "sha256:a49e0161897901d1ac9c4a79984b8410f450565bbad64dbfcbf76152743a0cdb"},
-    {file = "ruamel.yaml.clib-0.2.6-cp37-cp37m-win_amd64.whl", hash = "sha256:bf75d28fa071645c529b5474a550a44686821decebdd00e21127ef1fd566eabe"},
-    {file = "ruamel.yaml.clib-0.2.6-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:a32f8d81ea0c6173ab1b3da956869114cae53ba1e9f72374032e33ba3118c233"},
-    {file = "ruamel.yaml.clib-0.2.6-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:7f7ecb53ae6848f959db6ae93bdff1740e651809780822270eab111500842a84"},
-    {file = "ruamel.yaml.clib-0.2.6-cp38-cp38-win32.whl", hash = "sha256:89221ec6d6026f8ae859c09b9718799fea22c0e8da8b766b0b2c9a9ba2db326b"},
-    {file = "ruamel.yaml.clib-0.2.6-cp38-cp38-win_amd64.whl", hash = "sha256:31ea73e564a7b5fbbe8188ab8b334393e06d997914a4e184975348f204790277"},
-    {file = "ruamel.yaml.clib-0.2.6-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:dc6a613d6c74eef5a14a214d433d06291526145431c3b964f5e16529b1842bed"},
-    {file = "ruamel.yaml.clib-0.2.6-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:1866cf2c284a03b9524a5cc00daca56d80057c5ce3cdc86a52020f4c720856f0"},
-    {file = "ruamel.yaml.clib-0.2.6-cp39-cp39-win32.whl", hash = "sha256:3fb9575a5acd13031c57a62cc7823e5d2ff8bc3835ba4d94b921b4e6ee664104"},
-    {file = "ruamel.yaml.clib-0.2.6-cp39-cp39-win_amd64.whl", hash = "sha256:825d5fccef6da42f3c8eccd4281af399f21c02b32d98e113dbc631ea6a6ecbc7"},
-    {file = "ruamel.yaml.clib-0.2.6.tar.gz", hash = "sha256:4ff604ce439abb20794f05613c374759ce10e3595d1867764dd1ae675b85acbd"},
-]
-rules = [
-    {file = "rules-2.2.tar.gz", hash = "sha256:9bae429f9d4f91a375402990da1541f9e093b0ac077221d57124d06eeeca4405"},
-]
-s3transfer = [
-    {file = "s3transfer-0.5.0-py3-none-any.whl", hash = "sha256:9c1dc369814391a6bda20ebbf4b70a0f34630592c9aa520856bf384916af2803"},
-    {file = "s3transfer-0.5.0.tar.gz", hash = "sha256:50ed823e1dc5868ad40c8dc92072f757aa0e653a192845c94a3b676f4a62da4c"},
-]
-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.4.1-py3-none-any.whl", hash = "sha256:93c9cc2ffe54a451e02981c07a5a23cbd830701102789939cfb4ff91efd6ca8c"},
-    {file = "scramp-1.4.1.tar.gz", hash = "sha256:f964801077be9be2a1416ffe255d2d78834b3d9d5c8ce5d28f76a856f209f70e"},
-]
-selenium = [
-    {file = "selenium-3.141.0-py2.py3-none-any.whl", hash = "sha256:2d7131d7bc5a5b99a2d9b04aaf2612c411b03b8ca1b1ee8d3de5845a9be2cb3c"},
-    {file = "selenium-3.141.0.tar.gz", hash = "sha256:deaf32b60ad91a4611b98d8002757f29e6f2c2d5fcaf202e1c9ad06d6772300d"},
-]
-sentry-sdk = [
-    {file = "sentry-sdk-1.4.3.tar.gz", hash = "sha256:b9844751e40710e84a457c5bc29b21c383ccb2b63d76eeaad72f7f1c808c8828"},
-    {file = "sentry_sdk-1.4.3-py2.py3-none-any.whl", hash = "sha256:c091cc7115ff25fe3a0e410dbecd7a996f81a3f6137d2272daef32d6c3cfa6dc"},
-]
-six = [
-    {file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"},
-    {file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"},
-]
-smmap = [
-    {file = "smmap-5.0.0-py3-none-any.whl", hash = "sha256:2aba19d6a040e78d8b09de5c57e96207b09ed71d8e55ce0959eeee6c8e190d94"},
-    {file = "smmap-5.0.0.tar.gz", hash = "sha256:c840e62059cd3be204b0c9c9f74be2c09d5648eddd4580d9314c3ecde0b30936"},
-]
-snowballstemmer = [
-    {file = "snowballstemmer-2.1.0-py2.py3-none-any.whl", hash = "sha256:b51b447bea85f9968c13b650126a888aabd4cb4463fca868ec596826325dedc2"},
-    {file = "snowballstemmer-2.1.0.tar.gz", hash = "sha256:e997baa4f2e9139951b6f4c631bad912dfd3c792467e2f03d7239464af90e914"},
-]
-soupsieve = [
-    {file = "soupsieve-2.2.1-py3-none-any.whl", hash = "sha256:c2c1c2d44f158cdbddab7824a9af8c4f83c76b1e23e049479aa432feb6c4c23b"},
-    {file = "soupsieve-2.2.1.tar.gz", hash = "sha256:052774848f448cf19c7e959adf5566904d525f33a3f8b6ba6f6f8f26ec7de0cc"},
-]
-spdx-license-list = [
-    {file = "spdx_license_list-0.5.2-py3-none-any.whl", hash = "sha256:1b338470c7b403dbecceca563a316382c7977516128ca6c1e8f7078e3ed6e7b0"},
-    {file = "spdx_license_list-0.5.2.tar.gz", hash = "sha256:952996f72ab807972dc2278bb9b91e5294767211e51f09aad9c0e2ff5b82a31b"},
-]
-sphinx = [
-    {file = "Sphinx-3.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.12.0.tar.gz", hash = "sha256:193617d9dbe0847281b1399d369e74e34cd959c82e02c7efde077fca908a9f52"},
-    {file = "sphinx_autodoc_typehints-1.12.0-py3-none-any.whl", hash = "sha256:5e81776ec422dd168d688ab60f034fccfafbcd94329e9537712c93003bddc04a"},
-]
-sphinx-materialdesign-theme = [
-    {file = "sphinx_materialdesign_theme-0.1.11.tar.gz", hash = "sha256:6e9dae4c6e5ba23c0657a94c1cf65f64be9c8bc1594a6fb41815f7daa3326aa9"},
-]
-sphinxcontrib-applehelp = [
-    {file = "sphinxcontrib-applehelp-1.0.2.tar.gz", hash = "sha256:a072735ec80e7675e3f432fcae8610ecf509c5f1869d17e2eecff44389cdbc58"},
-    {file = "sphinxcontrib_applehelp-1.0.2-py2.py3-none-any.whl", hash = "sha256:806111e5e962be97c29ec4c1e7fe277bfd19e9652fb1a4392105b43e01af885a"},
-]
-sphinxcontrib-devhelp = [
-    {file = "sphinxcontrib-devhelp-1.0.2.tar.gz", hash = "sha256:ff7f1afa7b9642e7060379360a67e9c41e8f3121f2ce9164266f61b9f4b338e4"},
-    {file = "sphinxcontrib_devhelp-1.0.2-py2.py3-none-any.whl", hash = "sha256:8165223f9a335cc1af7ffe1ed31d2871f325254c0423bc0c4c7cd1c1e4734a2e"},
-]
-sphinxcontrib-django = [
-    {file = "sphinxcontrib-django-0.5.1.tar.gz", hash = "sha256:3b48a9067d8db4713d47e3a4160af10288d02d448c866d1b44b001adbe74cc1e"},
-    {file = "sphinxcontrib_django-0.5.1-py2.py3-none-any.whl", hash = "sha256:73ef7fdbf2ed6d4f35b7ae709032bd5ac493d93cedd0624ea7b51bf5fce41267"},
-]
-sphinxcontrib-htmlhelp = [
-    {file = "sphinxcontrib-htmlhelp-2.0.0.tar.gz", hash = "sha256:f5f8bb2d0d629f398bf47d0d69c07bc13b65f75a81ad9e2f71a63d4b7a2f6db2"},
-    {file = "sphinxcontrib_htmlhelp-2.0.0-py2.py3-none-any.whl", hash = "sha256:d412243dfb797ae3ec2b59eca0e52dac12e75a241bf0e4eb861e450d06c6ed07"},
-]
-sphinxcontrib-jsmath = [
-    {file = "sphinxcontrib-jsmath-1.0.1.tar.gz", hash = "sha256:a9925e4a4587247ed2191a22df5f6970656cb8ca2bd6284309578f2153e0c4b8"},
-    {file = "sphinxcontrib_jsmath-1.0.1-py2.py3-none-any.whl", hash = "sha256:2ec2eaebfb78f3f2078e73666b1415417a116cc848b72e5172e596c871103178"},
-]
-sphinxcontrib-qthelp = [
-    {file = "sphinxcontrib-qthelp-1.0.3.tar.gz", hash = "sha256:4c33767ee058b70dba89a6fc5c1892c0d57a54be67ddd3e7875a18d14cba5a72"},
-    {file = "sphinxcontrib_qthelp-1.0.3-py2.py3-none-any.whl", hash = "sha256:bd9fc24bcb748a8d51fd4ecaade681350aa63009a347a8c14e637895444dfab6"},
-]
-sphinxcontrib-serializinghtml = [
-    {file = "sphinxcontrib-serializinghtml-1.1.5.tar.gz", hash = "sha256:aa5f6de5dfdf809ef505c4895e51ef5c9eac17d0f287933eb49ec495280b6952"},
-    {file = "sphinxcontrib_serializinghtml-1.1.5-py2.py3-none-any.whl", hash = "sha256:352a9a00ae864471d3a7ead8d7d79f5fc0b57e8b3f95e9867eb9eb28999b92fd"},
-]
-sqlparse = [
-    {file = "sqlparse-0.4.2-py3-none-any.whl", hash = "sha256:48719e356bb8b42991bdbb1e8b83223757b93789c00910a616a071910ca4a64d"},
-    {file = "sqlparse-0.4.2.tar.gz", hash = "sha256:0c00730c74263a94e5a9919ade150dfc3b19c574389985446148402998287dae"},
-]
-stevedore = [
-    {file = "stevedore-3.5.0-py3-none-any.whl", hash = "sha256:a547de73308fd7e90075bb4d301405bebf705292fa90a90fc3bcf9133f58616c"},
-    {file = "stevedore-3.5.0.tar.gz", hash = "sha256:f40253887d8712eaa2bb0ea3830374416736dc8ec0e22f5a65092c1174c44335"},
-]
-termcolor = [
-    {file = "termcolor-1.1.0.tar.gz", hash = "sha256:1d6d69ce66211143803fbc56652b41d73b4a400a2891d7bf7a1cdf4c02de613b"},
-]
-testfixtures = [
-    {file = "testfixtures-6.18.3-py2.py3-none-any.whl", hash = "sha256:6ddb7f56a123e1a9339f130a200359092bd0a6455e31838d6c477e8729bb7763"},
-    {file = "testfixtures-6.18.3.tar.gz", hash = "sha256:2600100ae96ffd082334b378e355550fef8b4a529a6fa4c34f47130905c7426d"},
-]
-"testing.common.database" = [
-    {file = "testing.common.database-2.0.3-py2.py3-none-any.whl", hash = "sha256:e3ed492bf480a87f271f74c53b262caf5d85c8bc09989a8f534fa2283ec52492"},
-    {file = "testing.common.database-2.0.3.tar.gz", hash = "sha256:965d80b2985315325dc358c3061b174a712f4d4d5bf6a80b58b11f9a1dd86d73"},
-]
-"testing.postgresql" = [
-    {file = "testing.postgresql-1.3.0-py2.py3-none-any.whl", hash = "sha256:1b41daeb98dfc8cd4a584bb91e8f5f4ab182993870f95257afe5f1ba6151a598"},
-    {file = "testing.postgresql-1.3.0.tar.gz", hash = "sha256:8e1a69760369a7a8ffe63a66b6d95a5cd82db2fb976e4a8f85ffd24fbfc447d8"},
-]
-toml = [
-    {file = "toml-0.10.2-py2.py3-none-any.whl", hash = "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b"},
-    {file = "toml-0.10.2.tar.gz", hash = "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f"},
-]
-traitlets = [
-    {file = "traitlets-5.1.1-py3-none-any.whl", hash = "sha256:2d313cc50a42cd6c277e7d7dc8d4d7fedd06a2c215f78766ae7b1a66277e0033"},
-    {file = "traitlets-5.1.1.tar.gz", hash = "sha256:059f456c5a7c1c82b98c2e8c799f39c9b8128f6d0d46941ee118daace9eb70c7"},
-]
-twilio = [
-    {file = "twilio-7.2.0.tar.gz", hash = "sha256:f99643641c193fcd09b58f20ef65a78937e5c426319530a42c325b3abd80e739"},
-]
-typed-ast = [
-    {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"},
-]
-types-pytz = [
-    {file = "types-pytz-2021.3.0.tar.gz", hash = "sha256:86a61967834dceeaaf98b6902ed8357efdd262bb8afcaf4bc8ccecf748592778"},
-    {file = "types_pytz-2021.3.0-py3-none-any.whl", hash = "sha256:b5027e5de50a4c978cd60ca16849d934d44c44ebd7d29cf13ada009efaa9feef"},
-]
-types-pyyaml = [
-    {file = "types-PyYAML-6.0.0.tar.gz", hash = "sha256:3d3591ddfc488fc30be3c506a0c0fe54da968fe98d8b76ab12e59d455330ffca"},
-    {file = "types_PyYAML-6.0.0-py3-none-any.whl", hash = "sha256:746f23d351245d176d7bc89eef79e2ee94b4e7306f7d23bfefb3dc946c0fb58d"},
-]
-typing-extensions = [
-    {file = "typing_extensions-3.10.0.2-py2-none-any.whl", hash = "sha256:d8226d10bc02a29bcc81df19a26e56a9647f8b0a6d4a83924139f4a8b01f17b7"},
-    {file = "typing_extensions-3.10.0.2-py3-none-any.whl", hash = "sha256:f1d25edafde516b146ecd0613dabcc61409817af4766fbbcfb8d1ad4ec441a34"},
-    {file = "typing_extensions-3.10.0.2.tar.gz", hash = "sha256:49f75d16ff11f1cd258e1b988ccff82a3ca5570217d7ad8c5f48205dd99a677e"},
-]
-urllib3 = [
-    {file = "urllib3-1.26.7-py2.py3-none-any.whl", hash = "sha256:c4fdf4019605b6e5423637e01bc9fe4daef873709a7973e195ceba0a62bbc844"},
-    {file = "urllib3-1.26.7.tar.gz", hash = "sha256:4987c65554f7a2dbf30c18fd48778ef124af6fab771a377103da0585e2336ece"},
-]
-uwsgi = [
-    {file = "uwsgi-2.0.20.tar.gz", hash = "sha256:88ab9867d8973d8ae84719cf233b7dafc54326fcaec89683c3f9f77c002cdff9"},
-]
-vine = [
-    {file = "vine-5.0.0-py2.py3-none-any.whl", hash = "sha256:4c9dceab6f76ed92105027c49c823800dd33cacce13bdedc5b914e3514b7fb30"},
-    {file = "vine-5.0.0.tar.gz", hash = "sha256:7d3b1624a953da82ef63462013bbd271d3eb75751489f9807598e8f340bd637e"},
-]
-wcwidth = [
-    {file = "wcwidth-0.2.5-py2.py3-none-any.whl", hash = "sha256:beb4802a9cebb9144e99086eff703a642a13d6a0052920003a230f3294bbe784"},
-    {file = "wcwidth-0.2.5.tar.gz", hash = "sha256:c4d647b99872929fdb7bdcaa4fbe7f01413ed3d98077df798530e5b04f116c83"},
-]
-webencodings = [
-    {file = "webencodings-0.5.1-py2.py3-none-any.whl", hash = "sha256:a0af1213f3c2226497a97e2b3aa01a7e4bee4f403f95be16fc9acd2947514a78"},
-    {file = "webencodings-0.5.1.tar.gz", hash = "sha256:b36a1c245f2d304965eb4e0a82848379241dc04b865afcc4aab16748587e1923"},
-]
-whoosh = [
-    {file = "Whoosh-2.7.4-py2.py3-none-any.whl", hash = "sha256:aa39c3c3426e3fd107dcb4bde64ca1e276a65a889d9085a6e4b54ba82420a852"},
-    {file = "Whoosh-2.7.4.tar.gz", hash = "sha256:7ca5633dbfa9e0e0fa400d3151a8a0c4bec53bd2ecedc0a67705b17565c31a83"},
-    {file = "Whoosh-2.7.4.zip", hash = "sha256:e0857375f63e9041e03fedd5b7541f97cf78917ac1b6b06c1fcc9b45375dda69"},
-]
-wrapt = [
-    {file = "wrapt-1.13.2-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:3de7b4d3066cc610054e7aa2c005645e308df2f92be730aae3a47d42e910566a"},
-    {file = "wrapt-1.13.2-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:8164069f775c698d15582bf6320a4f308c50d048c1c10cf7d7a341feaccf5df7"},
-    {file = "wrapt-1.13.2-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:9adee1891253670575028279de8365c3a02d3489a74a66d774c321472939a0b1"},
-    {file = "wrapt-1.13.2-cp27-cp27m-manylinux2010_i686.whl", hash = "sha256:a70d876c9aba12d3bd7f8f1b05b419322c6789beb717044eea2c8690d35cb91b"},
-    {file = "wrapt-1.13.2-cp27-cp27m-manylinux2010_x86_64.whl", hash = "sha256:3f87042623530bcffea038f824b63084180513c21e2e977291a9a7e65a66f13b"},
-    {file = "wrapt-1.13.2-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:e634136f700a21e1fcead0c137f433dde928979538c14907640607d43537d468"},
-    {file = "wrapt-1.13.2-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:3e33c138d1e3620b1e0cc6fd21e46c266393ed5dae0d595b7ed5a6b73ed57aa0"},
-    {file = "wrapt-1.13.2-cp27-cp27mu-manylinux2010_i686.whl", hash = "sha256:283e402e5357e104ac1e3fba5791220648e9af6fb14ad7d9cc059091af2b31d2"},
-    {file = "wrapt-1.13.2-cp27-cp27mu-manylinux2010_x86_64.whl", hash = "sha256:ccb34ce599cab7f36a4c90318697ead18312c67a9a76327b3f4f902af8f68ea1"},
-    {file = "wrapt-1.13.2-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:fbad5ba74c46517e6488149514b2e2348d40df88cd6b52a83855b7a8bf04723f"},
-    {file = "wrapt-1.13.2-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:724ed2bc9c91a2b9026e5adce310fa60c6e7c8760b03391445730b9789b9d108"},
-    {file = "wrapt-1.13.2-cp35-cp35m-manylinux2010_i686.whl", hash = "sha256:83f2793ec6f3ef513ad8d5b9586f5ee6081cad132e6eae2ecb7eac1cc3decae0"},
-    {file = "wrapt-1.13.2-cp35-cp35m-manylinux2010_x86_64.whl", hash = "sha256:0473d1558b93e314e84313cc611f6c86be779369f9d3734302bf185a4d2625b1"},
-    {file = "wrapt-1.13.2-cp35-cp35m-win32.whl", hash = "sha256:15eee0e6fd07f48af2f66d0e6f2ff1916ffe9732d464d5e2390695296872cad9"},
-    {file = "wrapt-1.13.2-cp35-cp35m-win_amd64.whl", hash = "sha256:bc85d17d90201afd88e3d25421da805e4e135012b5d1f149e4de2981394b2a52"},
-    {file = "wrapt-1.13.2-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:c6ee5f8734820c21b9b8bf705e99faba87f21566d20626568eeb0d62cbeaf23c"},
-    {file = "wrapt-1.13.2-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:53c6706a1bcfb6436f1625511b95b812798a6d2ccc51359cd791e33722b5ea32"},
-    {file = "wrapt-1.13.2-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:fbe6aebc9559fed7ea27de51c2bf5c25ba2a4156cf0017556f72883f2496ee9a"},
-    {file = "wrapt-1.13.2-cp36-cp36m-manylinux2010_i686.whl", hash = "sha256:0582180566e7a13030f896c2f1ac6a56134ab5f3c3f4c5538086f758b1caf3f2"},
-    {file = "wrapt-1.13.2-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:bff0a59387a0a2951cb869251257b6553663329a1b5525b5226cab8c88dcbe7e"},
-    {file = "wrapt-1.13.2-cp36-cp36m-win32.whl", hash = "sha256:df3eae297a5f1594d1feb790338120f717dac1fa7d6feed7b411f87e0f2401c7"},
-    {file = "wrapt-1.13.2-cp36-cp36m-win_amd64.whl", hash = "sha256:1eb657ed84f4d3e6ad648483c8a80a0cf0a78922ef94caa87d327e2e1ad49b48"},
-    {file = "wrapt-1.13.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:a0cdedf681db878416c05e1831ec69691b0e6577ac7dca9d4f815632e3549580"},
-    {file = "wrapt-1.13.2-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:87ee3c73bdfb4367b26c57259995935501829f00c7b3eed373e2ad19ec21e4e4"},
-    {file = "wrapt-1.13.2-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:3e0d16eedc242d01a6f8cf0623e9cdc3b869329da3f97a15961d8864111d8cf0"},
-    {file = "wrapt-1.13.2-cp37-cp37m-manylinux2010_i686.whl", hash = "sha256:8318088860968c07e741537030b1abdd8908ee2c71fbe4facdaade624a09e006"},
-    {file = "wrapt-1.13.2-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:d90520616fce71c05dedeac3a0fe9991605f0acacd276e5f821842e454485a70"},
-    {file = "wrapt-1.13.2-cp37-cp37m-win32.whl", hash = "sha256:22142afab65daffc95863d78effcbd31c19a8003eca73de59f321ee77f73cadb"},
-    {file = "wrapt-1.13.2-cp37-cp37m-win_amd64.whl", hash = "sha256:d0d717e10f952df7ea41200c507cc7e24458f4c45b56c36ad418d2e79dacd1d4"},
-    {file = "wrapt-1.13.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:593cb049ce1c391e0288523b30426c4430b26e74c7e6f6e2844bd99ac7ecc831"},
-    {file = "wrapt-1.13.2-cp38-cp38-manylinux1_i686.whl", hash = "sha256:8860c8011a6961a651b1b9f46fdbc589ab63b0a50d645f7d92659618a3655867"},
-    {file = "wrapt-1.13.2-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:ada5e29e59e2feb710589ca1c79fd989b1dd94d27079dc1d199ec954a6ecc724"},
-    {file = "wrapt-1.13.2-cp38-cp38-manylinux2010_i686.whl", hash = "sha256:fdede980273aeca591ad354608778365a3a310e0ecdd7a3587b38bc5be9b1808"},
-    {file = "wrapt-1.13.2-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:af9480de8e63c5f959a092047aaf3d7077422ded84695b3398f5d49254af3e90"},
-    {file = "wrapt-1.13.2-cp38-cp38-win32.whl", hash = "sha256:c65e623ea7556e39c4f0818200a046cbba7575a6b570ff36122c276fdd30ab0a"},
-    {file = "wrapt-1.13.2-cp38-cp38-win_amd64.whl", hash = "sha256:b20703356cae1799080d0ad15085dc3213c1ac3f45e95afb9f12769b98231528"},
-    {file = "wrapt-1.13.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:1c5c4cf188b5643a97e87e2110bbd4f5bc491d54a5b90633837b34d5df6a03fe"},
-    {file = "wrapt-1.13.2-cp39-cp39-manylinux1_i686.whl", hash = "sha256:82223f72eba6f63eafca87a0f614495ae5aa0126fe54947e2b8c023969e9f2d7"},
-    {file = "wrapt-1.13.2-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:81a4cf257263b299263472d669692785f9c647e7dca01c18286b8f116dbf6b38"},
-    {file = "wrapt-1.13.2-cp39-cp39-manylinux2010_i686.whl", hash = "sha256:728e2d9b7a99dd955d3426f237b940fc74017c4a39b125fec913f575619ddfe9"},
-    {file = "wrapt-1.13.2-cp39-cp39-manylinux2010_x86_64.whl", hash = "sha256:7574de567dcd4858a2ffdf403088d6df8738b0e1eabea220553abf7c9048f59e"},
-    {file = "wrapt-1.13.2-cp39-cp39-win32.whl", hash = "sha256:c7ac2c7a8e34bd06710605b21dd1f3576764443d68e069d2afba9b116014d072"},
-    {file = "wrapt-1.13.2-cp39-cp39-win_amd64.whl", hash = "sha256:6e6d1a8eeef415d7fb29fe017de0e48f45e45efd2d1bfda28fc50b7b330859ef"},
-    {file = "wrapt-1.13.2.tar.gz", hash = "sha256:dca56cc5963a5fd7c2aa8607017753f534ee514e09103a6c55d2db70b50e7447"},
-]
-yubiotp = [
-    {file = "YubiOTP-1.0.0.post1-py2.py3-none-any.whl", hash = "sha256:7ad57011866e0bc6c6d179ffbc3926fcc0e82d410178a6d01ba4da0f88332878"},
-    {file = "YubiOTP-1.0.0.post1.tar.gz", hash = "sha256:c13825f7b76a69afb92f19521f4dea9f5031d70f45123b505dc2e0ac03132065"},
-]
diff --git a/pyproject.toml b/pyproject.toml
index 044f77bfbf8d6f1c1e45923525274aa81d2a0f0b..b67b071d64728be1cf835a1c1e7c0067c81c51f8 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -1,11 +1,18 @@
 [tool.poetry]
 name = "AlekSIS-Core"
-version = "2.1.dev0"
+version = "2.7.5.dev0"
 packages = [
     { include = "aleksis" }
 ]
 readme = "README.rst"
-include = ["CHANGELOG.rst", "LICENCE.rst", "docs/*", "docs/**/*", "aleksis/**/*.mo", "conftest.py", "tox.ini"]
+include = [
+    { path = "aleksis/**/*.mo", format = ["sdist", "wheel"] },
+    { path = "*.rst", format = "sdist" },
+    { path = "docs/*", format = "sdist" },
+    { path = "docs/**/*", format = "sdist" },
+    { path = "conftest.py", format = "sdist" },
+    { path = "tox.ini", format = "sdist" }
+]
 
 description = "AlekSIS (School Information System) — Core"
 authors = [
@@ -17,7 +24,8 @@ authors = [
     "Jonathan Weth <dev@jonathanweth.de>",
     "Hangzhi Yu <yuha@katharineum.de>",
     "Lloyd Meins <meinsll@katharineum.de>",
-    "magicfelix <felix@felix-zauberer.de>"
+    "magicfelix <felix@felix-zauberer.de>",
+    "Benedict Suska <benedict.suska@teckids.de>"
 ]
 maintainers = [
     "Jonathan Weth <dev@jonathanweth.de>",
@@ -29,7 +37,7 @@ repository = "https://edugit.org/AlekSIS/official/AlekSIS-Core"
 documentation = "https://aleksis.org/AlekSIS-Core/docs/html/"
 keywords = ["SIS", "education", "school", "digitisation", "school apps"]
 classifiers = [
-    "Development Status :: 4 - Beta",
+    "Development Status :: 5 - Production/Stable",
     "Environment :: Web Environment",
     "Framework :: Django :: 3.0",
     "Intended Audience :: Developers",
@@ -52,12 +60,12 @@ django-any-js = "^1.1"
 django-debug-toolbar = "^3.2"
 django-menu-generator-ng = "^1.2.3"
 django-tables2 = "^2.1"
-django-phonenumber-field = {version = "^5.2", extras = ["phonenumbers"]}
+django-phonenumber-field = {version = "^6.1", extras = ["phonenumbers"]}
 django-sass-processor = "1.0"
 libsass = "^0.21.0"
 colour = "^0.1.5"
 dynaconf = {version = "^3.1", extras = ["yaml", "toml", "ini"]}
-django-auth-ldap = { version = "^2.2", optional = true }
+django-auth-ldap = { version = "^4.0", optional = true }
 django-maintenance-mode = "^0.16.0"
 django-ipware = "^4.0"
 django-impersonate = "^1.4"
@@ -74,24 +82,24 @@ html2text = "^2020.0.0"
 django-ckeditor = "^6.0.0"
 django-js-reverse = "^0.9.1"
 calendarweek = "^0.5.0"
-Celery = {version="^5.0.0", extras=["django", "redis"]}
+Celery = {version="^5.2", extras=["django", "redis"]}
 django-celery-results = "^2.0.1"
 django-celery-beat = "^2.2.0"
 django-celery-email = "^3.0.0"
 django-jsonstore = "^0.5.0"
 django-polymorphic = "^3.0.0"
-django-colorfield = "^0.4.0"
-django-bleach = "^0.8.0"
+django-colorfield = "^0.6.0"
+django-bleach = "^1.0.0"
 django-guardian = "^2.2.0"
-rules = "^2.2"
+rules = "^3.0"
 django-cache-memoize = "^0.1.6"
-django-haystack = {version="3.0", allow-prereleases = true}
-celery-haystack-ng = "^0.20"
+django-haystack = "^3.1"
+celery-haystack-ng = "^2.0"
 django-dbbackup = "^3.3.0"
 spdx-license-list = "^0.5.0"
-license-expression = "^1.2"
-django-reversion = "^4.0.0"
-django-favicon-plus-reloaded = "^1.1.2"
+license-expression = "^21.6"
+django-reversion = "^5.0.0"
+django-favicon-plus-reloaded = "^1.1.5"
 django-health-check = "^3.12.1"
 psutil = "^5.7.0"
 celery-progress = "^0.1.0"
@@ -99,29 +107,34 @@ django-cachalot = "^2.3.2"
 django-prometheus = "^2.1.0"
 django-model-utils = "^4.0.0"
 bs4 = "^0.0.1"
-django-allauth = "^0.45.0"
+django-invitations = "^1.9.3"
+django-cleavejs = "^0.1.0"
+django-allauth = "^0.47.0"
 django-uwsgi-ng = "^1.1.0"
 django-extensions = "^3.1.1"
-ipython = "^7.20.0"
-django-oauth-toolkit = "^1.5.0"
+ipython = "^8.0.0"
+django-oauth-toolkit = "^1.7.0"
 django-redis = "^5.0.0"
 django-storages = {version = "^1.11.1", optional = true}
 boto3 = {version = "^1.17.33", optional = true}
-django-cleanup = "^5.1.0"
+django-cleanup = "^6.0.0"
 djangorestframework = "^3.12.4"
 Whoosh = "^2.7.4"
-django-titofisto = "^0.1.0"
+django-titofisto = "^0.2.0"
 haystack-redis = "^0.0.1"
 python-gnupg = "^0.4.7"
 sentry-sdk = {version = "^1.4.3", optional = true}
+django-cte = "^1.1.5"
+pycountry = "^22.0.0"
+customidenticon = "^0.1.5"
 
 [tool.poetry.extras]
 ldap = ["django-auth-ldap"]
 s3 = ["boto3", "django-storages"]
-sentry = ["sentry"]
+sentry = ["sentry-sdk"]
 
 [tool.poetry.dev-dependencies]
-aleksis-builddeps = "*"
+aleksis-builddeps = "^6"
 uwsgi = "^2.0"
 
 [tool.poetry.scripts]
diff --git a/tox.ini b/tox.ini
index 6ba5d926ea520a574125f4a9717b93ba0020a2c3..4819e5b95946e3f021429e75c36fe13e7dd61d25 100644
--- a/tox.ini
+++ b/tox.ini
@@ -9,7 +9,7 @@ whitelist_externals = poetry
 skip_install = true
 envdir = {toxworkdir}/globalenv
 commands_pre =
-     poetry install
+     poetry install -E ldap
      poetry run aleksis-admin yarn install
      poetry run aleksis-admin collectstatic --no-input
 commands =