diff --git a/.gitignore b/.gitignore
index 70d2c1202e645fc31260a0fdd6bac90fdd25e15a..a30a3fa095b66e018388937f3ffb79eed4692301 100644
--- a/.gitignore
+++ b/.gitignore
@@ -74,3 +74,8 @@ htmlcov/
 maintenance_mode_state.txt
 media/
 package-lock.json
+
+# VSCode
+.vscode/
+.history/
+*.code-workspace
diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index 1b40df197dd94deb70e14407da9b5c0a6f7175ac..d36458971660c0b5ab354ad1e4b03068fb46ebea 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -1,123 +1,6 @@
-image: registry.edugit.org/teckids/team-sysadmin/docker-images/python-pimped:master
-
-stages:
-  - test
-  - build
-  - deploy
-
-variables:
-  GIT_SUBMODULE_STRATEGY: recursive
-  PIP_CACHE_DIR: "$CI_PROJECT_DIR/.cache/pip"
-  FF_NETWORK_PER_BUILD: "true"
-
-cache:
-  key:
-    files:
-      - poetry.lock
-      - pyproject.toml
-  paths:
-    - .cache/pip
-    - .tox
-
-test:
-  stage: test
-  services:
-    - name: selenium/standalone-firefox
-      alias: selenium
-  before_script:
-    - adduser --disabled-password --gecos "Test User" testuser
-    - chown -R testuser .
-  script:
-    - sudo apt update
-    - sudo apt install python3-ldap libldap2-dev libssl-dev libsasl2-dev python3.7-dev -y
-    - sudo -u testuser
-      env TEST_SELENIUM_HUB=http://selenium:4444/wd/hub
-          TEST_SELENIUM_BROWSERS=firefox
-          TEST_HOST=build
-      tox -e selenium -- --junitxml=.tox/junit.xml
-  artifacts:
-    paths:
-      - .tox/screenshots
-    reports:
-      junit: .tox/junit.xml
-
-lint:
-  stage: test
-  script:
-    - tox -e lint,security
-  allow_failure: true
-
-build_dist:
-  stage: build
-  script:
-    - tox -e build
-  artifacts:
-    paths:
-      - dist/
-
-pages:
-  stage: deploy
-  before_script:
-    - cp -r .tox/screenshots/firefox docs/screenshots
-  script:
-    - export LC_ALL=en_GB.utf8
-    - tox -e docs -- BUILDDIR=../public/docs
-  artifacts:
-    paths:
-    - public/
-  only:
-    - master
-
-build_docker:
-  stage: build
-  image:
-    name: gcr.io/kaniko-project/executor:v0.19.0
-    entrypoint: [""]
-  script:
-    - echo "{\"auths\":{\"$CI_REGISTRY\":{\"username\":\"$CI_REGISTRY_USER\",\"password\":\"$CI_REGISTRY_PASSWORD\"}}}" >/kaniko/.docker/config.json
-    - /kaniko/executor
-       --context $CI_PROJECT_DIR
-       --dockerfile $CI_PROJECT_DIR/Dockerfile
-       --destination $CI_REGISTRY_IMAGE:$CI_COMMIT_REF_NAME
-       --cache=true
-       --cleanup
-    - /kaniko/executor
-       --context $CI_PROJECT_DIR/docker/nginx
-       --dockerfile $CI_PROJECT_DIR/docker/nginx/Dockerfile
-       --destination $CI_REGISTRY_IMAGE/nginx:$CI_COMMIT_REF_NAME
-       --cache=true
-       --cleanup
-  only:
-    - master
-    - tags
-
-deploy_demo-master:
-  stage: deploy
-  environment:
-    name: demo/master
-    url: http://demo-master.aleksis.org
-  before_script:
-    - 'which ssh-agent || ( apt-get update -y && apt-get install openssh-client -y )'
-    - eval $(ssh-agent -s)
-    - echo "$SSH_PRIVATE_KEY" | tr -d '\r' | ssh-add -
-    - mkdir -p ~/.ssh
-    - chmod 700 ~/.ssh
-    - echo "$SSH_KNOWN_HOSTS" >~/.ssh/known_hosts
-    - chmod 644 ~/.ssh/known_hosts
-  script:
-    - grep -v "build:" docker-compose.yml | ssh root@demo-master.aleksis.org
-       env ALEKSIS_IMAGE_TAG=${CI_COMMIT_REF_NAME}
-       docker-compose
-        -p aleksis-${CI_ENVIRONMENT_SLUG}
-        -f /dev/stdin
-        pull
-    - grep -v "build:" docker-compose.yml | ssh root@demo-master.aleksis.org
-       env ALEKSIS_IMAGE_TAG=${CI_COMMIT_REF_NAME}
-           NGINX_HTTP_PORT=80
-           ALEKSIS_maintenance__debug=true
-       docker-compose
-        -p aleksis-${CI_ENVIRONMENT_SLUG}
-        -f /dev/stdin
-        up -d
-  only:
-    - master
+include:
+    - local: "/ci/general.yml"
+    - local: "/ci/test.yml"
+    - local: "/ci/build_dist.yml"
+    - local: "/ci/build_docker.yml"
+    - local: "/ci/deploy.yml"
diff --git a/.gitmodules b/.gitmodules
index 9c04a74362075f927208cfbca8332da1d405e755..a7fa3c3c15f5415169a0f2b0ce67876831caf5f7 100644
--- a/.gitmodules
+++ b/.gitmodules
@@ -2,10 +2,6 @@
 	path = apps/official/AlekSIS-App-Chronos
 	url = https://edugit.org/AlekSIS/Official/AlekSIS-App-Chronos
 	ignore = untracked
-[submodule "apps/official/AlekSIS-App-Exlibris"]
-	path = apps/official/AlekSIS-App-Exlibris
-	url = https://edugit.org/AlekSIS/Official/AlekSIS-App-Exlibris
-	ignore = untracked
 [submodule "apps/official/AlekSIS-App-DashboardFeeds"]
 	path = apps/official/AlekSIS-App-DashboardFeeds
 	url = https://edugit.org/AlekSIS/Official/AlekSIS-App-DashboardFeeds
@@ -18,3 +14,7 @@
 	path = apps/official/AlekSIS-App-Untis
 	url = https://edugit.org/AlekSIS/official/AlekSIS-App-Untis
 	ignore = untracked
+[submodule "apps/official/AlekSIS-App-Hjelp"]
+	path = apps/official/AlekSIS-App-Hjelp
+	url = https://edugit.org/AlekSIS/official/AlekSIS-App-Hjelp
+	ignore = untracked
diff --git a/CHANGELOG.rst b/CHANGELOG.rst
index cf7ceac6515f4b7d61b10afa664fc43f42a9c48f..5e3d62bcac1140ac60e3ce5a841c487a7912b8f4 100644
--- a/CHANGELOG.rst
+++ b/CHANGELOG.rst
@@ -1,6 +1,50 @@
 Changelog
 =========
 
+`2.0a2`_
+--------
+
+New features
+~~~~~~~~~~~~
+
+* Frontend-ased announcement management
+* Auto-create Person on User creation
+* Select primary group by pattern if unset
+* Shortcut to personal information page
+* Support for defining group types
+* Add description to Person
+* age_at method and age property to Person
+* Synchronise AlekSIS groups with Django groups
+* Add celery worker, celery-beat worker and celery broker to docker-compose setup
+* Global search
+* License information page
+* Roles and permissions
+* User preferences
+* Additional fields for people per group
+* Support global permission flags by LDAP group
+* Persistent announcements
+* Custom menu entries (e.g. in footer)
+* New logo for AlekSIS
+* Two factor authentication with Yubikey, OTP or SMS
+* Devs: Add ExtensibleModel to allow apps to add fields, properties
+* Devs: Support multiple recipient object for one announcement
+
+Minor changes
+~~~~~~~~~~~~~
+
+* Make short_name for group optional
+* Generalised live loading of widgets for dashboard
+* Devs: Add some CSS helper classes for colours
+* Devs: Mandate use of AlekSIS base model
+* Devs: Drop import_ref field(s); apps shold now define their own reference fields
+
+Bug fixes
+~~~~~~~~~
+
+* DateTimeField Announcement.valid_from received a naive datetime
+* Enable SASS processor in production
+* Fix too short fields
+* Load select2 locally
 
 `2.0a1`_
 --------
@@ -12,8 +56,8 @@ New features
 * Dashboard
 * Notifications via SMS (Twilio), Email or on the dashboard
 * Admin interface
+* Turn into installable, progressive web app
 * Devs: Background Tasks with Celery
-* Turn into installable PWA
 
 Minor changes
 ~~~~~~~~~~~~~
@@ -79,3 +123,4 @@ _`1.0a1`: https://edugit.org/AlekSIS/Official/AlekSIS/-/tags/1.0a1
 _`1.0a2`: https://edugit.org/AlekSIS/Official/AlekSIS/-/tags/1.0a2
 _`1.0a4`: https://edugit.org/AlekSIS/Official/AlekSIS/-/tags/1.0a4
 _`2.0a1`: https://edugit.org/AlekSIS/Official/AlekSIS/-/tags/2.0a1
+_`2.0a2`: https://edugit.org/AlekSIS/Official/AlekSIS/-/tags/2.0a2
diff --git a/CONTRIBUTING.rst b/CONTRIBUTING.rst
index 2c24e62bb4d1acd60579eb0064c0a79c08748d7f..be12afc1552dc2b6d6cb398bc2a456392ef21b9d 100644
--- a/CONTRIBUTING.rst
+++ b/CONTRIBUTING.rst
@@ -48,7 +48,7 @@ Working with the Git repository
 
 The Git repository shall be used as a historic documentation of development
 and as change management. It is important that the Git commit history
-describes waht was changed, by whom and why.
+describes what was changed, by whom and why.
 
 Help and information on Git for beginners are available in the `Git guide`_
 
diff --git a/README.rst b/README.rst
index 5c08b69d1ac8d24ab74c7cdbf74a9e4b8e161d6e..e5d0251f38d496da608ba3c99526b31678378caf 100644
--- a/README.rst
+++ b/README.rst
@@ -4,31 +4,72 @@ AlekSIS — All-libre extensible kit for school information systems
 Warning
 -------
 
-**This is a preview version of AlekSIS. Do not use with sensitive data. Especially, do not grant access to students yet.**
-
+**This is an alpha version of AlekSIS, the free school information system.
+The AlekSIS team is looking for schools who want to help shape the 2.0
+final release and supports interested schools in operating AlekSIS.**
 
 What AlekSIS is
 ----------------
 
-AlekSIS is a web-based school information system (SIS) which can be used to
+`AlekSIS`_ is a web-based school information system (SIS) which can be used to
 manage and/or publish organisational subjects of educational institutions.
 
-It was originally developed together with Städt. Leibniz-Gymnasium Remscheid
-as a proprietary product. Five years after the school stole the original
-code base, as a complete re-implementation as well-designed, free and open
-source software, BiscuIT-ng was started. In the meantime, students from the
-Katharineum in Lübeck implemented School-Apps with the same goals and tools.
-In 2020, BiscuIT-ng and School-Apps were combined into AlekSIS.
+Formerly two separate projects (BiscuIT and SchoolApps), developed by
+`Teckids e.V.`_ and a team of students at `Katharineum zu Lübeck`_, they
+were merged into the AlekSIS project in 2020.
 
 AlekSIS is a platform based on Django, that provides central funstions
 and data structures that can be used by apps that are developed and provided
-seperately. The core can interact closely with the Debian Edu / Skolelinux
-system.
+seperately. The AlekSIS team also maintains a set of official apps which
+make AlekSIS a fully-featured software solutions for the information
+management needs of schools.
+
+By design, the platform can be used by schools to write their own apps for
+specific needs they face, also in coding classes. Students are empowered to
+create real-world applications that bring direct value to their environment.
+
+AlekSIS is part of the `schul-frei`_ project as a component in sustainable
+educational networks.
 
 Core features
 --------------
 
-TBA.
+* For users:
+
+ * Custom menu entries (e.g. in footer)
+ * Global preferences
+ * Group types
+ * Manage announcements
+ * Manage groups
+ * Manage persons
+ * Notifications via SMS email or dashboard
+ * Rules and permissions for users, objects and pages
+ * Two factor authentication via Yubikey, OTP or SMS
+ * User preferences
+
+* For admins
+
+ * Asynchronous tasks with celery
+ * Authentication via LDAP
+ * Automatic backup of database, static and media files
+
+Official apps
+-------------
+
++--------------------------------------+---------------------------------------------------------------------------------------------+
+| App name                             | Purpose                                                                                     |
++======================================+=============================================================================================+
+| `AlekSIS-App-Chronos`_               | The Chronos app provides functionality for digital timetables.                              |
++--------------------------------------+---------------------------------------------------------------------------------------------+
+| `AlekSIS-App-DashboardFeeds`_        | The DashboardFeeds app provides functionality to add RSS or Atom feeds to dashboard         |
++--------------------------------------+---------------------------------------------------------------------------------------------+
+| `AlekSIS-App-Hjelp`_                 | The Hjelp app provides functionality for aiding users.                                      |
++--------------------------------------+---------------------------------------------------------------------------------------------+
+| `AlekSIS-App-LDAP`_                  | The LDAP app provides functionality to import users and groups from LDAP                    |
++--------------------------------------+---------------------------------------------------------------------------------------------+
+| `AlekSIS-App-Untis`_                 | This app provides import and export functions to interact with Untis, a timetable software. |
++--------------------------------------+---------------------------------------------------------------------------------------------+
+
 
 Licence
 -------
@@ -40,8 +81,8 @@ Licence
   Copyright © 2018, 2019, 2020 Julian Leucker <leuckeju@katharineum.de>
   Copyright © 2018, 2019, 2020 Hangzhi Yu <yuha@katharineum.de>
   Copyright © 2019, 2020 Dominik George <dominik.george@teckids.org>
-  Copyright © 2019, 2020 mirabilos <thorsten.glaser@teckids.org>
   Copyright © 2019, 2020 Tom Teichler <tom.teichler@teckids.org>
+  Copyright © 2019 mirabilos <thorsten.glaser@teckids.org>
 
   Licenced under the EUPL, version 1.2 or later
 
@@ -50,5 +91,13 @@ 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://edugit.org/AlekSIS/Official/AlekSIS
+.. _AlekSIS: https://aleksis.org/
+.. _Teckids e.V.: https://www.teckids.org/
+.. _Katharineum zu Lübeck: https://www.katharineum.de/
 .. _European Union Public Licence: https://eupl.eu/
+.. _schul-frei: https://schul-frei.org/
+.. _AlekSIS-App-Chronos: https://edugit.org/AlekSIS/official/AlekSIS-App-Chronos
+.. _AlekSIS-App-DashboardFeeds: https://edugit.org/AlekSIS/official/AlekSIS-App-DashboardFeeds
+.. _AlekSIS-App-Hjelp: https://edugit.org/AlekSIS/official/AlekSIS-App-Hjelp
+.. _AlekSIS-App-LDAP: https://edugit.org/AlekSIS/official/AlekSIS-App-LDAP
+.. _AlekSIS-App-Untis: https://edugit.org/AlekSIS/official/AlekSIS-App-Untis
diff --git a/aleksis/core/__init__.py b/aleksis/core/__init__.py
index 587b829c611913af41c69f2734a48235956ff787..21a49f0785f3977d9a265059a32ec957c9e1eb3c 100644
--- a/aleksis/core/__init__.py
+++ b/aleksis/core/__init__.py
@@ -1,7 +1,5 @@
 import pkg_resources
 
-from django.utils.translation import gettext_lazy as _
-
 try:
     from .celery import app as celery_app
 except ModuleNotFoundError:
diff --git a/aleksis/core/admin.py b/aleksis/core/admin.py
index 29a48d1517db6661fafd7ddd81bc0f975faa2d5a..e35910f597983ea8cfaa957cd475e607c490dfa0 100644
--- a/aleksis/core/admin.py
+++ b/aleksis/core/admin.py
@@ -1,31 +1,32 @@
+# noqa
+
 from django.contrib import admin
 
+from reversion.admin import VersionAdmin
+
+from .mixins import BaseModelAdmin
 from .models import (
-    Group,
-    Person,
-    School,
-    SchoolTerm,
     Activity,
-    Notification,
     Announcement,
     AnnouncementRecipient,
     CustomMenuItem,
+    Group,
+    Notification,
+    Person,
 )
 
-admin.site.register(Person)
-admin.site.register(Group)
-admin.site.register(School)
-admin.site.register(SchoolTerm)
-admin.site.register(Activity)
-admin.site.register(Notification)
-admin.site.register(CustomMenuItem)
+admin.site.register(Person, VersionAdmin)
+admin.site.register(Group, VersionAdmin)
+admin.site.register(Activity, VersionAdmin)
+admin.site.register(Notification, VersionAdmin)
+admin.site.register(CustomMenuItem, VersionAdmin)
 
 
 class AnnouncementRecipientInline(admin.StackedInline):
     model = AnnouncementRecipient
 
 
-class AnnouncementAdmin(admin.ModelAdmin):
+class AnnouncementAdmin(BaseModelAdmin, VersionAdmin):
     inlines = [
         AnnouncementRecipientInline,
     ]
diff --git a/aleksis/core/apps.py b/aleksis/core/apps.py
index 648d063a378f30fddbbece6474c3b84a5a65bed6..aeda609326941428e483e05cabd6e5412b2832f7 100644
--- a/aleksis/core/apps.py
+++ b/aleksis/core/apps.py
@@ -1,12 +1,19 @@
 from typing import Any, List, Optional, Tuple
 
 import django.apps
-from django.contrib.auth.signals import user_logged_in
 from django.http import HttpRequest
+from django.utils.module_loading import autodiscover_modules
 
-from .signals import clean_scss
+from dynamic_preferences.registries import preference_models
+
+from .registries import (
+    group_preferences_registry,
+    person_preferences_registry,
+    site_preferences_registry,
+)
 from .util.apps import AppConfig
 from .util.core_helpers import has_person
+from .util.sass_helpers import clean_scss
 
 
 class CoreConfig(AppConfig):
@@ -17,18 +24,54 @@ class CoreConfig(AppConfig):
         "Repository": "https://edugit.org/AlekSIS/official/AlekSIS/",
     }
     licence = "EUPL-1.2+"
-    copyright = (
+    copyright_info = (
         ([2017, 2018, 2019, 2020], "Jonathan Weth", "wethjo@katharineum.de"),
         ([2017, 2018, 2019], "Frank Poetzsch-Heffter", "p-h@katharineum.de"),
         ([2018, 2019, 2020], "Julian Leucker", "leuckeju@katharineum.de"),
         ([2018, 2019, 2020], "Hangzhi Yu", "yuha@katharineum.de"),
         ([2019, 2020], "Dominik George", "dominik.george@teckids.org"),
-        ([2019, 2020], "mirabilos", "thorsten.glaser@teckids.org"),
         ([2019, 2020], "Tom Teichler", "tom.teichler@teckids.org"),
+        ([2019], "mirabilos", "thorsten.glaser@teckids.org"),
     )
 
-    def config_updated(self, *args, **kwargs) -> None:
-        clean_scss()
+    def ready(self):
+        super().ready()
+
+        # Autodiscover various modules defined by AlekSIS
+        autodiscover_modules("form_extensions", "model_extensions", "checks")
+
+        sitepreferencemodel = self.get_model("SitePreferenceModel")
+        personpreferencemodel = self.get_model("PersonPreferenceModel")
+        grouppreferencemodel = self.get_model("GroupPreferenceModel")
+
+        preference_models.register(sitepreferencemodel, site_preferences_registry)
+        preference_models.register(personpreferencemodel, person_preferences_registry)
+        preference_models.register(grouppreferencemodel, group_preferences_registry)
+
+    def preference_updated(
+        self,
+        sender: Any,
+        section: Optional[str] = None,
+        name: Optional[str] = None,
+        old_value: Optional[Any] = None,
+        new_value: Optional[Any] = None,
+        **kwargs,
+    ) -> None:
+        if section == "theme":
+            if name in ("primary", "secondary"):
+                clean_scss()
+            elif name in ("favicon", "pwa_icon"):
+                from favicon.models import Favicon  # noqa
+
+                is_favicon = name == "favicon"
+
+                if new_value:
+                    Favicon.on_site.update_or_create(
+                        title=name,
+                        defaults={"isFavicon": name == "favicon", "faviconImage": new_value,},
+                    )
+                else:
+                    Favicon.on_site.filter(title=name, isFavicon=is_favicon).delete()
 
     def post_migrate(
         self,
@@ -42,7 +85,7 @@ class CoreConfig(AppConfig):
     ) -> None:
         super().post_migrate(app_config, verbosity, interactive, using, plan, apps)
 
-        # Ensure presence of a OTP YubiKey default config
+        # Ensure presence of an OTP YubiKey default config
         apps.get_model("otp_yubikey", "ValidationService").objects.using(using).update_or_create(
             name="default", defaults={"use_ssl": True, "param_sl": "", "param_timeout": ""}
         )
diff --git a/aleksis/core/celery.py b/aleksis/core/celery.py
index 2f4dce954576a5d688311c466bc2b9fb4ad4e151..27a67c539babfc61701cc9521b42187ebebc5787 100644
--- a/aleksis/core/celery.py
+++ b/aleksis/core/celery.py
@@ -1,8 +1,9 @@
 import os
+
 from celery import Celery
 
 os.environ.setdefault("DJANGO_SETTINGS_MODULE", "aleksis.core.settings")
 
 app = Celery("aleksis")  # noqa
-app.config_from_object('django.conf:settings', namespace='CELERY')
+app.config_from_object("django.conf:settings", namespace="CELERY")
 app.autodiscover_tasks()
diff --git a/aleksis/core/checks.py b/aleksis/core/checks.py
index e52290eb3a48ae4d7e7efe90fbec61d8f86ed365..db3199b6081d77e9bd8e2c559db07f7924484599 100644
--- a/aleksis/core/checks.py
+++ b/aleksis/core/checks.py
@@ -11,8 +11,7 @@ from .util.apps import AppConfig
 def check_app_configs_base_class(
     app_configs: Optional[django.apps.registry.Apps] = None, **kwargs
 ) -> list:
-    """ Checks whether all apps derive from AlekSIS's base app config """
-
+    """Check whether all apps derive from AlekSIS's base app config."""
     results = []
 
     if app_configs is None:
@@ -22,8 +21,12 @@ def check_app_configs_base_class(
         if not isinstance(app_config, AppConfig):
             results.append(
                 Warning(
-                    "App config %s does not derive from aleksis.core.util.apps.AppConfig." % app_config.name,
-                    hint="Ensure the app uses the correct base class for all registry functionality to work.",
+                    f"App config {app_config.name} does not derive"
+                    "from aleksis.core.util.apps.AppConfig.",
+                    hint=(
+                        "Ensure the app uses the correct base class for all"
+                        "registry functionality to work."
+                    ),
                     obj=app_config,
                     id="aleksis.core.W001",
                 )
@@ -36,8 +39,7 @@ def check_app_configs_base_class(
 def check_app_models_base_class(
     app_configs: Optional[django.apps.registry.Apps] = None, **kwargs
 ) -> list:
-    """ Checks whether all app models derive from AlekSIS's base ExtensibleModel """
-
+    """Check whether all app models derive from AlekSIS's base ExtensibleModel."""
     results = []
 
     if app_configs is None:
@@ -48,8 +50,13 @@ def check_app_models_base_class(
             if ExtensibleModel not in model.__mro__ and PureDjangoModel not in model.__mro__:
                 results.append(
                     Warning(
-                        "Model %s in app config %s does not derive from aleksis.core.mixins.ExtensibleModel." % (model._meta.object_name, app_config.name),
-                        hint="Ensure all models in AlekSIS use ExtensibleModel as base. If your deviation is intentional, you can add the PureDjangoModel mixin instead to silence this warning.",
+                        f"Model {model._meta.object_name} in app config {app_config.name} does"
+                        "not derive from aleksis.core.mixins.ExtensibleModel.",
+                        hint=(
+                            "Ensure all models in AlekSIS use ExtensibleModel as base."
+                            "If your deviation is intentional, you can add the PureDjangoModel"
+                            "mixin instead to silence this warning."
+                        ),
                         obj=model,
                         id="aleksis.core.W002",
                     )
diff --git a/aleksis/core/decorators.py b/aleksis/core/decorators.py
deleted file mode 100644
index 1c884a123c8a4b2a60e4616e4c928064e4a7123e..0000000000000000000000000000000000000000
--- a/aleksis/core/decorators.py
+++ /dev/null
@@ -1,17 +0,0 @@
-from typing import Callable
-
-from django.contrib.auth.decorators import login_required, user_passes_test
-
-from .util.core_helpers import has_person
-
-
-def admin_required(function: Callable = None) -> Callable:
-    actual_decorator = user_passes_test(lambda u: u.is_active and u.is_superuser)
-    return actual_decorator(function)
-
-
-def person_required(function: Callable = None) -> Callable:
-    """ Requires a logged-in user which is linked to a person. """
-
-    actual_decorator = user_passes_test(has_person)
-    return actual_decorator(login_required(function))
diff --git a/aleksis/core/filters.py b/aleksis/core/filters.py
new file mode 100644
index 0000000000000000000000000000000000000000..c0159567101fe2620deb0f116019ca68e4cf9652
--- /dev/null
+++ b/aleksis/core/filters.py
@@ -0,0 +1,20 @@
+from django_filters import CharFilter, FilterSet
+from material import Layout, Row
+
+
+class GroupFilter(FilterSet):
+    name = CharFilter(lookup_expr="icontains")
+    short_name = CharFilter(lookup_expr="icontains")
+
+    def __init__(self, *args, **kwargs):
+        super().__init__(*args, **kwargs)
+        self.form.layout = Layout(Row("name", "short_name"))
+
+
+class PersonFilter(FilterSet):
+    first_name = CharFilter(lookup_expr="icontains")
+    last_name = CharFilter(lookup_expr="icontains")
+
+    def __init__(self, *args, **kwargs):
+        super().__init__(*args, **kwargs)
+        self.form.layout = Layout(Row("first_name", "last_name"))
diff --git a/aleksis/core/forms.py b/aleksis/core/forms.py
index fd331592f24d3527c3edfb2726cce25ec83095fe..208cbff93f76b7d44451550b73d3b5659ba4aa59 100644
--- a/aleksis/core/forms.py
+++ b/aleksis/core/forms.py
@@ -1,21 +1,26 @@
-from datetime import time, datetime
-from typing import Optional
+from datetime import datetime, time
 
 from django import forms
 from django.contrib.auth import get_user_model
-from django.contrib.contenttypes.models import ContentType
 from django.core.exceptions import ValidationError
-from django.utils import timezone
 from django.utils.translation import gettext_lazy as _
 
 from django_select2.forms import ModelSelect2MultipleWidget, Select2Widget
-from material import Layout, Fieldset, Row
-
-from .mixins import ExtensibleForm
-from .models import Group, Person, School, SchoolTerm, Announcement, AnnouncementRecipient
+from dynamic_preferences.forms import PreferenceForm
+from material import Fieldset, Layout, Row
+
+from .mixins import ExtensibleForm, SchoolTermRelatedExtensibleForm
+from .models import AdditionalField, Announcement, Group, GroupType, Person, SchoolTerm
+from .registries import (
+    group_preferences_registry,
+    person_preferences_registry,
+    site_preferences_registry,
+)
 
 
 class PersonAccountForm(forms.ModelForm):
+    """Form to assign user accounts to persons in the frontend."""
+
     class Meta:
         model = Person
         fields = ["last_name", "first_name", "user"]
@@ -25,22 +30,27 @@ class PersonAccountForm(forms.ModelForm):
 
     def __init__(self, *args, **kwargs):
         super().__init__(*args, **kwargs)
+
+        # Fields displayed only for informational purposes
         self.fields["first_name"].disabled = True
         self.fields["last_name"].disabled = True
 
     def clean(self) -> None:
-        User = get_user_model()
+        user = get_user_model()
 
         if self.cleaned_data.get("new_user", None):
             if self.cleaned_data.get("user", None):
+                # The user selected both an existing user and provided a name to create a new one
                 self.add_error(
                     "new_user",
                     _("You cannot set a new username when also selecting an existing user."),
                 )
-            elif User.objects.filter(username=self.cleaned_data["new_user"]).exists():
+            elif user.objects.filter(username=self.cleaned_data["new_user"]).exists():
+                # The user tried to create a new user with the name of an existing user
                 self.add_error("new_user", _("This username is already in use."))
             else:
-                new_user_obj = User.objects.create_user(
+                # Create new User object and assign to form field for existing user
+                new_user_obj = user.objects.create_user(
                     self.cleaned_data["new_user"],
                     self.instance.email,
                     first_name=self.instance.first_name,
@@ -50,12 +60,15 @@ class PersonAccountForm(forms.ModelForm):
                 self.cleaned_data["user"] = new_user_obj
 
 
+# Formset for batch-processing of assignments of users to persons
 PersonsAccountsFormSet = forms.modelformset_factory(
     Person, form=PersonAccountForm, max_num=0, extra=0
 )
 
 
 class EditPersonForm(ExtensibleForm):
+    """Form to edit an existing person object in the frontend."""
+
     layout = Layout(
         Fieldset(
             _("Base data"),
@@ -104,31 +117,18 @@ class EditPersonForm(ExtensibleForm):
     )
 
     def clean(self) -> None:
-        User = get_user_model()
+        # Use code implemented in dedicated form to verify user selection
+        return PersonAccountForm.clean(self)
 
-        if self.cleaned_data.get("new_user", None):
-            if self.cleaned_data.get("user", None):
-                self.add_error(
-                    "new_user",
-                    _("You cannot set a new username when also selecting an existing user."),
-                )
-            elif User.objects.filter(username=self.cleaned_data["new_user"]).exists():
-                self.add_error("new_user", _("This username is already in use."))
-            else:
-                new_user_obj = User.objects.create_user(
-                    self.cleaned_data["new_user"],
-                    self.instance.email,
-                    first_name=self.instance.first_name,
-                    last_name=self.instance.last_name,
-                )
-
-                self.cleaned_data["user"] = new_user_obj
 
+class EditGroupForm(SchoolTermRelatedExtensibleForm):
+    """Form to edit an existing group in the frontend."""
 
-class EditGroupForm(ExtensibleForm):
     layout = Layout(
-        Fieldset(_("Common data"), "name", "short_name"),
+        Fieldset(_("School term"), "school_term"),
+        Fieldset(_("Common data"), "name", "short_name", "group_type"),
         Fieldset(_("Persons"), "members", "owners", "parent_groups"),
+        Fieldset(_("Additional data"), "additional_fields"),
     )
 
     class Meta:
@@ -152,29 +152,13 @@ class EditGroupForm(ExtensibleForm):
             "parent_groups": ModelSelect2MultipleWidget(
                 search_fields=["name__icontains", "short_name__icontains"]
             ),
+            "additional_fields": ModelSelect2MultipleWidget(search_fields=["title__icontains",]),
         }
 
 
-class EditSchoolForm(ExtensibleForm):
-    layout = Layout(
-        Fieldset(_("School name"), "name", "name_official"),
-        Fieldset(_("School logo"), Row("logo", "logo_cropping")),
-    )
-
-    class Meta:
-        model = School
-        fields = ["name", "name_official", "logo", "logo_cropping"]
-
-
-class EditTermForm(ExtensibleForm):
-    layout = Layout("caption", Row("date_start", "date_end"))
-
-    class Meta:
-        model = SchoolTerm
-        fields = ["caption", "date_start", "date_end"]
-
-
 class AnnouncementForm(ExtensibleForm):
+    """Form to create or edit an announcement in the frontend."""
+
     valid_from = forms.DateTimeField(required=False)
     valid_until = forms.DateTimeField(required=False)
 
@@ -187,7 +171,7 @@ class AnnouncementForm(ExtensibleForm):
     persons = forms.ModelMultipleChoiceField(
         Person.objects.all(), label=_("Persons"), required=False
     )
-    groups = forms.ModelMultipleChoiceField(Group.objects.all(), label=_("Groups"), required=False)
+    groups = forms.ModelMultipleChoiceField(queryset=None, label=_("Groups"), required=False)
 
     layout = Layout(
         Fieldset(
@@ -200,6 +184,7 @@ class AnnouncementForm(ExtensibleForm):
 
     def __init__(self, *args, **kwargs):
         if "instance" not in kwargs:
+            # Default to today and whole day for new announcements
             kwargs["initial"] = {
                 "valid_from_date": datetime.now(),
                 "valid_from_time": time(0, 0),
@@ -218,20 +203,19 @@ class AnnouncementForm(ExtensibleForm):
                 "groups": announcement.get_recipients_for_model(Group),
                 "persons": announcement.get_recipients_for_model(Person),
             }
+
         super().__init__(*args, **kwargs)
 
+        self.fields["groups"].queryset = Group.objects.for_current_school_term_or_all()
+
     def clean(self):
         data = super().clean()
 
-        # Check date and time
-        from_date = data["valid_from_date"]
-        from_time = data["valid_from_time"]
-        until_date = data["valid_until_date"]
-        until_time = data["valid_until_time"]
-
-        valid_from = datetime.combine(from_date, from_time)
-        valid_until = datetime.combine(until_date, until_time)
+        # Combine date and time fields into datetime objects
+        valid_from = datetime.combine(data["valid_from_date"], data["valid_from_time"])
+        valid_until = datetime.combine(data["valid_until_date"], data["valid_until_time"])
 
+        # Sanity check validity range
         if valid_until < datetime.now():
             raise ValidationError(
                 _("You are not allowed to create announcements which are only valid in the past.")
@@ -241,38 +225,89 @@ class AnnouncementForm(ExtensibleForm):
                 _("The from date and time must be earlier then the until date and time.")
             )
 
+        # Inject real time data if all went well
         data["valid_from"] = valid_from
         data["valid_until"] = valid_until
 
-        # Check recipients
+        # Ensure at least one group or one person is set as recipient
         if "groups" not in data and "persons" not in data:
             raise ValidationError(_("You need at least one recipient."))
 
-        recipients = []
-        recipients += data.get("groups", [])
-        recipients += data.get("persons", [])
-
-        data["recipients"] = recipients
+        # Unwrap all recipients into single user objects and generate final list
+        data["recipients"] = []
+        data["recipients"] += data.get("groups", [])
+        data["recipients"] += data.get("persons", [])
 
         return data
 
     def save(self, _=False):
-        # Save announcement
-        a = self.instance if self.instance is not None else Announcement()
-        a.valid_from = self.cleaned_data["valid_from"]
-        a.valid_until = self.cleaned_data["valid_until"]
-        a.title = self.cleaned_data["title"]
-        a.description = self.cleaned_data["description"]
-        a.save()
+        # Save announcement, respecting data injected in clean()
+        if self.instance is None:
+            self.instance = Announcement()
+        self.instance.valid_from = self.cleaned_data["valid_from"]
+        self.instance.valid_until = self.cleaned_data["valid_until"]
+        self.instance.title = self.cleaned_data["title"]
+        self.instance.description = self.cleaned_data["description"]
+        self.instance.save()
 
         # Save recipients
-        a.recipients.all().delete()
+        self.instance.recipients.all().delete()
         for recipient in self.cleaned_data["recipients"]:
-            a.recipients.create(recipient=recipient)
-        a.save()
+            self.instance.recipients.create(recipient=recipient)
+        self.instance.save()
 
-        return a
+        return self.instance
 
     class Meta:
         model = Announcement
         exclude = []
+
+
+class ChildGroupsForm(forms.Form):
+    """Inline form for group editing to select child groups."""
+
+    child_groups = forms.ModelMultipleChoiceField(queryset=Group.objects.all())
+
+
+class SitePreferenceForm(PreferenceForm):
+    """Form to edit site preferences."""
+
+    registry = site_preferences_registry
+
+
+class PersonPreferenceForm(PreferenceForm):
+    """Form to edit preferences valid for one person."""
+
+    registry = person_preferences_registry
+
+
+class GroupPreferenceForm(PreferenceForm):
+    """Form to edit preferences valid for members of a group."""
+
+    registry = group_preferences_registry
+
+
+class EditAdditionalFieldForm(forms.ModelForm):
+    """Form to manage additional fields."""
+
+    class Meta:
+        model = AdditionalField
+        exclude = []
+
+
+class EditGroupTypeForm(forms.ModelForm):
+    """Form to manage group types."""
+
+    class Meta:
+        model = GroupType
+        exclude = []
+
+
+class SchoolTermForm(ExtensibleForm):
+    """Form for managing school years."""
+
+    layout = Layout("name", Row("date_start", "date_end"))
+
+    class Meta:
+        model = SchoolTerm
+        exclude = []
diff --git a/aleksis/core/locale/ar/LC_MESSAGES/django.po b/aleksis/core/locale/ar/LC_MESSAGES/django.po
index 8beda63fdc77717855d4c52f3ede65395fa9fcf9..f21f799bef0ace8309d458095e83ab705354ff96 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: 2020-03-30 09:18+0000\n"
+"POT-Creation-Date: 2020-05-04 15:39+0200\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"
@@ -16,70 +16,91 @@ msgstr ""
 "MIME-Version: 1.0\n"
 "Content-Type: text/plain; charset=UTF-8\n"
 "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"
+"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"
 
-#: forms.py:38 forms.py:93
+#: forms.py:46
 msgid "You cannot set a new username when also selecting an existing user."
 msgstr ""
 
-#: forms.py:41 forms.py:96
+#: forms.py:50
 msgid "This username is already in use."
 msgstr ""
 
+#: forms.py:74
+msgid "Base data"
+msgstr ""
+
+#: forms.py:80
+msgid "Address"
+msgstr ""
+
+#: forms.py:81
+msgid "Contact data"
+msgstr ""
+
 #: forms.py:83
+msgid "Advanced personal data"
+msgstr ""
+
+#: forms.py:116
 msgid "New user"
 msgstr ""
 
-#: forms.py:83
+#: forms.py:116
 msgid "Create a new account"
 msgstr ""
 
-#: forms.py:149 forms.py:152
-msgid "Date"
+#: forms.py:128
+msgid "Common data"
 msgstr ""
 
-#: forms.py:150 forms.py:153
-msgid "Time"
+#: forms.py:129 forms.py:169 menus.py:141 models.py:54
+#: templates/core/persons.html:8 templates/core/persons.html:9
+msgid "Persons"
 msgstr ""
 
-#: forms.py:155 menus.py:127 models.py:95 templates/core/persons.html:8
-#: templates/core/persons.html:9
-msgid "Persons"
+#: forms.py:162 forms.py:165 models.py:31
+msgid "Date"
 msgstr ""
 
-#: forms.py:156 menus.py:133 models.py:232 templates/core/groups.html:8
-#: templates/core/groups.html:9 templates/core/person_full.html:79
+#: forms.py:163 forms.py:166 models.py:39
+msgid "Time"
+msgstr ""
+
+#: forms.py:171 menus.py:149 models.py:253 templates/core/groups.html:8
+#: templates/core/groups.html:9 templates/core/person_full.html:106
 msgid "Groups"
 msgstr ""
 
-#: forms.py:160
+#: forms.py:175
 msgid "From when until when should the announcement be displayed?"
 msgstr ""
 
-#: forms.py:163
+#: forms.py:178
 msgid "Who should see the announcement?"
 msgstr ""
 
-#: forms.py:164
+#: forms.py:179
 msgid "Write your announcement:"
 msgstr ""
 
-#: forms.py:203
-msgid "You are not allowed to create announcements which are only valid in the past."
+#: forms.py:216
+msgid ""
+"You are not allowed to create announcements which are only valid in the past."
 msgstr ""
 
-#: forms.py:207
+#: forms.py:220
 msgid "The from date and time must be earlier then the until date and time."
 msgstr ""
 
-#: forms.py:215
+#: forms.py:229
 msgid "You need at least one recipient."
 msgstr ""
 
-#: menus.py:7 templates/registration/login.html:21
-#: templates/two_factor/core/login.html:6
+#: menus.py:7 templates/two_factor/core/login.html:6
 #: templates/two_factor/core/login.html:10
-#: templates/two_factor/core/login.html:64
+#: templates/two_factor/core/login.html:73
 msgid "Login"
 msgstr ""
 
@@ -100,415 +121,628 @@ msgid "Logout"
 msgstr ""
 
 #: menus.py:41
-msgid "Two factor auth"
+msgid "2FA"
 msgstr ""
 
-#: menus.py:52
+#: menus.py:47
+msgid "Me"
+msgstr ""
+
+#: menus.py:56 templates/dynamic_preferences/form.html:5
+msgid "Preferences"
+msgstr ""
+
+#: menus.py:67
 msgid "Admin"
 msgstr ""
 
-#: menus.py:61 models.py:395 templates/core/announcement/list.html:7
+#: menus.py:75 models.py:487 templates/core/announcement/list.html:7
 #: templates/core/announcement/list.html:8
 msgid "Announcements"
 msgstr ""
 
-#: menus.py:70 templates/core/data_management.html:6
+#: menus.py:86 templates/core/data_management.html:6
 #: templates/core/data_management.html:7
 msgid "Data management"
 msgstr ""
 
-#: menus.py:79 templates/core/system_status.html:5
+#: menus.py:94 templates/core/system_status.html:5
 #: templates/core/system_status.html:7
 msgid "System status"
 msgstr ""
 
-#: menus.py:88
+#: menus.py:105
 msgid "Impersonation"
 msgstr ""
 
-#: menus.py:97
-msgid "Manage school"
+#: menus.py:113
+msgid "Configuration"
 msgstr ""
 
-#: menus.py:106
+#: menus.py:124
 msgid "Backend Admin"
 msgstr ""
 
-#: menus.py:117
+#: menus.py:132
 msgid "People"
 msgstr ""
 
-#: menus.py:139
+#: menus.py:157
 msgid "Persons and accounts"
 msgstr ""
 
-#: menus.py:152
-msgid "Edit school information"
+#: menus.py:168
+msgid "Groups and child groups"
 msgstr ""
 
-#: menus.py:153 templates/core/edit_schoolterm.html:8
-#: templates/core/edit_schoolterm.html:9
-msgid "Edit school term"
+#: menus.py:183 templates/core/groups_child_groups.html:7
+#: templates/core/groups_child_groups.html:9
+msgid "Assign child groups to groups"
 msgstr ""
 
-#: models.py:31 models.py:517
-msgid "Name"
+#: models.py:29
+msgid "Boolean (Yes/No)"
+msgstr ""
+
+#: models.py:30
+msgid "Text (one line)"
+msgstr ""
+
+#: models.py:32
+msgid "Date and time"
 msgstr ""
 
 #: models.py:33
-msgid "Official name"
+msgid "Decimal number"
+msgstr ""
+
+#: models.py:34 models.py:95
+msgid "E-mail address"
 msgstr ""
 
 #: models.py:35
-msgid "Official name of the school, e.g. as given by supervisory authority"
+msgid "Integer"
 msgstr ""
 
-#: models.py:38
-msgid "School logo"
+#: models.py:36
+msgid "IP address"
 msgstr ""
 
-#: models.py:51
-msgid "School"
+#: models.py:37
+msgid "Boolean or empty (Yes/No/Neither)"
 msgstr ""
 
-#: models.py:52
-msgid "Schools"
+#: models.py:38
+msgid "Text (multi-line)"
 msgstr ""
 
-#: models.py:60
-msgid "Visible caption of the term"
+#: models.py:40
+msgid "URL / Link"
 msgstr ""
 
-#: models.py:62
-msgid "Effective start date of term"
+#: models.py:53 templates/core/persons_accounts.html:36
+msgid "Person"
 msgstr ""
 
-#: models.py:63
-msgid "Effective end date of term"
+#: models.py:56
+msgid "Can view address"
 msgstr ""
 
-#: models.py:83
-msgid "School term"
+#: models.py:57
+msgid "Can view contact details"
 msgstr ""
 
-#: models.py:84
-msgid "School terms"
+#: models.py:58
+msgid "Can view photo"
 msgstr ""
 
-#: models.py:94 templates/core/persons_accounts.html:36
-msgid "Person"
+#: models.py:59
+msgid "Can view persons groups"
 msgstr ""
 
-#: models.py:97
+#: models.py:60
+msgid "Can view personal details"
+msgstr ""
+
+#: models.py:65
 msgid "female"
 msgstr ""
 
-#: models.py:97
+#: models.py:65
 msgid "male"
 msgstr ""
 
-#: models.py:102
+#: models.py:73
+msgid "Linked user"
+msgstr ""
+
+#: models.py:75
 msgid "Is person active?"
 msgstr ""
 
-#: models.py:104
+#: models.py:77
 msgid "First name"
 msgstr ""
 
-#: models.py:105
+#: models.py:78
 msgid "Last name"
 msgstr ""
 
-#: models.py:107
+#: models.py:80
 msgid "Additional name(s)"
 msgstr ""
 
-#: models.py:111
+#: models.py:84 models.py:260
 msgid "Short name"
 msgstr ""
 
-#: models.py:114
+#: models.py:87
 msgid "Street"
 msgstr ""
 
-#: models.py:115
+#: models.py:88
 msgid "Street number"
 msgstr ""
 
-#: models.py:116
+#: models.py:89
 msgid "Postal code"
 msgstr ""
 
-#: models.py:117
+#: models.py:90
 msgid "Place"
 msgstr ""
 
-#: models.py:119
+#: models.py:92
 msgid "Home phone"
 msgstr ""
 
-#: models.py:120
+#: models.py:93
 msgid "Mobile phone"
 msgstr ""
 
-#: models.py:122
-msgid "E-mail address"
-msgstr ""
-
-#: models.py:124
+#: models.py:97
 msgid "Date of birth"
 msgstr ""
 
-#: models.py:125
+#: models.py:98
 msgid "Sex"
 msgstr ""
 
-#: models.py:127
+#: models.py:100
 msgid "Photo"
 msgstr ""
 
-#: models.py:131 models.py:249
-msgid "Reference ID of import source"
+#: models.py:105
+msgid "Guardians / Parents"
 msgstr ""
 
-#: models.py:140
-msgid "Guardians / Parents"
+#: models.py:112
+msgid "Primary group"
 msgstr ""
 
-#: models.py:231
-msgid "Group"
+#: models.py:115 models.py:346 models.py:370 models.py:455 models.py:643
+msgid "Description"
 msgstr ""
 
-#: models.py:234
-msgid "Long name of group"
+#: models.py:233
+msgid "Title of field"
 msgstr ""
 
 #: models.py:235
-msgid "Short name of group"
+msgid "Type of field"
+msgstr ""
+
+#: models.py:239
+msgid "Addtitional field for groups"
 msgstr ""
 
-#: models.py:244
+#: models.py:240
+msgid "Addtitional fields for groups"
+msgstr ""
+
+#: models.py:252
+msgid "Group"
+msgstr ""
+
+#: models.py:254
+msgid "Can assign child groups to groups"
+msgstr ""
+
+#: models.py:258
+msgid "Long name"
+msgstr ""
+
+#: models.py:268 templates/core/group_full.html:37
+msgid "Members"
+msgstr ""
+
+#: models.py:271 templates/core/group_full.html:34
+msgid "Owners"
+msgstr ""
+
+#: models.py:278
 msgid "Parent groups"
 msgstr ""
 
-#: models.py:268 models.py:285 models.py:363
-#: templates/core/announcement/list.html:18
-msgid "Title"
+#: models.py:286
+msgid "Type of group"
 msgstr ""
 
-#: models.py:269 models.py:286 models.py:364
-msgid "Description"
+#: models.py:290
+msgid "Additional fields"
 msgstr ""
 
-#: models.py:271
+#: models.py:342
+msgid "User"
+msgstr ""
+
+#: models.py:345 models.py:369 models.py:454
+#: templates/core/announcement/list.html:18
+msgid "Title"
+msgstr ""
+
+#: models.py:348
 msgid "Application"
 msgstr ""
 
-#: models.py:277
+#: models.py:354
 msgid "Activity"
 msgstr ""
 
-#: models.py:278
+#: models.py:355
 msgid "Activities"
 msgstr ""
 
-#: models.py:282
+#: models.py:361
 msgid "Sender"
 msgstr ""
 
-#: models.py:287 models.py:365 models.py:518
+#: models.py:366
+msgid "Recipient"
+msgstr ""
+
+#: models.py:371 models.py:624
 msgid "Link"
 msgstr ""
 
-#: models.py:289
+#: models.py:373
 msgid "Read"
 msgstr ""
 
-#: models.py:290
+#: models.py:374
 msgid "Sent"
 msgstr ""
 
-#: models.py:301
+#: models.py:387
 msgid "Notification"
 msgstr ""
 
-#: models.py:302
+#: models.py:388
 msgid "Notifications"
 msgstr ""
 
-#: models.py:368
+#: models.py:456
+msgid "Link to detailed view"
+msgstr ""
+
+#: models.py:459
 msgid "Date and time from when to show"
 msgstr ""
 
-#: models.py:371
+#: models.py:462
 msgid "Date and time until when to show"
 msgstr ""
 
-#: models.py:394
+#: models.py:486
 msgid "Announcement"
 msgstr ""
 
-#: models.py:422
+#: models.py:524
 msgid "Announcement recipient"
 msgstr ""
 
-#: models.py:423
+#: models.py:525
 msgid "Announcement recipients"
 msgstr ""
 
-#: models.py:473
+#: models.py:575
 msgid "Widget Title"
 msgstr ""
 
-#: models.py:474
+#: models.py:576
 msgid "Activate Widget"
 msgstr ""
 
-#: models.py:486
+#: models.py:594
 msgid "Dashboard Widget"
 msgstr ""
 
-#: models.py:487
+#: models.py:595
 msgid "Dashboard Widgets"
 msgstr ""
 
-#: models.py:491
+#: models.py:601
 msgid "Menu ID"
 msgstr ""
 
-#: models.py:492
-msgid "Menu name"
-msgstr ""
-
-#: models.py:509
+#: models.py:613
 msgid "Custom menu"
 msgstr ""
 
-#: models.py:510
+#: models.py:614
 msgid "Custom menus"
 msgstr ""
 
-#: models.py:515
+#: models.py:621
 msgid "Menu"
 msgstr ""
 
-#: models.py:520
+#: models.py:623
+msgid "Name"
+msgstr ""
+
+#: models.py:625
 msgid "Icon"
 msgstr ""
 
-#: models.py:527
+#: models.py:631
 msgid "Custom menu item"
 msgstr ""
 
-#: models.py:528
+#: models.py:632
 msgid "Custom menu items"
 msgstr ""
 
-#: settings.py:254
-msgid "German"
+#: models.py:642
+msgid "Title of type"
 msgstr ""
 
-#: settings.py:255
-msgid "English"
+#: models.py:646
+msgid "Group type"
+msgstr ""
+
+#: models.py:647
+msgid "Group types"
+msgstr ""
+
+#: models.py:656
+msgid "Can view system status"
+msgstr ""
+
+#: models.py:657
+msgid "Can link persons to accounts"
+msgstr ""
+
+#: models.py:658
+msgid "Can manage data"
+msgstr ""
+
+#: models.py:659
+msgid "Can impersonate"
+msgstr ""
+
+#: models.py:660
+msgid "Can use search"
+msgstr ""
+
+#: models.py:661
+msgid "Can change site preferences"
+msgstr ""
+
+#: models.py:662
+msgid "Can change person preferences"
+msgstr ""
+
+#: models.py:663
+msgid "Can change group preferences"
 msgstr ""
 
-#: settings.py:373
+#: preferences.py:27
 msgid "Site title"
 msgstr ""
 
-#: settings.py:374
+#: preferences.py:36
 msgid "Site description"
 msgstr ""
 
-#: settings.py:375
+#: preferences.py:45
 msgid "Primary colour"
 msgstr ""
 
-#: settings.py:376
+#: preferences.py:54
 msgid "Secondary colour"
 msgstr ""
 
-#: settings.py:377
+#: preferences.py:62
+msgid "Logo"
+msgstr ""
+
+#: preferences.py:70
+msgid "Favicon"
+msgstr ""
+
+#: preferences.py:78
+msgid "PWA-Icon"
+msgstr ""
+
+#: preferences.py:87
 msgid "Mail out name"
 msgstr ""
 
-#: settings.py:378
+#: preferences.py:96
 msgid "Mail out address"
 msgstr ""
 
-#: settings.py:379
+#: preferences.py:106
 msgid "Link to privacy policy"
 msgstr ""
 
-#: settings.py:380
+#: preferences.py:116
 msgid "Link to imprint"
 msgstr ""
 
-#: settings.py:381
-msgid "Name format of adresses"
+#: preferences.py:126
+msgid "Name format for addressing"
 msgstr ""
 
-#: settings.py:382
-msgid "Channels to allow for notifications"
+#: preferences.py:140
+msgid "Channels to use for notifications"
 msgstr ""
 
-#: settings.py:383
+#: preferences.py:150
 msgid "Regular expression to match primary group, e.g. '^Class .*'"
 msgstr ""
 
+#: preferences.py:159
+msgid "Field on person to match primary group against"
+msgstr ""
+
+#: preferences.py:171
+msgid "Display name of the school"
+msgstr ""
+
+#: preferences.py:180
+msgid "Official name of the school, e.g. as given by supervisory authority"
+msgstr ""
+
+#: settings.py:276
+msgid "English"
+msgstr ""
+
+#: settings.py:277
+msgid "German"
+msgstr ""
+
+#: settings.py:278
+msgid "French"
+msgstr ""
+
+#: templates/403.html:10 templates/404.html:10 templates/500.html:10
+msgid "Error"
+msgstr ""
+
 #: templates/403.html:10
-msgid "Error (403): You are not allowed to access the requested page or object."
+msgid ""
+"You are not allowed to access the requested page or\n"
+"          object."
 msgstr ""
 
-#: templates/403.html:12
+#: templates/403.html:13 templates/404.html:17
 msgid ""
 "\n"
-"      If you think this is an error in AlekSIS, please contact your site\n"
-"     administrators:\n"
-"     "
+"            If you think this is an error in AlekSIS, please contact your "
+"site\n"
+"            administrators:\n"
+"          "
 msgstr ""
 
 #: templates/404.html:10
-msgid "Error (404): The requested page or object was not found."
+msgid ""
+"The requested page or object was not\n"
+"          found."
 msgstr ""
 
-#: templates/404.html:12
+#: templates/404.html:13
 msgid ""
 "\n"
-"      If you were redirected by a link on an external page,\n"
-"      it is possible that that link was outdated.\n"
-"     "
+"            If you were redirected by a link on an external page,\n"
+"            it is possible that that link was outdated.\n"
+"          "
+msgstr ""
+
+#: templates/500.html:10
+msgid ""
+"An unexpected error has\n"
+"          occured."
 msgstr ""
 
-#: templates/404.html:16
+#: templates/500.html:13
 msgid ""
 "\n"
-"      If you think this is an error in AlekSIS, please contact your site\n"
-"      administrators:\n"
-"     "
+"            Your site administrators will automatically be notified about "
+"this\n"
+"            error. You can also contact them directly:\n"
+"          "
 msgstr ""
 
-#: templates/500.html:10
-msgid "Error (500): An unexpected error has occured.."
+#: templates/503.html:10
+msgid ""
+"The maintenance mode is currently enabled. Please try again\n"
+"          later."
 msgstr ""
 
-#: templates/500.html:12
+#: templates/503.html:13
 msgid ""
 "\n"
-"      Your site administrators will automatically be notified about this\n"
-"     error.\n"
-"     "
+"            This page is currently unavailable. If this error persists, "
+"contact your site administrators:\n"
+"          "
 msgstr ""
 
-#: templates/503.html:10
-msgid "The maintenance mode is currently enabled. Please try again later."
+#: templates/core/about.html:6 templates/core/about.html:15
+msgid "About AlekSIS"
+msgstr ""
+
+#: templates/core/about.html:7
+msgid "AlekSIS – The Free School Information System"
+msgstr ""
+
+#: templates/core/about.html:17
+msgid ""
+"\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 ""
+
+#: templates/core/about.html:25
+msgid "Website of AlekSIS"
+msgstr ""
+
+#: templates/core/about.html:26
+msgid "Source code"
 msgstr ""
 
-#: templates/503.html:12
+#: templates/core/about.html:35
+msgid "Licence information"
+msgstr ""
+
+#: templates/core/about.html:37
+msgid ""
+"\n"
+"              The core and the official apps of AlekSIS are licenced under "
+"the EUPL, version 1.2 or later. For licence\n"
+"              information from third-party apps, if installed, refer to the "
+"respective components below. The\n"
+"              licences are marked like this:\n"
+"            "
+msgstr ""
+
+#: templates/core/about.html:45
+msgid "Free/Open Source Licence"
+msgstr ""
+
+#: templates/core/about.html:46
+msgid "Other Licence"
+msgstr ""
+
+#: templates/core/about.html:50
+msgid "Full licence text"
+msgstr ""
+
+#: templates/core/about.html:51
+msgid "More information about the EUPL"
+msgstr ""
+
+#: templates/core/about.html:90
+#, python-format
 msgid ""
 "\n"
-"      This page is currently unavailable. If this error stays, contact your site administrators:\n"
-"     "
+"                    This app is licenced under %(licence)s.\n"
+"                  "
 msgstr ""
 
 #: templates/core/announcement/form.html:10
@@ -545,8 +779,8 @@ msgstr ""
 msgid "Actions"
 msgstr ""
 
-#: templates/core/announcement/list.html:36 templates/core/group_full.html:15
-#: templates/core/person_full.html:15
+#: templates/core/announcement/list.html:36 templates/core/group_full.html:22
+#: templates/core/person_full.html:21
 msgid "Edit"
 msgstr ""
 
@@ -586,15 +820,24 @@ msgstr ""
 msgid "Logged in as"
 msgstr ""
 
-#: templates/core/base.html:146
+#: templates/core/base.html:76 templates/search/search.html:7
+#: templates/search/search.html:22
+msgid "Search"
+msgstr ""
+
+#: templates/core/base.html:148
+msgid "About AlekSIS — The Free School Information System"
+msgstr ""
+
+#: templates/core/base.html:156
 msgid "Impress"
 msgstr ""
 
-#: templates/core/base.html:154
+#: templates/core/base.html:164
 msgid "Privacy Policy"
 msgstr ""
 
-#: templates/core/base_print.html:60
+#: templates/core/base_print.html:62
 msgid "Powered by AlekSIS"
 msgstr ""
 
@@ -606,71 +849,132 @@ msgstr ""
 msgid "Edit person"
 msgstr ""
 
-#: templates/core/edit_school.html:8 templates/core/edit_school.html:9
-msgid "Edit school"
+#: templates/core/group_full.html:28 templates/core/person_full.html:28
+msgid "Change preferences"
 msgstr ""
 
-#: templates/core/group_full.html:19
-msgid "Owners"
+#: templates/core/groups.html:14
+msgid "Create group"
 msgstr ""
 
-#: templates/core/group_full.html:22
-msgid "Members"
+#: templates/core/groups_child_groups.html:18
+msgid ""
+"\n"
+"          You can use this to assign child groups to groups. Please use the "
+"filters below to select groups you want to\n"
+"          change and click \"Next\".\n"
+"        "
 msgstr ""
 
-#: templates/core/groups.html:14
-msgid "Create group"
+#: templates/core/groups_child_groups.html:31
+msgid "Update selection"
 msgstr ""
 
-#: templates/core/index.html:4
-msgid "Home"
+#: templates/core/groups_child_groups.html:35
+msgid "Clear all filters"
+msgstr ""
+
+#: templates/core/groups_child_groups.html:39
+msgid "Currently selected groups"
+msgstr ""
+
+#: templates/core/groups_child_groups.html:52
+msgid "Start assigning child groups for this groups"
+msgstr ""
+
+#: templates/core/groups_child_groups.html:61
+msgid ""
+"\n"
+"            Please select some groups in order to go on with assigning.\n"
+"          "
+msgstr ""
+
+#: templates/core/groups_child_groups.html:72
+msgid "Current group:"
+msgstr ""
+
+#: templates/core/groups_child_groups.html:78
+msgid "Please be careful!"
+msgstr ""
+
+#: templates/core/groups_child_groups.html:79
+msgid ""
+"\n"
+"            If you click \"Back\" or \"Next\" the current group assignments "
+"are not saved.\n"
+"            If you click \"Save\", you will overwrite all existing child "
+"group relations for this group with what you\n"
+"            selected on this page.\n"
+"          "
+msgstr ""
+
+#: templates/core/groups_child_groups.html:93
+#: templates/core/groups_child_groups.html:128
+#: templates/two_factor/_wizard_actions.html:15
+#: templates/two_factor/_wizard_actions.html:20
+msgid "Back"
 msgstr ""
 
-#: templates/core/index.html:11
-msgid "AlekSIS (School Information System)"
+#: templates/core/groups_child_groups.html:99
+#: templates/core/groups_child_groups.html:134
+#: templates/two_factor/_wizard_actions.html:26
+msgid "Next"
+msgstr ""
+
+#: templates/core/groups_child_groups.html:106
+#: templates/core/groups_child_groups.html:141
+#: templates/core/save_button.html:3
+msgid "Save"
+msgstr ""
+
+#: templates/core/groups_child_groups.html:112
+#: templates/core/groups_child_groups.html:147
+msgid "Save and next"
+msgstr ""
+
+#: templates/core/index.html:4
+msgid "Home"
 msgstr ""
 
-#: templates/core/index.html:43
+#: templates/core/index.html:42
 msgid "Last activities"
 msgstr ""
 
-#: templates/core/index.html:61
+#: templates/core/index.html:60
 msgid "No activities available yet."
 msgstr ""
 
-#: templates/core/index.html:66
+#: templates/core/index.html:65
 msgid "Recent notifications"
 msgstr ""
 
-#: templates/core/index.html:82
+#: templates/core/index.html:81
 msgid "More information →"
 msgstr ""
 
-#: templates/core/index.html:89
+#: templates/core/index.html:88
 msgid "No notifications available yet."
 msgstr ""
 
-#: templates/core/no_person.html:11
+#: templates/core/no_person.html:12
 msgid ""
 "\n"
-"          Your user account is not linked to a person. This means you\n"
-"          cannot access any school-related information. Please contact\n"
-"          the managers of AlekSIS at your school.\n"
-"        "
-msgstr ""
-
-#: templates/core/offline.html:6
-msgid "No internet connection."
+"            Your administrator account is not linked to any person. "
+"Therefore,\n"
+"            a dummy person has been linked to your account.\n"
+"          "
 msgstr ""
 
-#: templates/core/offline.html:9
+#: templates/core/no_person.html:19
 msgid ""
 "\n"
-"        There was an error accessing this page. You probably don't have an internet connection. Check to see if your WiFi or mobile data is turned on and try again. If you think you are connected, please contact the system administrators:\n"
-"      "
+"            Your user account is not linked to a person. This means you\n"
+"            cannot access any school-related information. Please contact\n"
+"            the managers of AlekSIS at your school.\n"
+"          "
 msgstr ""
 
-#: templates/core/person_full.html:19
+#: templates/core/person_full.html:34
 msgid "Contact details"
 msgstr ""
 
@@ -683,7 +987,8 @@ msgstr ""
 msgid ""
 "\n"
 "        You can use this form to assign user accounts to persons. Use the\n"
-"        dropdowns to select existing accounts; use the text fields to create new\n"
+"        dropdowns to select existing accounts; use the text fields to create "
+"new\n"
 "        accounts on-the-fly. The latter will create a new account with the\n"
 "        entered username and copy all other details from the person.\n"
 "      "
@@ -702,15 +1007,6 @@ msgstr ""
 msgid "New account"
 msgstr ""
 
-#: templates/core/save_button.html:3
-msgid "Save"
-msgstr ""
-
-#: templates/core/school_management.html:6
-#: templates/core/school_management.html:7
-msgid "School management"
-msgstr ""
-
 #: templates/core/system_status.html:12
 msgid "System checks"
 msgstr ""
@@ -722,7 +1018,8 @@ msgstr ""
 #: templates/core/system_status.html:23
 msgid ""
 "\n"
-"                Only admin and visitors from internal IPs can access thesite.\n"
+"                Only admin and visitors from internal IPs can access "
+"thesite.\n"
 "              "
 msgstr ""
 
@@ -741,7 +1038,8 @@ msgstr ""
 #: templates/core/system_status.html:47
 msgid ""
 "\n"
-"                The web server throws back debug information on errors. Do not use in production!\n"
+"                The web server throws back debug information on errors. Do "
+"not use in production!\n"
 "              "
 msgstr ""
 
@@ -752,105 +1050,97 @@ msgstr ""
 #: templates/core/system_status.html:56
 msgid ""
 "\n"
-"                Debug mode is disabled. Default error pages are displayed on errors.\n"
+"                Debug mode is disabled. Default error pages are displayed on "
+"errors.\n"
 "              "
 msgstr ""
 
-#: templates/impersonate/list_users.html:8
-msgid "Impersonate user"
-msgstr ""
-
-#: templates/martor/editor.html:27
-msgid "Uploading... please wait..."
-msgstr ""
-
-#: templates/martor/editor.html:36
-msgid "Nothing to preview"
-msgstr ""
-
-#: templates/martor/emoji.html:4
-msgid "Select Emoji to Insert"
+#: templates/dynamic_preferences/form.html:9
+msgid "Site preferences"
 msgstr ""
 
-#: templates/martor/emoji.html:8
-msgid "Preparing emojis..."
+#: templates/dynamic_preferences/form.html:11
+msgid "My preferences"
 msgstr ""
 
-#: templates/martor/guide.html:8
-msgid "Markdown Guide"
-msgstr ""
-
-#: templates/martor/guide.html:9
+#: templates/dynamic_preferences/form.html:13
 #, python-format
-msgid ""
-"This site is powered by Markdown. For full\n"
-"            documentation,\n"
-"            <a href=\"%(doc_url)s\" target=\"_blank\">click here</a>"
-msgstr ""
-
-#: templates/martor/guide.html:15 templates/martor/toolbar.html:42
-msgid "Code"
+msgid "Preferences for %(instance)s"
 msgstr ""
 
-#: templates/martor/guide.html:16
-msgid "Or"
+#: templates/dynamic_preferences/form.html:25
+msgid "Save preferences"
 msgstr ""
 
-#: templates/martor/guide.html:19
-msgid "... to Get"
+#: templates/dynamic_preferences/sections.html:7
+msgid "All"
 msgstr ""
 
-#: templates/martor/toolbar.html:3
-msgid "Bold"
+#: templates/impersonate/list_users.html:8
+msgid "Impersonate user"
 msgstr ""
 
-#: templates/martor/toolbar.html:6
-msgid "Italic"
+#: templates/offline.html:6
+msgid ""
+"No internet\n"
+"    connection."
 msgstr ""
 
-#: templates/martor/toolbar.html:10
-msgid "Horizontal Line"
+#: templates/offline.html:10
+msgid ""
+"\n"
+"      There was an error accessing this page. You probably don't have an "
+"internet connection. Check to see if your WiFi\n"
+"      or mobile data is turned on and try again. If you think you are "
+"connected, please contact the system\n"
+"      administrators:\n"
+"    "
 msgstr ""
 
-#: templates/martor/toolbar.html:15
-msgid "Heading"
+#: templates/search/search.html:8
+msgid "Global Search"
 msgstr ""
 
-#: templates/martor/toolbar.html:20 templates/martor/toolbar.html:23
-#: templates/martor/toolbar.html:26
-msgid "H"
+#: templates/search/search.html:15
+msgid "Search Term"
 msgstr ""
 
-#: templates/martor/toolbar.html:31
-msgid "Pre or Code"
+#: templates/search/search.html:26
+msgid "Results"
 msgstr ""
 
-#: templates/martor/toolbar.html:38
-msgid "Pre"
+#: templates/search/search.html:38
+msgid "No search results could be found to your search."
 msgstr ""
 
-#: templates/martor/toolbar.html:48
-msgid "Quote"
+#: templates/search/search.html:87
+msgid "Please enter a search term above."
 msgstr ""
 
-#: templates/martor/toolbar.html:52
-msgid "Unordered List"
+#: templates/templated_email/notification.email:3
+msgid "New notification for"
 msgstr ""
 
-#: templates/martor/toolbar.html:56
-msgid "Ordered List"
+#: templates/templated_email/notification.email:7
+msgid "Dear"
 msgstr ""
 
-#: templates/martor/toolbar.html:60
-msgid "URL/Link"
+#: templates/templated_email/notification.email:8
+msgid "we got a new notification for you:"
 msgstr ""
 
-#: templates/martor/toolbar.html:82
-msgid "Full Screen"
+#: templates/templated_email/notification.email:12
+msgid "More information"
 msgstr ""
 
-#: templates/martor/toolbar.html:86
-msgid "Markdown Guide (Help)"
+#: templates/templated_email/notification.email:16
+#, python-format
+msgid ""
+"\n"
+"    <p>By %(trans_sender)s at %(trans_created_at)s</p>\n"
+"\n"
+"    <i>Your AlekSIS team</i>\n"
+"    "
 msgstr ""
 
 #: templates/two_factor/_base_focus.html:6
@@ -864,15 +1154,6 @@ msgstr ""
 msgid "Cancel"
 msgstr ""
 
-#: templates/two_factor/_wizard_actions.html:15
-#: templates/two_factor/_wizard_actions.html:20
-msgid "Back"
-msgstr ""
-
-#: templates/two_factor/_wizard_actions.html:26
-msgid "Next"
-msgstr ""
-
 #: templates/two_factor/core/backup_tokens.html:5
 #: templates/two_factor/core/backup_tokens.html:9
 #: templates/two_factor/profile/profile.html:46
@@ -884,8 +1165,10 @@ msgid ""
 "\n"
 "        Backup tokens can be used when your primary and backup\n"
 "        phone numbers aren't available. The backup tokens below can be used\n"
-"        for login verification. If you've used up all your backup tokens, you\n"
-"        can generate a new set of backup tokens. Only the backup tokens shown\n"
+"        for login verification. If you've used up all your backup tokens, "
+"you\n"
+"        can generate a new set of backup tokens. Only the backup tokens "
+"shown\n"
 "        below will be valid.\n"
 "      "
 msgstr ""
@@ -909,44 +1192,50 @@ msgstr ""
 msgid "Generate Tokens"
 msgstr ""
 
-#: templates/two_factor/core/login.html:17
-msgid "Enter your credentials."
+#: templates/two_factor/core/login.html:16
+msgid ""
+"You have no permission to view this page. Please login with an other account."
+msgstr ""
+
+#: templates/two_factor/core/login.html:25
+msgid "Please login to see this page."
 msgstr ""
 
-#: templates/two_factor/core/login.html:20
+#: templates/two_factor/core/login.html:28
 msgid ""
 "We are calling your phone right now, please enter the\n"
-"            digits you hear."
+"              digits you hear."
 msgstr ""
 
-#: templates/two_factor/core/login.html:23
+#: templates/two_factor/core/login.html:31
 msgid ""
 "We sent you a text message, please enter the tokens we\n"
-"            sent."
+"              sent."
 msgstr ""
 
-#: templates/two_factor/core/login.html:26
+#: templates/two_factor/core/login.html:34
 msgid ""
 "Please enter the tokens generated by your token\n"
-"            generator."
+"              generator."
 msgstr ""
 
-#: templates/two_factor/core/login.html:30
+#: templates/two_factor/core/login.html:38
 msgid ""
 "Use this form for entering backup tokens for logging in.\n"
-"          These tokens have been generated for you to print and keep safe. Please\n"
-"          enter one of these backup tokens to login to your account."
+"            These tokens have been generated for you to print and keep safe. "
+"Please\n"
+"            enter one of these backup tokens to login to your account."
 msgstr ""
 
-#: templates/two_factor/core/login.html:47
+#: templates/two_factor/core/login.html:56
 msgid "Or, alternatively, use one of your backup phones:"
 msgstr ""
 
-#: templates/two_factor/core/login.html:57
+#: templates/two_factor/core/login.html:66
 msgid "As a last resort, you can use a backup token:"
 msgstr ""
 
-#: templates/two_factor/core/login.html:60
+#: templates/two_factor/core/login.html:69
 msgid "Use Backup Token"
 msgstr ""
 
@@ -957,7 +1246,8 @@ msgstr ""
 #: templates/two_factor/core/otp_required.html:10
 msgid ""
 "The page you requested, enforces users to verify using\n"
-"          two-factor authentication for security reasons. You need to enable these\n"
+"          two-factor authentication for security reasons. You need to enable "
+"these\n"
 "          security features in order to access this page."
 msgstr ""
 
@@ -1034,7 +1324,8 @@ msgstr ""
 #: templates/two_factor/core/setup.html:50
 msgid ""
 "\n"
-"            We are calling your phone right now, please enter the digits you hear.\n"
+"            We are calling your phone right now, please enter the digits you "
+"hear.\n"
 "          "
 msgstr ""
 
@@ -1048,9 +1339,12 @@ msgstr ""
 #: templates/two_factor/core/setup.html:63
 msgid ""
 "\n"
-"          We've encountered an issue with the selected authentication method. Please\n"
-"          go back and verify that you entered your information correctly, try\n"
-"          again, or use a different authentication method instead. If the issue\n"
+"          We've encountered an issue with the selected authentication "
+"method. Please\n"
+"          go back and verify that you entered your information correctly, "
+"try\n"
+"          again, or use a different authentication method instead. If the "
+"issue\n"
 "          persists, contact the site administrator.\n"
 "        "
 msgstr ""
@@ -1072,7 +1366,8 @@ msgstr ""
 #: templates/two_factor/core/setup_complete.html:14
 msgid ""
 "\n"
-"        Congratulations, you've successfully enabled two-factor authentication.\n"
+"        Congratulations, you've successfully enabled two-factor "
+"authentication.\n"
 "      "
 msgstr ""
 
@@ -1090,7 +1385,8 @@ msgstr ""
 msgid ""
 "\n"
 "          However, it might happen that you don't have access to\n"
-"          your primary token device. To enable account recovery, generate backup codes\n"
+"          your primary token device. To enable account recovery, generate "
+"backup codes\n"
 "          or add a phone number.\n"
 "        "
 msgstr ""
@@ -1108,7 +1404,9 @@ msgid "Disable Two-Factor Authentication"
 msgstr ""
 
 #: templates/two_factor/profile/disable.html:12
-msgid "You are about to disable two-factor authentication. This weakens your account security, are you sure?"
+msgid ""
+"You are about to disable two-factor authentication. This weakens your "
+"account security, are you sure?"
 msgstr ""
 
 #: templates/two_factor/profile/disable.html:26
@@ -1191,38 +1489,34 @@ msgid ""
 "      "
 msgstr ""
 
-#: util/notifications.py:66
+#: util/notifications.py:65
 msgid "E-Mail"
 msgstr ""
 
-#: util/notifications.py:67
+#: util/notifications.py:66
 msgid "SMS"
 msgstr ""
 
-#: views.py:172
-msgid "The person has been saved."
-msgstr ""
-
-#: views.py:195
-msgid "The group has been saved."
-msgstr ""
-
-#: views.py:236
-msgid "The school has been saved."
+#: views.py:212
+msgid "The child groups were successfully saved."
 msgstr ""
 
-#: views.py:255
-msgid "The term has been saved."
+#: views.py:240
+msgid "The person has been saved."
 msgstr ""
 
-#: views.py:272
-msgid "You are not allowed to mark notifications from other users as read!"
+#: views.py:276
+msgid "The group has been saved."
 msgstr ""
 
-#: views.py:307
+#: views.py:348
 msgid "The announcement has been saved."
 msgstr ""
 
-#: views.py:320
+#: views.py:364
 msgid "The announcement has been deleted."
 msgstr ""
+
+#: views.py:435
+msgid "The preferences have been saved successfully."
+msgstr ""
diff --git a/aleksis/core/locale/ar/LC_MESSAGES/djangojs.po b/aleksis/core/locale/ar/LC_MESSAGES/djangojs.po
index f20797d98cc1b2033821636f574477d9e07e1adb..28f74abb7354ad25df12874286481ef1858f96a0 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: 2020-03-30 09:18+0000\n"
+"POT-Creation-Date: 2020-04-28 13:31+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"
diff --git a/aleksis/core/locale/de_DE/LC_MESSAGES/django.po b/aleksis/core/locale/de_DE/LC_MESSAGES/django.po
index ccd9c971f5d0615838e6d81eb5b9c56f6344601a..d8fe1369884426025d603c428f4a4eb21b13d313 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: 2020-03-30 09:18+0000\n"
-"PO-Revision-Date: 2020-04-14 18:42+0000\n"
-"Last-Translator: Tom Teichler <tom.teichler@teckids.org>\n"
+"POT-Creation-Date: 2020-05-04 15:39+0200\n"
+"PO-Revision-Date: 2020-05-04 15:09+0000\n"
+"Last-Translator: Anonymous <noreply@weblate.org>\n"
 "Language-Team: German <https://translate.edugit.org/projects/aleksis/aleksis/"
 "de/>\n"
 "Language: de_DE\n"
@@ -17,74 +17,96 @@ msgstr ""
 "Content-Type: text/plain; charset=UTF-8\n"
 "Content-Transfer-Encoding: 8bit\n"
 "Plural-Forms: nplurals=2; plural=n != 1;\n"
-"X-Generator: Weblate 3.8\n"
+"X-Generator: Weblate 4.0.1\n"
 
-#: forms.py:38 forms.py:93
+#: forms.py:46
 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."
+msgstr ""
+"Sie können keine neuen Benutzer erstellen, wenn Sie gleichzeitig einen "
+"existierenden Benutzer auswählen."
 
-#: forms.py:41 forms.py:96
+#: forms.py:50
 msgid "This username is already in use."
 msgstr "Dieser Benutzername wird bereits genutzt."
 
+#: forms.py:74
+msgid "Base data"
+msgstr "Basisdaten"
+
+#: forms.py:80
+msgid "Address"
+msgstr "Adresse"
+
+#: forms.py:81
+msgid "Contact data"
+msgstr "Kontaktdaten"
+
 #: forms.py:83
+msgid "Advanced personal data"
+msgstr "Zusätzliche persönliche Daten"
+
+#: forms.py:116
 msgid "New user"
 msgstr "Neuer Benutzer"
 
-#: forms.py:83
+#: forms.py:116
 msgid "Create a new account"
 msgstr "Neues Benutzerkonto erstellen"
 
-#: forms.py:149 forms.py:152
+#: forms.py:128
+msgid "Common data"
+msgstr "Allgemeine Daten"
+
+#: forms.py:129 forms.py:169 menus.py:141 models.py:54
+#: templates/core/persons.html:8 templates/core/persons.html:9
+msgid "Persons"
+msgstr "Personen"
+
+#: forms.py:162 forms.py:165 models.py:31
 msgid "Date"
 msgstr "Datum"
 
-#: forms.py:150 forms.py:153
+#: forms.py:163 forms.py:166 models.py:39
 msgid "Time"
 msgstr "Zeit"
 
-#: forms.py:155 menus.py:127 models.py:95 templates/core/persons.html:8
-#: templates/core/persons.html:9
-msgid "Persons"
-msgstr "Personen"
-
-#: forms.py:156 menus.py:133 models.py:232 templates/core/groups.html:8
-#: templates/core/groups.html:9 templates/core/person_full.html:79
+#: forms.py:171 menus.py:149 models.py:253 templates/core/groups.html:8
+#: templates/core/groups.html:9 templates/core/person_full.html:106
 msgid "Groups"
 msgstr "Gruppen"
 
-#: forms.py:160
+#: forms.py:175
 msgid "From when until when should the announcement be displayed?"
 msgstr "Von wann bis wann soll die Ankündigung angezeigt werden?"
 
-#: forms.py:163
+#: forms.py:178
 msgid "Who should see the announcement?"
 msgstr "Wer soll die Ankündigung sehen?"
 
-#: forms.py:164
+#: forms.py:179
 msgid "Write your announcement:"
 msgstr "Schreiben Sie ihre Ankündigung:"
 
-#: forms.py:203
-msgid "You are not allowed to create announcements which are only valid in the past."
+#: forms.py:216
+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."
 
-#: forms.py:207
+#: forms.py:220
 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."
 
-#: forms.py:215
+#: forms.py:229
 msgid "You need at least one recipient."
 msgstr "Sie benötigen mindestens einen Empfänger."
 
-#: menus.py:7 templates/registration/login.html:21
-#: templates/two_factor/core/login.html:6
+#: menus.py:7 templates/two_factor/core/login.html:6
 #: templates/two_factor/core/login.html:10
-#: templates/two_factor/core/login.html:64
+#: templates/two_factor/core/login.html:73
 msgid "Login"
 msgstr "Anmelden"
 
@@ -105,449 +127,674 @@ msgid "Logout"
 msgstr "Abmelden"
 
 #: menus.py:41
-msgid "Two factor auth"
-msgstr "Zwei-Faktor-Authentifizierung"
+msgid "2FA"
+msgstr "2FA"
+
+#: menus.py:47
+msgid "Me"
+msgstr "Ich"
 
-#: menus.py:52
+#: menus.py:56 templates/dynamic_preferences/form.html:5
+msgid "Preferences"
+msgstr "Einstellungen"
+
+#: menus.py:67
 msgid "Admin"
 msgstr "Admin"
 
-#: menus.py:61 models.py:395 templates/core/announcement/list.html:7
+#: menus.py:75 models.py:487 templates/core/announcement/list.html:7
 #: templates/core/announcement/list.html:8
 msgid "Announcements"
 msgstr "Ankündigungen"
 
-#: menus.py:70 templates/core/data_management.html:6
+#: menus.py:86 templates/core/data_management.html:6
 #: templates/core/data_management.html:7
 msgid "Data management"
 msgstr "Datenverwaltung"
 
-#: menus.py:79 templates/core/system_status.html:5
+#: menus.py:94 templates/core/system_status.html:5
 #: templates/core/system_status.html:7
 msgid "System status"
-msgstr "System-Status"
+msgstr "Systemstatus"
 
-#: menus.py:88
+#: menus.py:105
 msgid "Impersonation"
 msgstr "Verkleidung"
 
-#: menus.py:97
-msgid "Manage school"
-msgstr "Schulverwaltung"
+#: menus.py:113
+msgid "Configuration"
+msgstr "Konfiguration"
 
-#: menus.py:106
+#: menus.py:124
 msgid "Backend Admin"
 msgstr "Backend-Administration"
 
-#: menus.py:117
+#: menus.py:132
 msgid "People"
 msgstr "Leute"
 
-#: menus.py:139
+#: menus.py:157
 msgid "Persons and accounts"
 msgstr "Personen und Konten"
 
-#: menus.py:152
-msgid "Edit school information"
-msgstr "Schulinformationen bearbeiten"
+#: menus.py:168
+msgid "Groups and child groups"
+msgstr "Gruppen und Kindgruppen"
 
-#: menus.py:153 templates/core/edit_schoolterm.html:8
-#: templates/core/edit_schoolterm.html:9
-msgid "Edit school term"
-msgstr "Schuljahr bearbeiten"
+#: menus.py:183 templates/core/groups_child_groups.html:7
+#: templates/core/groups_child_groups.html:9
+msgid "Assign child groups to groups"
+msgstr "Kindgruppen zu Gruppen zuordnen"
 
-#: models.py:31 models.py:517
-msgid "Name"
-msgstr "Name"
+#: models.py:29
+msgid "Boolean (Yes/No)"
+msgstr "Boolean (Ja/Nein)"
+
+#: models.py:30
+msgid "Text (one line)"
+msgstr "Text (eine Zeile)"
+
+#: models.py:32
+msgid "Date and time"
+msgstr "Datum und Uhrzeit"
 
 #: models.py:33
-msgid "Official name"
-msgstr "Offizieller Name"
+msgid "Decimal number"
+msgstr "Dezimalzahl"
+
+#: models.py:34 models.py:95
+msgid "E-mail address"
+msgstr "E-Mail-Adresse"
 
 #: models.py:35
-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"
+msgid "Integer"
+msgstr "Ganze Zahl"
 
-#: models.py:38
-msgid "School logo"
-msgstr "Schullogo"
+#: models.py:36
+msgid "IP address"
+msgstr "IP-Adresse"
 
-#: models.py:51
-msgid "School"
-msgstr "Schule"
+#: models.py:37
+msgid "Boolean or empty (Yes/No/Neither)"
+msgstr "Boolean oder leer (Ja/Nein/weder)"
 
-#: models.py:52
-msgid "Schools"
-msgstr "Schulen"
+#: models.py:38
+msgid "Text (multi-line)"
+msgstr "Text (mehrzeilig)"
 
-#: models.py:60
-msgid "Visible caption of the term"
-msgstr "Sichtbare Beschriftung des Schuljahres"
+#: models.py:40
+msgid "URL / Link"
+msgstr "URL / Link"
 
-#: models.py:62
-msgid "Effective start date of term"
-msgstr "Startdatum des Schuljahres"
+#: models.py:53 templates/core/persons_accounts.html:36
+msgid "Person"
+msgstr "Person"
 
-#: models.py:63
-msgid "Effective end date of term"
-msgstr "Enddatum des Schuljahres"
+#: models.py:56
+msgid "Can view address"
+msgstr "Kann Adresse sehen"
 
-#: models.py:83
-msgid "School term"
-msgstr "Schuljahr"
+#: models.py:57
+msgid "Can view contact details"
+msgstr "Kann Kontaktdetails sehen"
 
-#: models.py:84
-msgid "School terms"
-msgstr "Schuljahre"
+#: models.py:58
+msgid "Can view photo"
+msgstr "Kann Foto sehen"
 
-#: models.py:94 templates/core/persons_accounts.html:36
-msgid "Person"
-msgstr "Person"
+#: models.py:59
+msgid "Can view persons groups"
+msgstr "Kann Gruppen einer Person sehen"
 
-#: models.py:97
+#: models.py:60
+msgid "Can view personal details"
+msgstr "Kann persönliche Daten sehen"
+
+#: models.py:65
 msgid "female"
 msgstr "weiblich"
 
-#: models.py:97
+#: models.py:65
 msgid "male"
 msgstr "männlich"
 
-#: models.py:102
+#: models.py:73
+msgid "Linked user"
+msgstr "Verknüpfter Benutzer"
+
+#: models.py:75
 msgid "Is person active?"
 msgstr "Ist die Person aktiv?"
 
-#: models.py:104
+#: models.py:77
 msgid "First name"
 msgstr "Vorname"
 
-#: models.py:105
+#: models.py:78
 msgid "Last name"
 msgstr "Nachname"
 
-#: models.py:107
+#: models.py:80
 msgid "Additional name(s)"
 msgstr "Zusätzliche Namen"
 
-#: models.py:111
+#: models.py:84 models.py:260
 msgid "Short name"
 msgstr "Kurzname"
 
-#: models.py:114
+#: models.py:87
 msgid "Street"
 msgstr "Straße"
 
-#: models.py:115
+#: models.py:88
 msgid "Street number"
 msgstr "Hausnummer"
 
-#: models.py:116
+#: models.py:89
 msgid "Postal code"
 msgstr "Postleitzahl"
 
-#: models.py:117
+#: models.py:90
 msgid "Place"
 msgstr "Ort"
 
-#: models.py:119
+#: models.py:92
 msgid "Home phone"
 msgstr "Festnetz"
 
-#: models.py:120
+#: models.py:93
 msgid "Mobile phone"
 msgstr "Handy"
 
-#: models.py:122
-msgid "E-mail address"
-msgstr "E-Mail-Adresse"
-
-#: models.py:124
+#: models.py:97
 msgid "Date of birth"
 msgstr "Geburtsdatum"
 
-#: models.py:125
+#: models.py:98
 msgid "Sex"
 msgstr "Geschlecht"
 
-#: models.py:127
+#: models.py:100
 msgid "Photo"
 msgstr "Foto"
 
-#: models.py:131 models.py:249
-msgid "Reference ID of import source"
-msgstr "Referenz-ID der Import-Quelle"
-
-#: models.py:140
+#: models.py:105
 msgid "Guardians / Parents"
 msgstr "Erziehungsberechtigte / Eltern"
 
-#: models.py:231
+#: models.py:112
+msgid "Primary group"
+msgstr "Primärgruppe"
+
+#: models.py:115 models.py:346 models.py:370 models.py:455 models.py:643
+msgid "Description"
+msgstr "Beschreibung"
+
+#: models.py:233
+msgid "Title of field"
+msgstr "Feldtitel"
+
+#: models.py:235
+msgid "Type of field"
+msgstr "Feldtyp"
+
+#: models.py:239
+msgid "Addtitional field for groups"
+msgstr "Zusätzliche Felder für Gruppen"
+
+#: models.py:240
+msgid "Addtitional fields for groups"
+msgstr "Zusätzliche Felder für Gruppen"
+
+#: models.py:252
 msgid "Group"
 msgstr "Gruppe"
 
-#: models.py:234
-msgid "Long name of group"
-msgstr "Langer Name der Gruppe"
+#: models.py:254
+msgid "Can assign child groups to groups"
+msgstr "Kann Kindgruppen zu Gruppen zuordnen"
 
-#: models.py:235
-msgid "Short name of group"
-msgstr "Kurzer Name der Gruppe"
+#: models.py:258
+msgid "Long name"
+msgstr "Langer Name"
+
+#: models.py:268 templates/core/group_full.html:37
+msgid "Members"
+msgstr "Mitglieder"
+
+#: models.py:271 templates/core/group_full.html:34
+msgid "Owners"
+msgstr "Leiter/-innen"
 
-#: models.py:244
+#: models.py:278
 msgid "Parent groups"
 msgstr "Ãœbergeordnete Gruppen"
 
-#: models.py:268 models.py:285 models.py:363
+#: models.py:286
+msgid "Type of group"
+msgstr "Gruppentyp"
+
+#: models.py:290
+msgid "Additional fields"
+msgstr "Zusätzliche Felder"
+
+#: models.py:342
+msgid "User"
+msgstr "Benutzer"
+
+#: models.py:345 models.py:369 models.py:454
 #: templates/core/announcement/list.html:18
 msgid "Title"
 msgstr "Titel"
 
-#: models.py:269 models.py:286 models.py:364
-msgid "Description"
-msgstr "Beschreibung"
-
-#: models.py:271
+#: models.py:348
 msgid "Application"
 msgstr "Anwendung"
 
-#: models.py:277
+#: models.py:354
 msgid "Activity"
 msgstr "Aktivität"
 
-#: models.py:278
+#: models.py:355
 msgid "Activities"
 msgstr "Aktivitäten"
 
-#: models.py:282
+#: models.py:361
 msgid "Sender"
 msgstr "Absender"
 
-#: models.py:287 models.py:365 models.py:518
+#: models.py:366
+msgid "Recipient"
+msgstr "Empfänger"
+
+#: models.py:371 models.py:624
 msgid "Link"
 msgstr "Link"
 
-#: models.py:289
+#: models.py:373
 msgid "Read"
 msgstr "Gelesen"
 
-#: models.py:290
+#: models.py:374
 msgid "Sent"
 msgstr "Versandt"
 
-#: models.py:301
+#: models.py:387
 msgid "Notification"
 msgstr "Benachrichtigung"
 
-#: models.py:302
+#: models.py:388
 msgid "Notifications"
 msgstr "Benachrichtigungen"
 
-#: models.py:368
+#: models.py:456
+msgid "Link to detailed view"
+msgstr "Link zur detaillierten Ansicht"
+
+#: models.py:459
 msgid "Date and time from when to show"
 msgstr "Datum und Uhrzeit des Anzeigestarts"
 
-#: models.py:371
+#: models.py:462
 msgid "Date and time until when to show"
-msgstr ""
+msgstr "Anzeigezeitraum"
 
-#: models.py:394
+#: models.py:486
 msgid "Announcement"
 msgstr "Ankündigung"
 
-#: models.py:422
+#: models.py:524
 msgid "Announcement recipient"
 msgstr "Empfänger der Ankündigung"
 
-#: models.py:423
+#: models.py:525
 msgid "Announcement recipients"
 msgstr "Empfänger der Ankündigung"
 
-#: models.py:473
+#: models.py:575
 msgid "Widget Title"
 msgstr "Widget-Titel"
 
-#: models.py:474
+#: models.py:576
 msgid "Activate Widget"
 msgstr "Widget aktivieren"
 
-#: models.py:486
+#: models.py:594
 msgid "Dashboard Widget"
 msgstr "Dashboard-Widget"
 
-#: models.py:487
+#: models.py:595
 msgid "Dashboard Widgets"
 msgstr "Dashboard-Widgets"
 
-#: models.py:491
+#: models.py:601
 msgid "Menu ID"
 msgstr "Menü-ID"
 
-#: models.py:492
-msgid "Menu name"
-msgstr "Menü-Name"
-
-#: models.py:509
+#: models.py:613
 msgid "Custom menu"
 msgstr "Benutzerdefiniertes Menü"
 
-#: models.py:510
+#: models.py:614
 msgid "Custom menus"
 msgstr "Benutzerdefinierte Menüs"
 
-#: models.py:515
+#: models.py:621
 msgid "Menu"
 msgstr "Menü"
 
-#: models.py:520
+#: models.py:623
+msgid "Name"
+msgstr "Name"
+
+#: models.py:625
 msgid "Icon"
 msgstr "Icon"
 
-#: models.py:527
+#: models.py:631
 msgid "Custom menu item"
 msgstr "Benutzerdefiniertes Menüelement"
 
-#: models.py:528
+#: models.py:632
 msgid "Custom menu items"
 msgstr "Benutzerdefinierte Menüelemente"
 
-#: settings.py:254
-msgid "German"
-msgstr "Deutsch"
+#: models.py:642
+msgid "Title of type"
+msgstr "Titel des Typs"
 
-#: settings.py:255
-msgid "English"
-msgstr "Englisch"
+#: models.py:646
+msgid "Group type"
+msgstr "Gruppentyp"
+
+#: models.py:647
+msgid "Group types"
+msgstr "Gruppentypen"
+
+#: models.py:656
+msgid "Can view system status"
+msgstr "Kann Systemstatus sehen"
+
+#: models.py:657
+msgid "Can link persons to accounts"
+msgstr "Kann Personen mit Benutzerkonten verknüpfen"
+
+#: models.py:658
+msgid "Can manage data"
+msgstr "Kann Daten verwalten"
+
+#: models.py:659
+msgid "Can impersonate"
+msgstr "Kann sich verkleiden"
 
-#: settings.py:373
+#: models.py:660
+msgid "Can use search"
+msgstr "Kann Suche benutzen"
+
+#: models.py:661
+msgid "Can change site preferences"
+msgstr "Kann Konfiguration ändern"
+
+#: models.py:662
+msgid "Can change person preferences"
+msgstr "Kann Einstellungen einer Person verändern"
+
+#: models.py:663
+msgid "Can change group preferences"
+msgstr "Kann Einstellungen einer Gruppe verändern"
+
+#: preferences.py:27
 msgid "Site title"
 msgstr "Seitentitel"
 
-#: settings.py:374
+#: preferences.py:36
 msgid "Site description"
 msgstr "Seitenbeschreibung"
 
-#: settings.py:375
+#: preferences.py:45
 msgid "Primary colour"
 msgstr "Primärfarbe"
 
-#: settings.py:376
+#: preferences.py:54
 msgid "Secondary colour"
 msgstr "Akzentfarbe"
 
-#: settings.py:377
+#: preferences.py:62
+msgid "Logo"
+msgstr "Logo"
+
+#: preferences.py:70
+msgid "Favicon"
+msgstr "Favicon"
+
+#: preferences.py:78
+msgid "PWA-Icon"
+msgstr "PWA-Icon"
+
+#: preferences.py:87
 msgid "Mail out name"
 msgstr "Ausgangsmailname"
 
-#: settings.py:378
+#: preferences.py:96
 msgid "Mail out address"
 msgstr "E-Mail-Ausgangsadresse"
 
-#: settings.py:379
+#: preferences.py:106
 msgid "Link to privacy policy"
 msgstr "Link zur Datenschutzerklärung"
 
-#: settings.py:380
+#: preferences.py:116
 msgid "Link to imprint"
 msgstr "Link zum Impressum"
 
-#: settings.py:381
-msgid "Name format of adresses"
-msgstr "Namensformat von Anreden"
+#: preferences.py:126
+msgid "Name format for addressing"
+msgstr "Namensformat für Anreden"
 
-#: settings.py:382
-msgid "Channels to allow for notifications"
-msgstr "Kanäle, welche für Benachrichtigungen erlaubt sind"
+#: preferences.py:140
+msgid "Channels to use for notifications"
+msgstr "Aktivierte Benachrichtungskanäle"
 
-#: settings.py:383
+#: preferences.py:150
 msgid "Regular expression to match primary group, e.g. '^Class .*'"
 msgstr "Regulärer Ausdruck um Primärgruppen zu finden, z. B.  '^Class .*'"
 
+#: preferences.py:159
+msgid "Field on person to match primary group against"
+msgstr "Feld um Primärgruppen zu finden"
+
+#: preferences.py:171
+msgid "Display name of the school"
+msgstr "Sichtbarer Name der Schule"
+
+#: preferences.py:180
+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"
+
+#: settings.py:276
+msgid "English"
+msgstr "Englisch"
+
+#: settings.py:277
+msgid "German"
+msgstr "Deutsch"
+
+#: settings.py:278
+msgid "French"
+msgstr "Französisch"
+
+#: templates/403.html:10 templates/404.html:10 templates/500.html:10
+msgid "Error"
+msgstr "Fehler"
+
 #: templates/403.html:10
-msgid "Error (403): You are not allowed to access the requested page or object."
+msgid ""
+"You are not allowed to access the requested page or\n"
+"          object."
 msgstr ""
-"Fehler(403): Es ist Ihnen nicht erlaubt, auf die angefragte Seite oder das "
-"angefragte Objekt zuzugreifen."
+"Es ist Ihnen nicht erlaubt, auf die angefragte Seite oder das angefragte\n"
+"             Objekt zuzugreifen."
 
-#: templates/403.html:12
+#: templates/403.html:13 templates/404.html:17
 msgid ""
 "\n"
-"      If you think this is an error in AlekSIS, please contact your site\n"
-"     administrators:\n"
-"     "
+"            If you think this is an error in AlekSIS, please contact your "
+"site\n"
+"            administrators:\n"
+"          "
 msgstr ""
 "\n"
-"      Wenn Sie der Meinung sind, dass es sich um einen Fehler in AlekSIS "
-"handelt, kontaktieren Sie bitte einen Ihrer\n"
+"            Wenn Sie der Meinung sind, dass es sich um einen Fehler in "
+"AlekSIS handelt, kontaktieren Sie bitte einen Ihrer\n"
 "     Systemadministratoren:\n"
-"     "
+"          "
 
 #: templates/404.html:10
-msgid "Error (404): The requested page or object was not found."
+msgid ""
+"The requested page or object was not\n"
+"          found."
 msgstr ""
-"Fehler (404): Die angefragte Seite oder das angefragte Objekt wurde nicht "
-"gefunden."
+"Die angefragte Seite oder das angefragte Objekt wurde nicht\n"
+"            gefunden."
 
-#: templates/404.html:12
-#, fuzzy
-#| msgid ""
-#| "\n"
-#| "     If you were redirected by a link on an external page,\n"
-#| "     it is possible that that link was outdated.\n"
-#| "    "
+#: templates/404.html:13
 msgid ""
 "\n"
-"      If you were redirected by a link on an external page,\n"
-"      it is possible that that link was outdated.\n"
-"     "
+"            If you were redirected by a link on an external page,\n"
+"            it is possible that that link was outdated.\n"
+"          "
 msgstr ""
 "\n"
-"      Wenn Sie über einen Link auf einer externen Seite hierher gelangt "
-"sind, ist es möglich, dass dieser veraltet war.\n"
-"     "
+"            Wenn Sie über einen Link auf einer externen Seite hierher "
+"gelangt sind,\n"
+"      ist es möglich, dass dieser veraltet war.\n"
+"          "
 
-#: templates/404.html:16
+#: templates/500.html:10
+msgid ""
+"An unexpected error has\n"
+"          occured."
+msgstr ""
+"Ein unerwarteter Fehler ist\n"
+"            aufgetreten."
+
+#: templates/500.html:13
 msgid ""
 "\n"
-"      If you think this is an error in AlekSIS, please contact your site\n"
-"      administrators:\n"
-"     "
+"            Your site administrators will automatically be notified about "
+"this\n"
+"            error. You can also contact them directly:\n"
+"          "
 msgstr ""
 "\n"
-"      Wenn Sie der Meinung sind, dass es sich um einen Fehler in AlekSIS "
-"handelt, kontaktieren Sie bitte einen Ihrer\n"
-"     Systemadministratoren:\n"
-"     "
+"            Ihre Administratoren werden automatisch über diesen Fehler "
+"informiert.\n"
+"      Sie können diese auch direkt kontaktieren:\n"
+"          "
 
-#: templates/500.html:10
-msgid "Error (500): An unexpected error has occured.."
-msgstr "Error (500): Ein unerwarteter Fehler ist aufgetreten.."
+#: templates/503.html:10
+msgid ""
+"The maintenance mode is currently enabled. Please try again\n"
+"          later."
+msgstr ""
+"Der Wartungsmodus ist aktuell aktiviert. Bitte versuchen Sie es\n"
+"            später erneut."
 
-#: templates/500.html:12
+#: templates/503.html:13
 msgid ""
 "\n"
-"      Your site administrators will automatically be notified about this\n"
-"     error.\n"
-"     "
+"            This page is currently unavailable. If this error persists, "
+"contact your site administrators:\n"
+"          "
 msgstr ""
 "\n"
-"      Ihre Administratoren werden automatisch über diesen Fehler informiert."
+"            Diese Seite ist aktuell nicht erreichbar. Wenn dieser Fehler "
+"bestehen bleibt, kontaktieren Sie bitte einen Ihrer Systemadministratoren:\n"
+"          "
+
+#: templates/core/about.html:6 templates/core/about.html:15
+msgid "About AlekSIS"
+msgstr "Ãœber AlekSIS"
+
+#: templates/core/about.html:7
+msgid "AlekSIS – The Free School Information System"
+msgstr "AlekSIS – The Free School Information System"
+
+#: templates/core/about.html:17
+msgid ""
 "\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"
+"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"
+"            "
 
-#: templates/503.html:10
-msgid "The maintenance mode is currently enabled. Please try again later."
-msgstr "Der Wartungsmodus ist aktuell aktiviert. Bitte versuchen Sie es später erneut."
+#: templates/core/about.html:25
+msgid "Website of AlekSIS"
+msgstr "Website von AlekSIS"
+
+#: templates/core/about.html:26
+msgid "Source code"
+msgstr "Quellcode"
+
+#: templates/core/about.html:35
+msgid "Licence information"
+msgstr "Lizenzinformationen"
 
-#: templates/503.html:12
+#: templates/core/about.html:37
 msgid ""
 "\n"
-"      This page is currently unavailable. If this error stays, contact your site administrators:\n"
-"     "
+"              The core and the official apps of AlekSIS are licenced under "
+"the EUPL, version 1.2 or later. For licence\n"
+"              information from third-party apps, if installed, refer to the "
+"respective components below. The\n"
+"              licences are marked like this:\n"
+"            "
 msgstr ""
 "\n"
-"      Diese Seite ist aktuell nicht erreichbar. Wenn dieser Fehler bestehen "
-"bleibt, kontaktieren Sie bitte einen Ihrer\n"
-"     Systemadministratoren:\n"
-"     "
+"              Der Core und die offiziellen Apps von AlekSIS sind unter der "
+"EUPL, Version 1.2 oder später, lizenziert. Für Lizenzinformationen\n"
+"zu Apps von Drittanbietern, wenn installiert, siehe direkt bei der "
+"jeweiligen App weiter unten auf dieser Seite. Die Lizenzen\n"
+"sind wie folgt markiert:\n"
+"            "
+
+#: templates/core/about.html:45
+msgid "Free/Open Source Licence"
+msgstr "Freie/Open Source Lizenz"
+
+#: templates/core/about.html:46
+msgid "Other Licence"
+msgstr "Andere Lizenz"
+
+#: templates/core/about.html:50
+msgid "Full licence text"
+msgstr "Kompletter Lizenztext"
+
+#: templates/core/about.html:51
+msgid "More information about the EUPL"
+msgstr "Weitere Informationen über die EUPL"
+
+#: templates/core/about.html:90
+#, python-format
+msgid ""
+"\n"
+"                    This app is licenced under %(licence)s.\n"
+"                  "
+msgstr ""
+"\n"
+"                    Diese App ist unter %(licence)s lizenziert.\n"
+"                  "
 
 #: templates/core/announcement/form.html:10
 #: templates/core/announcement/form.html:17
@@ -583,8 +830,8 @@ msgstr "Empfänger"
 msgid "Actions"
 msgstr "Aktionen"
 
-#: templates/core/announcement/list.html:36 templates/core/group_full.html:15
-#: templates/core/person_full.html:15
+#: templates/core/announcement/list.html:36 templates/core/group_full.html:22
+#: templates/core/person_full.html:21
 msgid "Edit"
 msgstr "Bearbeiten"
 
@@ -633,15 +880,24 @@ msgstr ""
 msgid "Logged in as"
 msgstr "Angemeldet als"
 
-#: templates/core/base.html:146
+#: templates/core/base.html:76 templates/search/search.html:7
+#: templates/search/search.html:22
+msgid "Search"
+msgstr "Suchen"
+
+#: templates/core/base.html:148
+msgid "About AlekSIS — The Free School Information System"
+msgstr "Über AlekSIS — The Free School Information System"
+
+#: templates/core/base.html:156
 msgid "Impress"
 msgstr "Impressum"
 
-#: templates/core/base.html:154
+#: templates/core/base.html:164
 msgid "Privacy Policy"
 msgstr "Datenschutzerklärung"
 
-#: templates/core/base_print.html:60
+#: templates/core/base_print.html:62
 msgid "Powered by AlekSIS"
 msgstr "Betrieben mit AlekSIS"
 
@@ -653,85 +909,160 @@ msgstr "Gruppe editieren"
 msgid "Edit person"
 msgstr "Person editieren"
 
-#: templates/core/edit_school.html:8 templates/core/edit_school.html:9
-msgid "Edit school"
-msgstr "Schule bearbeiten"
-
-#: templates/core/group_full.html:19
-msgid "Owners"
-msgstr "Leiter/-innen"
-
-#: templates/core/group_full.html:22
-msgid "Members"
-msgstr "Mitglieder"
+#: templates/core/group_full.html:28 templates/core/person_full.html:28
+msgid "Change preferences"
+msgstr "Einstellungen ändern"
 
 #: templates/core/groups.html:14
 msgid "Create group"
 msgstr "Gruppe erstellen"
 
+#: templates/core/groups_child_groups.html:18
+msgid ""
+"\n"
+"          You can use this to assign child groups to groups. Please use the "
+"filters below to select groups you want to\n"
+"          change and click \"Next\".\n"
+"        "
+msgstr ""
+"\n"
+"          Sie können diese Seite verwenden, um Kindgruppen zu Gruppen "
+"zuzuordnen. Bitte nutzen Sie die folgenden Filter, um die Gruppen "
+"auszuwählen, die Sie \n"
+"          ändern möchten und klicken auf \"Weiter\".\n"
+"        "
+
+#: templates/core/groups_child_groups.html:31
+msgid "Update selection"
+msgstr "Auswahl aktualisieren"
+
+#: templates/core/groups_child_groups.html:35
+msgid "Clear all filters"
+msgstr "Alle Filter leeren"
+
+#: templates/core/groups_child_groups.html:39
+msgid "Currently selected groups"
+msgstr "Aktuell ausgewählte Gruppen"
+
+#: templates/core/groups_child_groups.html:52
+msgid "Start assigning child groups for this groups"
+msgstr "Zuordnung von Kindgruppen zu Gruppen starten"
+
+#: templates/core/groups_child_groups.html:61
+msgid ""
+"\n"
+"            Please select some groups in order to go on with assigning.\n"
+"          "
+msgstr ""
+"\n"
+"            Bitte wählen Sie Gruppen aus, um Gruppen zuzuordnen.\n"
+"          "
+
+#: templates/core/groups_child_groups.html:72
+msgid "Current group:"
+msgstr "Aktuelle Gruppe:"
+
+#: templates/core/groups_child_groups.html:78
+msgid "Please be careful!"
+msgstr "Bitte seien Sie vorsichtig!"
+
+#: templates/core/groups_child_groups.html:79
+msgid ""
+"\n"
+"            If you click \"Back\" or \"Next\" the current group assignments "
+"are not saved.\n"
+"            If you click \"Save\", you will overwrite all existing child "
+"group relations for this group with what you\n"
+"            selected on this page.\n"
+"          "
+msgstr ""
+"\n"
+"            Wenn Sie auf \"Zurück\" oder \"Weiter\" klicken, werden die "
+"aktuellen Gruppenzuordnungen nicht gespeichert.\n"
+"Wenn Sie auf \"Speichern\" klicken, werden alle existierenden Zuordnungen "
+"von Kindgruppen für diese Gruppe\n"
+"mit dem überschrieben, was Sie auf dieser Seite ausgewählt haben.\n"
+"          "
+
+#: templates/core/groups_child_groups.html:93
+#: templates/core/groups_child_groups.html:128
+#: templates/two_factor/_wizard_actions.html:15
+#: templates/two_factor/_wizard_actions.html:20
+msgid "Back"
+msgstr "Zurück"
+
+#: templates/core/groups_child_groups.html:99
+#: templates/core/groups_child_groups.html:134
+#: templates/two_factor/_wizard_actions.html:26
+msgid "Next"
+msgstr "Weiter"
+
+#: templates/core/groups_child_groups.html:106
+#: templates/core/groups_child_groups.html:141
+#: templates/core/save_button.html:3
+msgid "Save"
+msgstr "Speichern"
+
+#: templates/core/groups_child_groups.html:112
+#: templates/core/groups_child_groups.html:147
+msgid "Save and next"
+msgstr "Speichern und weiter"
+
 #: templates/core/index.html:4
 msgid "Home"
 msgstr "Startseite"
 
-#: templates/core/index.html:11
-msgid "AlekSIS (School Information System)"
-msgstr "AlekSIS (Schulinformationssystem)"
-
-#: templates/core/index.html:43
+#: templates/core/index.html:42
 msgid "Last activities"
 msgstr "Letzte Aktivitäten"
 
-#: templates/core/index.html:61
+#: templates/core/index.html:60
 msgid "No activities available yet."
 msgstr "Aktuell keine Aktivitäten verfügbar."
 
-#: templates/core/index.html:66
+#: templates/core/index.html:65
 msgid "Recent notifications"
 msgstr "Letzte Benachrichtigungen"
 
-#: templates/core/index.html:82
+#: templates/core/index.html:81
 msgid "More information →"
 msgstr "Mehr Informationen →"
 
-#: templates/core/index.html:89
+#: templates/core/index.html:88
 msgid "No notifications available yet."
 msgstr "Aktuell keine Benachrichtigungen verfügbar."
 
-#: templates/core/no_person.html:11
+#: templates/core/no_person.html:12
 msgid ""
 "\n"
-"          Your user account is not linked to a person. This means you\n"
-"          cannot access any school-related information. Please contact\n"
-"          the managers of AlekSIS at your school.\n"
-"        "
+"            Your administrator account is not linked to any person. "
+"Therefore,\n"
+"            a dummy person has been linked to your account.\n"
+"          "
 msgstr ""
 "\n"
-"          Ihr Benutzerkonto ist nicht mit einer Person verknüpft. Das "
-"bedeutet, dass Sie\n"
-"        keine schulbezogenen Informationen aufrufen können. Bitte wenden Sie "
-"sich an\n"
-"        die Verwaltenden von AlekSIS an Ihrer Schule.\n"
-"        "
-
-#: templates/core/offline.html:6
-msgid "No internet connection."
-msgstr "Keine Internetverbindung."
+"            Ihr Administratorenkonto ist mit keiner Person verknüpft. "
+"Deshalb\n"
+"            wurde Ihr Konto mit einer Dummyperson verknüpft.\n"
+"          "
 
-#: templates/core/offline.html:9
+#: templates/core/no_person.html:19
 msgid ""
 "\n"
-"        There was an error accessing this page. You probably don't have an internet connection. Check to see if your WiFi or mobile data is turned on and try again. If you think you are connected, please contact the system administrators:\n"
-"      "
+"            Your user account is not linked to a person. This means you\n"
+"            cannot access any school-related information. Please contact\n"
+"            the managers of AlekSIS at your school.\n"
+"          "
 msgstr ""
 "\n"
-"        Es ist ein Fehler beim Aufrufen der Seite aufgetreten. Eventuell "
-"haben Sie keine Internetverbindung. Bitte prüfen Sie, ob WLAN oder mobile "
-"Daten aktiv sind, und probieren Sie es erneut. Wenn Sie der Meinung sind, "
-"dass Sie mit dem Internet verbunden sind, kontaktieren Sie bitte einen Ihrer "
-"Systemadministratoren:\n"
-"      "
+"            Ihr Benutzerkonto ist nicht mit einer Person verknüpft. Das "
+"bedeutet, dass Sie\n"
+"        keine schulbezogenen Informationen aufrufen können. Bitte wenden Sie "
+"sich an\n"
+"        die Verwaltenden von AlekSIS an Ihrer Schule.\n"
+"          "
 
-#: templates/core/person_full.html:19
+#: templates/core/person_full.html:34
 msgid "Contact details"
 msgstr "Kontaktdetails"
 
@@ -744,14 +1075,17 @@ msgstr "Personen mit Benutzerkonten verknüpfen"
 msgid ""
 "\n"
 "        You can use this form to assign user accounts to persons. Use the\n"
-"        dropdowns to select existing accounts; use the text fields to create new\n"
+"        dropdowns to select existing accounts; use the text fields to create "
+"new\n"
 "        accounts on-the-fly. The latter will create a new account with the\n"
 "        entered username and copy all other details from the person.\n"
 "      "
 msgstr ""
 "\n"
-"        Sie können dieses Formular nutzen, um Benutzerkonten Personen zuzuweisen. Nutzen Sie das\n"
-"    Auswahlfeld um ein existierendes Benutzerkonto auszuwählen; nutzen Sie das Textfeld, um einen neuen Benutzer zu\n"
+"        Sie können dieses Formular nutzen, um Benutzerkonten Personen "
+"zuzuweisen. Nutzen Sie das\n"
+"    Auswahlfeld um ein existierendes Benutzerkonto auszuwählen; nutzen Sie "
+"das Textfeld, um einen neuen Benutzer zu\n"
 "    erstellen. Letzteres erstellt ein neues Benutzerkonto mit dem\n"
 "    eingegebenen Benutzernamen und kopiert alle anderen Daten der Person.\n"
 "      "
@@ -769,15 +1103,6 @@ msgstr "Existierendes Konto"
 msgid "New account"
 msgstr "Neues Konto"
 
-#: templates/core/save_button.html:3
-msgid "Save"
-msgstr "Speichern"
-
-#: templates/core/school_management.html:6
-#: templates/core/school_management.html:7
-msgid "School management"
-msgstr "Schulverwaltung"
-
 #: templates/core/system_status.html:12
 msgid "System checks"
 msgstr "Systemprüfungen"
@@ -789,7 +1114,8 @@ msgstr "Wartungsmodus aktiviert"
 #: templates/core/system_status.html:23
 msgid ""
 "\n"
-"                Only admin and visitors from internal IPs can access thesite.\n"
+"                Only admin and visitors from internal IPs can access "
+"thesite.\n"
 "              "
 msgstr ""
 "\n"
@@ -812,7 +1138,8 @@ msgstr "Debug-Modus aktiviert"
 #: templates/core/system_status.html:47
 msgid ""
 "\n"
-"                The web server throws back debug information on errors. Do not use in production!\n"
+"                The web server throws back debug information on errors. Do "
+"not use in production!\n"
 "              "
 msgstr ""
 "\n"
@@ -827,7 +1154,8 @@ msgstr "Debug-Modus deaktivert"
 #: templates/core/system_status.html:56
 msgid ""
 "\n"
-"                Debug mode is disabled. Default error pages are displayed on errors.\n"
+"                Debug mode is disabled. Default error pages are displayed on "
+"errors.\n"
 "              "
 msgstr ""
 "\n"
@@ -835,105 +1163,108 @@ msgstr ""
 "bei Fehlern angezeigt.\n"
 "              "
 
-#: templates/impersonate/list_users.html:8
-msgid "Impersonate user"
-msgstr "Als Benutzer verkleiden"
+#: templates/dynamic_preferences/form.html:9
+msgid "Site preferences"
+msgstr "Seiteneinstellungen ändern"
 
-#: templates/martor/editor.html:27
-msgid "Uploading... please wait..."
-msgstr "Lädt hoch… bitte warten…"
+#: templates/dynamic_preferences/form.html:11
+msgid "My preferences"
+msgstr "Meine Einstellungen"
 
-#: templates/martor/editor.html:36
-msgid "Nothing to preview"
-msgstr "Keine Vorschau"
+#: templates/dynamic_preferences/form.html:13
+#, python-format
+msgid "Preferences for %(instance)s"
+msgstr "Einstellungen für %(instance)s"
 
-#: templates/martor/emoji.html:4
-msgid "Select Emoji to Insert"
-msgstr "Wählen Sie ein Emoji zum Einfügen aus"
+#: templates/dynamic_preferences/form.html:25
+msgid "Save preferences"
+msgstr "Einstellungen speichern"
 
-#: templates/martor/emoji.html:8
-msgid "Preparing emojis..."
-msgstr "Bereite Emoji vor…"
+#: templates/dynamic_preferences/sections.html:7
+msgid "All"
+msgstr "Alle"
 
-#: templates/martor/guide.html:8
-msgid "Markdown Guide"
-msgstr "Markdown-Anleitung"
+#: templates/impersonate/list_users.html:8
+msgid "Impersonate user"
+msgstr "Als Benutzer verkleiden"
 
-#: templates/martor/guide.html:9
-#, python-format
+#: templates/offline.html:6
 msgid ""
-"This site is powered by Markdown. For full\n"
-"            documentation,\n"
-"            <a href=\"%(doc_url)s\" target=\"_blank\">click here</a>"
+"No internet\n"
+"    connection."
 msgstr ""
-"Diese Seite wird mit Markdown betrieben. Für komplette\n"
-"            Dokumentation,\n"
-"            <a href=\"%(doc_url)s\" target=\"_blank\">hier klicken</a>"
+"Keine\n"
+"    Internetverbindung."
 
-#: templates/martor/guide.html:15 templates/martor/toolbar.html:42
-msgid "Code"
-msgstr "Code"
-
-#: templates/martor/guide.html:16
-msgid "Or"
-msgstr "oder"
-
-#: templates/martor/guide.html:19
-msgid "... to Get"
+#: templates/offline.html:10
+msgid ""
+"\n"
+"      There was an error accessing this page. You probably don't have an "
+"internet connection. Check to see if your WiFi\n"
+"      or mobile data is turned on and try again. If you think you are "
+"connected, please contact the system\n"
+"      administrators:\n"
+"    "
 msgstr ""
-
-#: templates/martor/toolbar.html:3
-msgid "Bold"
-msgstr "Fett"
-
-#: templates/martor/toolbar.html:6
-msgid "Italic"
-msgstr "kursiv"
-
-#: templates/martor/toolbar.html:10
-msgid "Horizontal Line"
-msgstr "horizontale Linie"
-
-#: templates/martor/toolbar.html:15
-msgid "Heading"
-msgstr "Ãœberschrift"
-
-#: templates/martor/toolbar.html:20 templates/martor/toolbar.html:23
-#: templates/martor/toolbar.html:26
-msgid "H"
-msgstr "H"
-
-#: templates/martor/toolbar.html:31
-msgid "Pre or Code"
-msgstr "Pre oder Code"
-
-#: templates/martor/toolbar.html:38
-msgid "Pre"
-msgstr "Pre"
-
-#: templates/martor/toolbar.html:48
-msgid "Quote"
-msgstr "Zitat"
-
-#: templates/martor/toolbar.html:52
-msgid "Unordered List"
-msgstr "Unsortierte Liste"
-
-#: templates/martor/toolbar.html:56
-msgid "Ordered List"
-msgstr "Sortierte Liste"
-
-#: templates/martor/toolbar.html:60
-msgid "URL/Link"
-msgstr "URL/Link"
-
-#: templates/martor/toolbar.html:82
-msgid "Full Screen"
-msgstr "Vollbild"
-
-#: templates/martor/toolbar.html:86
-msgid "Markdown Guide (Help)"
-msgstr "Markdown-Anleitung (Hilfe)"
+"\n"
+"      Es ist ein Fehler beim Aufrufen der Seite aufgetreten. Eventuell haben "
+"Sie keine Internetverbindung. Bitte prüfen Sie, ob WLAN oder mobile Daten "
+"aktiv sind, \n"
+"      und probieren Sie es erneut. Wenn Sie der Meinung sind, dass Sie mit "
+"dem Internet verbunden sind, kontaktieren Sie bitte einen Ihrer \n"
+"      Systemadministratoren:\n"
+"    "
+
+#: templates/search/search.html:8
+msgid "Global Search"
+msgstr "Globale Suche"
+
+#: templates/search/search.html:15
+msgid "Search Term"
+msgstr "Suchausdruck"
+
+#: templates/search/search.html:26
+msgid "Results"
+msgstr "Ergebnisse"
+
+#: templates/search/search.html:38
+msgid "No search results could be found to your search."
+msgstr "Es konnten keine Suchergebnisse zu Ihrem Suchausdruck gefunden werden."
+
+#: templates/search/search.html:87
+msgid "Please enter a search term above."
+msgstr "Bitte geben Sie einen Suchausdruck ein."
+
+#: templates/templated_email/notification.email:3
+msgid "New notification for"
+msgstr "Neue Benachrichtigung für"
+
+#: templates/templated_email/notification.email:7
+msgid "Dear"
+msgstr "Sehr geehrter"
+
+#: templates/templated_email/notification.email:8
+msgid "we got a new notification for you:"
+msgstr "Wir haben eine neue Benachrichtigung für Sie:"
+
+#: templates/templated_email/notification.email:12
+msgid "More information"
+msgstr "Mehr Informationen"
+
+#: templates/templated_email/notification.email:16
+#, python-format
+msgid ""
+"\n"
+"    <p>By %(trans_sender)s at %(trans_created_at)s</p>\n"
+"\n"
+"    <i>Your AlekSIS team</i>\n"
+"    "
+msgstr ""
+"\n"
+"    <p>Von %(trans_sender)s um %(trans_created_at)s</p>\n"
+"\n"
+"    <i>Ihr AlekSIS-Team</i>\n"
+"    "
 
 #: templates/two_factor/_base_focus.html:6
 #: templates/two_factor/core/otp_required.html:22
@@ -946,15 +1277,6 @@ msgstr "Zwei-Faktor-Authentifizierung aktivieren"
 msgid "Cancel"
 msgstr "Abbrechen"
 
-#: templates/two_factor/_wizard_actions.html:15
-#: templates/two_factor/_wizard_actions.html:20
-msgid "Back"
-msgstr "Zurück"
-
-#: templates/two_factor/_wizard_actions.html:26
-msgid "Next"
-msgstr "Weiter"
-
 #: templates/two_factor/core/backup_tokens.html:5
 #: templates/two_factor/core/backup_tokens.html:9
 #: templates/two_factor/profile/profile.html:46
@@ -966,8 +1288,10 @@ msgid ""
 "\n"
 "        Backup tokens can be used when your primary and backup\n"
 "        phone numbers aren't available. The backup tokens below can be used\n"
-"        for login verification. If you've used up all your backup tokens, you\n"
-"        can generate a new set of backup tokens. Only the backup tokens shown\n"
+"        for login verification. If you've used up all your backup tokens, "
+"you\n"
+"        can generate a new set of backup tokens. Only the backup tokens "
+"shown\n"
 "        below will be valid.\n"
 "      "
 msgstr ""
@@ -1003,51 +1327,63 @@ msgstr "Zurück zur Kontosicherheit"
 msgid "Generate Tokens"
 msgstr "Tokens generieren"
 
-#: templates/two_factor/core/login.html:17
-msgid "Enter your credentials."
-msgstr "Geben Sie Ihre Zugangsdaten ein."
+#: templates/two_factor/core/login.html:16
+msgid ""
+"You have no permission to view this page. Please login with an other account."
+msgstr ""
+"Sie haben nicht die nötigen Berechtigungen, um diese Seite aufzurufen. Bitte "
+"loggen Sie sich mit einem anderen Account ein."
+
+#: templates/two_factor/core/login.html:25
+msgid "Please login to see this page."
+msgstr "Bitte melden Sie sich an, um diese Seite zu sehen."
 
-#: templates/two_factor/core/login.html:20
+#: templates/two_factor/core/login.html:28
 msgid ""
 "We are calling your phone right now, please enter the\n"
-"            digits you hear."
+"              digits you hear."
 msgstr ""
 "Wir rufen Ihr Telefon jetzt an, bitte geben Sie die\n"
-"              Zahlen ein, die Sie hören."
+"                Zahlen ein, die Sie hören."
 
-#: templates/two_factor/core/login.html:23
+#: templates/two_factor/core/login.html:31
 msgid ""
 "We sent you a text message, please enter the tokens we\n"
-"            sent."
+"              sent."
 msgstr ""
-"Wir haben Ihnen eine Textnachricht geschickt. Bitte geben Sie die Tokens ein,"
-"\n"
-"            die wir Ihnen geschickt haben."
+"Wir haben Ihnen eine Textnachricht geschickt. Bitte geben Sie die Tokens "
+"ein,\n"
+"              die wir Ihnen geschickt haben."
 
-#: templates/two_factor/core/login.html:26
+#: templates/two_factor/core/login.html:34
 msgid ""
 "Please enter the tokens generated by your token\n"
-"            generator."
+"              generator."
 msgstr ""
 "Bitte geben Sie den von Ihrem Token-Generator\n"
-"            generierten Token ein."
+"              generierten Token ein."
 
-#: templates/two_factor/core/login.html:30
+#: templates/two_factor/core/login.html:38
 msgid ""
 "Use this form for entering backup tokens for logging in.\n"
-"          These tokens have been generated for you to print and keep safe. Please\n"
-"          enter one of these backup tokens to login to your account."
+"            These tokens have been generated for you to print and keep safe. "
+"Please\n"
+"            enter one of these backup tokens to login to your account."
 msgstr ""
+"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."
 
-#: templates/two_factor/core/login.html:47
+#: templates/two_factor/core/login.html:56
 msgid "Or, alternatively, use one of your backup phones:"
 msgstr "Oder, alternativ, nutzen Sie eins Ihrer Backup-Telefone:"
 
-#: templates/two_factor/core/login.html:57
+#: templates/two_factor/core/login.html:66
 msgid "As a last resort, you can use a backup token:"
 msgstr "Als letzte Möglichkeit können Sie einen Backup-Token nutzen:"
 
-#: templates/two_factor/core/login.html:60
+#: templates/two_factor/core/login.html:69
 msgid "Use Backup Token"
 msgstr "Backup-Token nutzen"
 
@@ -1058,9 +1394,14 @@ msgstr "Zugriff verwehrt"
 #: templates/two_factor/core/otp_required.html:10
 msgid ""
 "The page you requested, enforces users to verify using\n"
-"          two-factor authentication for security reasons. You need to enable these\n"
+"          two-factor authentication for security reasons. You need to enable "
+"these\n"
 "          security features in order to access this page."
 msgstr ""
+"Die von Ihnen gewünschte Seite erfordert aus Sicherheitsgründen\n"
+"          eine Verifizierung durch Zwei-Faktor-Authentifizierung. Sie müssen "
+"diese\n"
+"          Sicherheitsfunktion aktivieren, um diese Seite aufzurufen."
 
 #: templates/two_factor/core/otp_required.html:14
 msgid ""
@@ -1068,6 +1409,9 @@ msgid ""
 "          account. Enable two-factor authentication for enhanced account\n"
 "          security."
 msgstr ""
+"Die Zwei-Faktor-Authentifizierung ist nicht für Ihren Account aktiviert.\n"
+"          Aktivieren Sie Zwei-Faktor-Authentifizierung für eine verbesserte\n"
+"          Accountsicherheit."
 
 #: templates/two_factor/core/otp_required.html:19
 msgid "Go back"
@@ -1084,12 +1428,17 @@ msgid ""
 "      account. This number will be used if your primary method of\n"
 "      registration is not available."
 msgstr ""
+"Sie werden eine Backup-Telefonnummer zu Ihrem\n"
+"      Account hinzufügen. Diese Nummer wird genutzt, wenn Ihre primäre\n"
+"      Authentifikationsmethode nicht verfügbar ist."
 
 #: templates/two_factor/core/phone_register.html:16
 msgid ""
 "We've sent a token to your phone number. Please\n"
 "      enter the token you've received."
 msgstr ""
+"Wir haben Ihnen einen Token an Ihre Telefonnummer gesendet.\n"
+"      Bitte geben Sie diesen Token ein."
 
 #: templates/two_factor/core/setup.html:9
 msgid ""
@@ -1099,6 +1448,12 @@ msgid ""
 "        authentication.\n"
 "      "
 msgstr ""
+"\n"
+"        Sie sind dabei, Ihre Accountsicherheit auf das\n"
+"       nächste Level zu erhöhen. Bitte folgen Sie den Schritten im Wizard um "
+"die\n"
+"       Zwei-Faktor-Authentifizierug zu aktivieren.\n"
+"      "
 
 #: templates/two_factor/core/setup.html:17
 msgid ""
@@ -1106,6 +1461,10 @@ msgid ""
 "        Please select which authentication method you would like to use:\n"
 "      "
 msgstr ""
+"\n"
+"        Bitte wählen Sie aus, welche Authentifikationsmethode Sie nutzen "
+"wollen:\n"
+"      "
 
 #: templates/two_factor/core/setup.html:23
 msgid ""
@@ -1115,6 +1474,13 @@ msgid ""
 "        Authenticator. Then, enter the token generated by the app.\n"
 "      "
 msgstr ""
+"\n"
+"        Um mit dem Codegenerator zu starten, benutzen Sie bitte Ihr "
+"Smartphone,\n"
+"um diesen QR-Code zu scannen (z. B. den Google Authenticator). Dann geben "
+"Sie \n"
+"den in der App angezeigten Code an:\n"
+"      "
 
 #: templates/two_factor/core/setup.html:34
 msgid ""
@@ -1123,6 +1489,11 @@ msgid ""
 "        text messages on. This number will be validated in the next step.\n"
 "      "
 msgstr ""
+"\n"
+"        Bitte geben Sie die Telefonnummer des Gerätes an,\n"
+"        an die die SMS-Nachrichten geschickt werden sollen. Diese Nummer "
+"wird im nächsten Schritt überprüft.\n"
+"      "
 
 #: templates/two_factor/core/setup.html:41
 msgid ""
@@ -1131,13 +1502,22 @@ msgid ""
 "        This number will be validated in the next step.\n"
 "      "
 msgstr ""
+"\n"
+"        Bitte geben Sie die Telefonnummer an, die wir anrufen sollen.\n"
+"Diese Nummer wird im nächsten Schritt überprüft.\n"
+"      "
 
 #: templates/two_factor/core/setup.html:50
 msgid ""
 "\n"
-"            We are calling your phone right now, please enter the digits you hear.\n"
+"            We are calling your phone right now, please enter the digits you "
+"hear.\n"
 "          "
 msgstr ""
+"\n"
+"            Wir rufen Ihr Telefon jetzt an, bitte geben Sie die Zahlen ein, "
+"die Sie hören.\n"
+"          "
 
 #: templates/two_factor/core/setup.html:56
 msgid ""
@@ -1145,16 +1525,32 @@ msgid ""
 "            We sent you a text message, please enter the tokens we sent.\n"
 "          "
 msgstr ""
+"\n"
+"            Wir haben Ihnen per SMS einen Token geschickt, bitte geben Sie "
+"diesen ein.\n"
+"          "
 
 #: templates/two_factor/core/setup.html:63
 msgid ""
 "\n"
-"          We've encountered an issue with the selected authentication method. Please\n"
-"          go back and verify that you entered your information correctly, try\n"
-"          again, or use a different authentication method instead. If the issue\n"
+"          We've encountered an issue with the selected authentication "
+"method. Please\n"
+"          go back and verify that you entered your information correctly, "
+"try\n"
+"          again, or use a different authentication method instead. If the "
+"issue\n"
 "          persists, contact the site administrator.\n"
 "        "
 msgstr ""
+"\n"
+"          Mit der ausgewählten Authentifizierungsmethode ist ein Fehler "
+"aufgetreten. \n"
+"Bitte gehen Sie zurück und überprüfen, dass Sie die Informationen korrekt "
+"eingegeben haben, versuchen Sie es erneut,\n"
+"oder benutzen Sie stattdessen eine andere Authentifizierungsmethode. \n"
+"Wenn der Fehler bestehen bleibt, kontaktieren Sie bitte einen der "
+"Administratoren.\n"
+"        "
 
 #: templates/two_factor/core/setup.html:73
 msgid ""
@@ -1164,6 +1560,11 @@ msgid ""
 "        account.\n"
 "      "
 msgstr ""
+"\n"
+"        Um Ihren YubiKey zu identifizieren und verifizieren, \n"
+"geben Sie bitte ein Token ein. \n"
+"Dann wird Ihr YubiKey mit Ihrem Konto verknüpft.\n"
+"      "
 
 #: templates/two_factor/core/setup_complete.html:5
 #: templates/two_factor/core/setup_complete.html:9
@@ -1173,7 +1574,8 @@ msgstr "Zwei-Faktor-Authentifizierung erfolgreich aktiviert"
 #: templates/two_factor/core/setup_complete.html:14
 msgid ""
 "\n"
-"        Congratulations, you've successfully enabled two-factor authentication.\n"
+"        Congratulations, you've successfully enabled two-factor "
+"authentication.\n"
 "      "
 msgstr ""
 "\n"
@@ -1195,10 +1597,17 @@ msgstr "Backup-Codes generieren"
 msgid ""
 "\n"
 "          However, it might happen that you don't have access to\n"
-"          your primary token device. To enable account recovery, generate backup codes\n"
+"          your primary token device. To enable account recovery, generate "
+"backup codes\n"
 "          or add a phone number.\n"
 "        "
 msgstr ""
+"\n"
+"          Es kann passieren, dass Sie keinen Zugriff auf Ihren "
+"Tokengenerator haben. \n"
+"Um die Wiederherstellung zu aktivieren,\n"
+"generieren Sie Backupcodes oder fügen eine Telefonnummer hinzu.\n"
+"        "
 
 #: templates/two_factor/core/setup_complete.html:52
 #: templates/two_factor/profile/profile.html:41
@@ -1213,7 +1622,9 @@ msgid "Disable Two-Factor Authentication"
 msgstr "Zwei-Faktor-Authentifizierung deaktiveren"
 
 #: templates/two_factor/profile/disable.html:12
-msgid "You are about to disable two-factor authentication. This weakens your account security, are you sure?"
+msgid ""
+"You are about to disable two-factor authentication. This weakens your "
+"account security, are you sure?"
 msgstr ""
 "Sie sind dabei, Zwei-Faktor-Authentifizierung zu deaktivieren. Das "
 "verschlechtert Ihre Kontosicherheit. Sind Sie sicher?"
@@ -1249,6 +1660,8 @@ msgid ""
 "If your primary method is not available, we are able to\n"
 "        send backup tokens to the phone numbers listed below."
 msgstr ""
+"Wenn Ihre primäre Methode nicht verfügbar ist, sind wir in der Lage,\n"
+"        Backup-Tokens zu den unten aufgelisteten Telefonnummern zu schicken."
 
 #: templates/two_factor/profile/profile.html:33
 msgid "Unregister"
@@ -1259,6 +1672,8 @@ msgid ""
 "If you don't have any device with you, you can access\n"
 "        your account using backup tokens."
 msgstr ""
+"Wenn Sie keines Ihrer Geräte dabei haben, können Sie sich\n"
+"      mit Backup-Tokens einloggen."
 
 #: templates/two_factor/profile/profile.html:50
 #, python-format
@@ -1290,6 +1705,11 @@ msgid ""
 "        also disable two-factor authentication for your account.\n"
 "      "
 msgstr ""
+"\n"
+"        Wir raten Ihnen jedoch dringend davon ab.\n"
+"        Sie können jedoch auch die Zwei-Faktor-Authentifizierung für Ihr "
+"Konto deaktivieren.\n"
+"      "
 
 #: templates/two_factor/profile/profile.html:78
 msgid ""
@@ -1299,45 +1719,202 @@ msgid ""
 "        security.\n"
 "      "
 msgstr ""
+"\n"
+"        Die Zwei-Faktor-Authentifizierung ist nicht für Ihren Account "
+"aktiviert.\n"
+"          Aktivieren Sie Zwei-Faktor-Authentifizierung für eine verbesserte\n"
+"          Accountsicherheit.\n"
+"      "
 
-#: util/notifications.py:66
+#: util/notifications.py:65
 msgid "E-Mail"
 msgstr "E-Mail"
 
-#: util/notifications.py:67
+#: util/notifications.py:66
 msgid "SMS"
 msgstr "SMS"
 
-#: views.py:172
+#: views.py:212
+msgid "The child groups were successfully saved."
+msgstr "Die Untergruppen wurden gespeichert."
+
+#: views.py:240
 msgid "The person has been saved."
 msgstr "Die Person wurde gespeichert."
 
-#: views.py:195
+#: views.py:276
 msgid "The group has been saved."
 msgstr "Die Gruppe wurde gespeichert."
 
-#: views.py:236
-msgid "The school has been saved."
-msgstr "Die Schule wurde gespeichert."
-
-#: views.py:255
-msgid "The term has been saved."
-msgstr "Das Schuljahr wurde gespeichert."
-
-#: views.py:272
-msgid "You are not allowed to mark notifications from other users as read!"
-msgstr ""
-"Es ist Ihnen nicht erlaubt, Benachrichtigungen von anderen Benutzern als "
-"gelesen zu markieren!"
-
-#: views.py:307
+#: views.py:348
 msgid "The announcement has been saved."
 msgstr "Die Ankündigung wurde gespeichert."
 
-#: views.py:320
+#: views.py:364
 msgid "The announcement has been deleted."
 msgstr "Ankündigung wurde gelöscht."
 
+#: views.py:435
+msgid "The preferences have been saved successfully."
+msgstr "Die Einstellungen wurde gespeichert."
+
+#~ msgid "Two factor auth"
+#~ msgstr "Zwei-Faktor-Authentifizierung"
+
+#~ msgid "Manage school"
+#~ msgstr "Schulverwaltung"
+
+#~ msgid "Edit school information"
+#~ msgstr "Schulinformationen bearbeiten"
+
+#~ msgid "Edit school term"
+#~ msgstr "Schuljahr bearbeiten"
+
+#~ msgid "Addtitional field"
+#~ msgstr "Zusätzliches Feld"
+
+#~ msgid "Short name of group"
+#~ msgstr "Kurzer Name der Gruppe"
+
+#~ msgid "Menu name"
+#~ msgstr "Menü-Name"
+
+#~ msgid ""
+#~ "\n"
+#~ "      If you think this is an error in AlekSIS, please contact your site\n"
+#~ "     administrators:\n"
+#~ "     "
+#~ msgstr ""
+#~ "\n"
+#~ "      Wenn Sie der Meinung sind, dass es sich um einen Fehler in AlekSIS "
+#~ "handelt, kontaktieren Sie bitte einen Ihrer\n"
+#~ "     Systemadministratoren:\n"
+#~ "     "
+
+#~ msgid "AlekSIS (School Information System)"
+#~ msgstr "AlekSIS (Schulinformationssystem)"
+
+#~ msgid "Uploading... please wait..."
+#~ msgstr "Lädt hoch… bitte warten…"
+
+#~ msgid "Nothing to preview"
+#~ msgstr "Keine Vorschau"
+
+#~ msgid "Select Emoji to Insert"
+#~ msgstr "Wählen Sie ein Emoji zum Einfügen aus"
+
+#~ msgid "Preparing emojis..."
+#~ msgstr "Bereite Emoji vor…"
+
+#~ msgid "Markdown Guide"
+#~ msgstr "Markdown-Anleitung"
+
+#~ msgid ""
+#~ "This site is powered by Markdown. For full\n"
+#~ "            documentation,\n"
+#~ "            <a href=\"%(doc_url)s\" target=\"_blank\">click here</a>"
+#~ msgstr ""
+#~ "Diese Seite wird mit Markdown betrieben. Für komplette\n"
+#~ "            Dokumentation,\n"
+#~ "            <a href=\"%(doc_url)s\" target=\"_blank\">hier klicken</a>"
+
+#~ msgid "Code"
+#~ msgstr "Code"
+
+#~ msgid "Or"
+#~ msgstr "oder"
+
+#~ msgid "Bold"
+#~ msgstr "Fett"
+
+#~ msgid "Italic"
+#~ msgstr "kursiv"
+
+#~ msgid "Horizontal Line"
+#~ msgstr "horizontale Linie"
+
+#~ msgid "Heading"
+#~ msgstr "Ãœberschrift"
+
+#~ msgid "H"
+#~ msgstr "H"
+
+#~ msgid "Pre or Code"
+#~ msgstr "Pre oder Code"
+
+#~ msgid "Pre"
+#~ msgstr "Pre"
+
+#~ msgid "Quote"
+#~ msgstr "Zitat"
+
+#~ msgid "Unordered List"
+#~ msgstr "Unsortierte Liste"
+
+#~ msgid "Ordered List"
+#~ msgstr "Sortierte Liste"
+
+#~ msgid "Full Screen"
+#~ msgstr "Vollbild"
+
+#~ msgid "Markdown Guide (Help)"
+#~ msgstr "Markdown-Anleitung (Hilfe)"
+
+#~ msgid "You are not allowed to mark notifications from other users as read!"
+#~ msgstr ""
+#~ "Es ist Ihnen nicht erlaubt, Benachrichtigungen von anderen Benutzern als "
+#~ "gelesen zu markieren!"
+
+#, fuzzy
+#~| msgid "School management"
+#~ msgid "School name"
+#~ msgstr "Schulverwaltung"
+
+#~ msgid "School logo"
+#~ msgstr "Schullogo"
+
+#~ msgid "Official name"
+#~ msgstr "Offizieller Name"
+
+#~ msgid "School"
+#~ msgstr "Schule"
+
+#~ msgid "Schools"
+#~ msgstr "Schulen"
+
+#~ msgid "Visible caption of the term"
+#~ msgstr "Sichtbare Beschriftung des Schuljahres"
+
+#~ msgid "Effective start date of term"
+#~ msgstr "Startdatum des Schuljahres"
+
+#~ msgid "Effective end date of term"
+#~ msgstr "Enddatum des Schuljahres"
+
+#~ msgid "School term"
+#~ msgstr "Schuljahr"
+
+#~ msgid "School terms"
+#~ msgstr "Schuljahre"
+
+#~ msgid "Edit school"
+#~ msgstr "Schule bearbeiten"
+
+#~ msgid "School management"
+#~ msgstr "Schulverwaltung"
+
+#~ msgid "The school has been saved."
+#~ msgstr "Die Schule wurde gespeichert."
+
+#~ msgid "The term has been saved."
+#~ msgstr "Das Schuljahr wurde gespeichert."
+
+#~ msgid "Reference ID of import source"
+#~ msgstr "Referenz-ID der Import-Quelle"
+
+#~ msgid "Enter your credentials."
+#~ msgstr "Geben Sie Ihre Zugangsdaten ein."
+
 #~ msgid "Website"
 #~ msgstr "Website"
 
@@ -1365,7 +1942,8 @@ msgstr "Ankündigung wurde gelöscht."
 #~ "    "
 #~ msgstr ""
 #~ "\n"
-#~ "     Der Wartungsmodus ist aktuell aktiviert. Bitte versuchen Sie es später erneut.\n"
+#~ "     Der Wartungsmodus ist aktuell aktiviert. Bitte versuchen Sie es "
+#~ "später erneut.\n"
 #~ "    "
 
 #~ msgid "Details"
@@ -1374,25 +1952,6 @@ msgstr "Ankündigung wurde gelöscht."
 #~ msgid "Group not found"
 #~ msgstr "Gruppe nicht gefunden"
 
-#~ msgid ""
-#~ "\n"
-#~ "          There is no group with this id.\n"
-#~ "        "
-#~ msgstr ""
-#~ "\n"
-#~ "          Es existiert keine Gruppe mit dieser ID.\n"
-#~ "        "
-
-#~ msgid ""
-#~ "\n"
-#~ "        AlekSIS is a web-based school information system (SIS) which can be used to\n"
-#~ "        manage and/or publish organisational data of educational institutions.\n"
-#~ "      "
-#~ msgstr ""
-#~ "\n"
-#~ "        AlekSIS ist ein webbasiertes Schul-Informations-System (SIS), das verwendet werden kann, um organisatorische Daten von Bildungseinrichten zu verwalten oder zu publizieren.\n"
-#~ "      "
-
 #~ msgid "Person not found"
 #~ msgstr "Person nicht gefunden"
 
diff --git a/aleksis/core/locale/de_DE/LC_MESSAGES/djangojs.po b/aleksis/core/locale/de_DE/LC_MESSAGES/djangojs.po
index d62e52377a4ce6af7272b5568c5da2a4698fb057..a872c31afd29d0d617356303698963017e5381a9 100644
--- a/aleksis/core/locale/de_DE/LC_MESSAGES/djangojs.po
+++ b/aleksis/core/locale/de_DE/LC_MESSAGES/djangojs.po
@@ -8,7 +8,7 @@ msgid ""
 msgstr ""
 "Project-Id-Version: PACKAGE VERSION\n"
 "Report-Msgid-Bugs-To: \n"
-"POT-Creation-Date: 2020-03-30 09:18+0000\n"
+"POT-Creation-Date: 2020-05-03 10:36+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,14 +17,14 @@ msgstr ""
 "Content-Type: text/plain; charset=UTF-8\n"
 "Content-Transfer-Encoding: 8bit\n"
 
-#: static/js/main.js:21
+#: aleksis/core/static/js/main.js:21
 msgid "Today"
 msgstr ""
 
-#: static/js/main.js:22
+#: aleksis/core/static/js/main.js:22
 msgid "Cancel"
 msgstr ""
 
-#: static/js/main.js:23
+#: aleksis/core/static/js/main.js:23
 msgid "OK"
 msgstr ""
diff --git a/aleksis/core/locale/fr/LC_MESSAGES/django.po b/aleksis/core/locale/fr/LC_MESSAGES/django.po
index eab71180f8738fad60e761f7a4f138c350b056fd..e248a116e72f4957542b94f55a3dc8d37ab24748 100644
--- a/aleksis/core/locale/fr/LC_MESSAGES/django.po
+++ b/aleksis/core/locale/fr/LC_MESSAGES/django.po
@@ -3,83 +3,114 @@
 # This file is distributed under the same license as the PACKAGE package.
 # FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
 #
-#, fuzzy
 msgid ""
 msgstr ""
 "Project-Id-Version: AlekSIS (School Information System) 0.1\n"
 "Report-Msgid-Bugs-To: \n"
-"POT-Creation-Date: 2020-03-30 09:18+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"
-"Language: \n"
+"POT-Creation-Date: 2020-05-04 15:39+0200\n"
+"PO-Revision-Date: 2020-04-27 13:03+0000\n"
+"Last-Translator: Marlene Grundey <grundema@katharineum.de>\n"
+"Language-Team: French <https://translate.edugit.org/projects/aleksis/aleksis/"
+"fr/>\n"
+"Language: fr\n"
 "MIME-Version: 1.0\n"
 "Content-Type: text/plain; charset=UTF-8\n"
 "Content-Transfer-Encoding: 8bit\n"
-"Plural-Forms: nplurals=2; plural=(n > 1);\n"
+"Plural-Forms: nplurals=2; plural=n > 1;\n"
+"X-Generator: Weblate 4.0.1\n"
 
-#: forms.py:38 forms.py:93
+#: forms.py:46
 msgid "You cannot set a new username when also selecting an existing user."
 msgstr ""
 
-#: forms.py:41 forms.py:96
+#: forms.py:50
 msgid "This username is already in use."
 msgstr ""
 
+#: forms.py:74
+msgid "Base data"
+msgstr ""
+
+#: forms.py:80
+msgid "Address"
+msgstr ""
+
+#: forms.py:81
+#, fuzzy
+#| msgid "Contact details"
+msgid "Contact data"
+msgstr "Détails de contact"
+
 #: forms.py:83
+#, fuzzy
+#| msgid "Contact details"
+msgid "Advanced personal data"
+msgstr "Détails de contact"
+
+#: forms.py:116
 msgid "New user"
 msgstr ""
 
-#: forms.py:83
+#: forms.py:116
 msgid "Create a new account"
 msgstr ""
 
-#: forms.py:149 forms.py:152
+#: forms.py:128
+#, fuzzy
+#| msgid "Contact details"
+msgid "Common data"
+msgstr "Détails de contact"
+
+#: forms.py:129 forms.py:169 menus.py:141 models.py:54
+#: templates/core/persons.html:8 templates/core/persons.html:9
+#, fuzzy
+#| msgid "Person"
+msgid "Persons"
+msgstr "Personne"
+
+#: forms.py:162 forms.py:165 models.py:31
 msgid "Date"
-msgstr ""
+msgstr "Date"
 
-#: forms.py:150 forms.py:153
+#: forms.py:163 forms.py:166 models.py:39
 msgid "Time"
 msgstr ""
 
-#: forms.py:155 menus.py:127 models.py:95 templates/core/persons.html:8
-#: templates/core/persons.html:9
-msgid "Persons"
-msgstr ""
-
-#: forms.py:156 menus.py:133 models.py:232 templates/core/groups.html:8
-#: templates/core/groups.html:9 templates/core/person_full.html:79
+#: forms.py:171 menus.py:149 models.py:253 templates/core/groups.html:8
+#: templates/core/groups.html:9 templates/core/person_full.html:106
+#, fuzzy
+#| msgid "Group"
 msgid "Groups"
-msgstr ""
+msgstr "Groupe"
 
-#: forms.py:160
+#: forms.py:175
 msgid "From when until when should the announcement be displayed?"
 msgstr ""
 
-#: forms.py:163
+#: forms.py:178
 msgid "Who should see the announcement?"
 msgstr ""
 
-#: forms.py:164
+#: forms.py:179
 msgid "Write your announcement:"
 msgstr ""
 
-#: forms.py:203
-msgid "You are not allowed to create announcements which are only valid in the past."
+#: forms.py:216
+msgid ""
+"You are not allowed to create announcements which are only valid in the past."
 msgstr ""
 
-#: forms.py:207
+#: forms.py:220
 msgid "The from date and time must be earlier then the until date and time."
 msgstr ""
 
-#: forms.py:215
+#: forms.py:229
 msgid "You need at least one recipient."
 msgstr ""
 
-#: menus.py:7 templates/registration/login.html:21
-#: templates/two_factor/core/login.html:6
+#: menus.py:7 templates/two_factor/core/login.html:6
 #: templates/two_factor/core/login.html:10
-#: templates/two_factor/core/login.html:64
+#: templates/two_factor/core/login.html:73
 msgid "Login"
 msgstr ""
 
@@ -100,415 +131,656 @@ msgid "Logout"
 msgstr ""
 
 #: menus.py:41
-msgid "Two factor auth"
+msgid "2FA"
+msgstr ""
+
+#: menus.py:47
+msgid "Me"
+msgstr ""
+
+#: menus.py:56 templates/dynamic_preferences/form.html:5
+msgid "Preferences"
 msgstr ""
 
-#: menus.py:52
+#: menus.py:67
 msgid "Admin"
 msgstr ""
 
-#: menus.py:61 models.py:395 templates/core/announcement/list.html:7
+#: menus.py:75 models.py:487 templates/core/announcement/list.html:7
 #: templates/core/announcement/list.html:8
 msgid "Announcements"
 msgstr ""
 
-#: menus.py:70 templates/core/data_management.html:6
+#: menus.py:86 templates/core/data_management.html:6
 #: templates/core/data_management.html:7
 msgid "Data management"
 msgstr ""
 
-#: menus.py:79 templates/core/system_status.html:5
+#: menus.py:94 templates/core/system_status.html:5
 #: templates/core/system_status.html:7
 msgid "System status"
 msgstr ""
 
-#: menus.py:88
+#: menus.py:105
 msgid "Impersonation"
 msgstr ""
 
-#: menus.py:97
-msgid "Manage school"
+#: menus.py:113
+msgid "Configuration"
 msgstr ""
 
-#: menus.py:106
+#: menus.py:124
 msgid "Backend Admin"
 msgstr ""
 
-#: menus.py:117
+#: menus.py:132
 msgid "People"
 msgstr ""
 
-#: menus.py:139
+#: menus.py:157
 msgid "Persons and accounts"
 msgstr ""
 
-#: menus.py:152
-msgid "Edit school information"
+#: menus.py:168
+msgid "Groups and child groups"
 msgstr ""
 
-#: menus.py:153 templates/core/edit_schoolterm.html:8
-#: templates/core/edit_schoolterm.html:9
-msgid "Edit school term"
+#: menus.py:183 templates/core/groups_child_groups.html:7
+#: templates/core/groups_child_groups.html:9
+msgid "Assign child groups to groups"
 msgstr ""
 
-#: models.py:31 models.py:517
-msgid "Name"
+#: models.py:29
+msgid "Boolean (Yes/No)"
 msgstr ""
 
-#: models.py:33
-msgid "Official name"
+#: models.py:30
+msgid "Text (one line)"
 msgstr ""
 
-#: models.py:35
-msgid "Official name of the school, e.g. as given by supervisory authority"
+#: models.py:32
+msgid "Date and time"
 msgstr ""
 
-#: models.py:38
-msgid "School logo"
-msgstr ""
-
-#: models.py:51
-msgid "School"
+#: models.py:33
+msgid "Decimal number"
 msgstr ""
 
-#: models.py:52
-msgid "Schools"
+#: models.py:34 models.py:95
+msgid "E-mail address"
 msgstr ""
 
-#: models.py:60
-msgid "Visible caption of the term"
+#: models.py:35
+msgid "Integer"
 msgstr ""
 
-#: models.py:62
-msgid "Effective start date of term"
+#: models.py:36
+msgid "IP address"
 msgstr ""
 
-#: models.py:63
-msgid "Effective end date of term"
+#: models.py:37
+msgid "Boolean or empty (Yes/No/Neither)"
 msgstr ""
 
-#: models.py:83
-msgid "School term"
+#: models.py:38
+msgid "Text (multi-line)"
 msgstr ""
 
-#: models.py:84
-msgid "School terms"
+#: models.py:40
+msgid "URL / Link"
 msgstr ""
 
-#: models.py:94 templates/core/persons_accounts.html:36
+#: models.py:53 templates/core/persons_accounts.html:36
 msgid "Person"
-msgstr ""
+msgstr "Personne"
 
-#: models.py:97
+#: models.py:56
+#, fuzzy
+#| msgid "Contact details"
+msgid "Can view address"
+msgstr "Détails de contact"
+
+#: models.py:57
+#, fuzzy
+#| msgid "Contact details"
+msgid "Can view contact details"
+msgstr "Détails de contact"
+
+#: models.py:58
+#, fuzzy
+#| msgid "Contact details"
+msgid "Can view photo"
+msgstr "Détails de contact"
+
+#: models.py:59
+#, fuzzy
+#| msgid "Contact details"
+msgid "Can view persons groups"
+msgstr "Détails de contact"
+
+#: models.py:60
+#, fuzzy
+#| msgid "Contact details"
+msgid "Can view personal details"
+msgstr "Détails de contact"
+
+#: models.py:65
 msgid "female"
 msgstr ""
 
-#: models.py:97
+#: models.py:65
 msgid "male"
 msgstr ""
 
-#: models.py:102
+#: models.py:73
+msgid "Linked user"
+msgstr ""
+
+#: models.py:75
 msgid "Is person active?"
 msgstr ""
 
-#: models.py:104
+#: models.py:77
 msgid "First name"
-msgstr ""
+msgstr "Prénom"
 
-#: models.py:105
+#: models.py:78
 msgid "Last name"
-msgstr ""
+msgstr "Nom de famille"
 
-#: models.py:107
+#: models.py:80
 msgid "Additional name(s)"
 msgstr ""
 
-#: models.py:111
+#: models.py:84 models.py:260
+#, fuzzy
+#| msgid "First name"
 msgid "Short name"
-msgstr ""
+msgstr "Prénom"
 
-#: models.py:114
+#: models.py:87
 msgid "Street"
 msgstr ""
 
-#: models.py:115
+#: models.py:88
 msgid "Street number"
 msgstr ""
 
-#: models.py:116
+#: models.py:89
 msgid "Postal code"
 msgstr ""
 
-#: models.py:117
+#: models.py:90
 msgid "Place"
 msgstr ""
 
-#: models.py:119
+#: models.py:92
 msgid "Home phone"
 msgstr ""
 
-#: models.py:120
+#: models.py:93
 msgid "Mobile phone"
 msgstr ""
 
-#: models.py:122
-msgid "E-mail address"
+#: models.py:97
+msgid "Date of birth"
+msgstr "Date d'anniversaire"
+
+#: models.py:98
+msgid "Sex"
+msgstr "Sexe"
+
+#: models.py:100
+msgid "Photo"
 msgstr ""
 
-#: models.py:124
-msgid "Date of birth"
+#: models.py:105
+msgid "Guardians / Parents"
 msgstr ""
 
-#: models.py:125
-msgid "Sex"
+#: models.py:112
+msgid "Primary group"
 msgstr ""
 
-#: models.py:127
-msgid "Photo"
+#: models.py:115 models.py:346 models.py:370 models.py:455 models.py:643
+msgid "Description"
+msgstr "Description"
+
+#: models.py:233
+msgid "Title of field"
 msgstr ""
 
-#: models.py:131 models.py:249
-msgid "Reference ID of import source"
+#: models.py:235
+msgid "Type of field"
 msgstr ""
 
-#: models.py:140
-msgid "Guardians / Parents"
+#: models.py:239
+msgid "Addtitional field for groups"
 msgstr ""
 
-#: models.py:231
-msgid "Group"
+#: models.py:240
+msgid "Addtitional fields for groups"
 msgstr ""
 
-#: models.py:234
-msgid "Long name of group"
+#: models.py:252
+msgid "Group"
+msgstr "Groupe"
+
+#: models.py:254
+msgid "Can assign child groups to groups"
 msgstr ""
 
-#: models.py:235
-msgid "Short name of group"
+#: models.py:258
+#, fuzzy
+#| msgid "Last name"
+msgid "Long name"
+msgstr "Nom de famille"
+
+#: models.py:268 templates/core/group_full.html:37
+msgid "Members"
 msgstr ""
 
-#: models.py:244
+#: models.py:271 templates/core/group_full.html:34
+msgid "Owners"
+msgstr "Propriétaires"
+
+#: models.py:278
 msgid "Parent groups"
 msgstr ""
 
-#: models.py:268 models.py:285 models.py:363
-#: templates/core/announcement/list.html:18
-msgid "Title"
+#: models.py:286
+msgid "Type of group"
 msgstr ""
 
-#: models.py:269 models.py:286 models.py:364
-msgid "Description"
+#: models.py:290
+msgid "Additional fields"
+msgstr ""
+
+#: models.py:342
+msgid "User"
+msgstr ""
+
+#: models.py:345 models.py:369 models.py:454
+#: templates/core/announcement/list.html:18
+msgid "Title"
 msgstr ""
 
-#: models.py:271
+#: models.py:348
 msgid "Application"
 msgstr ""
 
-#: models.py:277
+#: models.py:354
 msgid "Activity"
 msgstr ""
 
-#: models.py:278
+#: models.py:355
 msgid "Activities"
 msgstr ""
 
-#: models.py:282
+#: models.py:361
 msgid "Sender"
 msgstr ""
 
-#: models.py:287 models.py:365 models.py:518
+#: models.py:366
+msgid "Recipient"
+msgstr ""
+
+#: models.py:371 models.py:624
 msgid "Link"
 msgstr ""
 
-#: models.py:289
+#: models.py:373
 msgid "Read"
 msgstr ""
 
-#: models.py:290
+#: models.py:374
 msgid "Sent"
 msgstr ""
 
-#: models.py:301
+#: models.py:387
 msgid "Notification"
 msgstr ""
 
-#: models.py:302
+#: models.py:388
 msgid "Notifications"
 msgstr ""
 
-#: models.py:368
+#: models.py:456
+msgid "Link to detailed view"
+msgstr ""
+
+#: models.py:459
 msgid "Date and time from when to show"
 msgstr ""
 
-#: models.py:371
+#: models.py:462
 msgid "Date and time until when to show"
 msgstr ""
 
-#: models.py:394
+#: models.py:486
 msgid "Announcement"
 msgstr ""
 
-#: models.py:422
+#: models.py:524
 msgid "Announcement recipient"
 msgstr ""
 
-#: models.py:423
+#: models.py:525
 msgid "Announcement recipients"
 msgstr ""
 
-#: models.py:473
+#: models.py:575
 msgid "Widget Title"
 msgstr ""
 
-#: models.py:474
+#: models.py:576
 msgid "Activate Widget"
 msgstr ""
 
-#: models.py:486
+#: models.py:594
 msgid "Dashboard Widget"
 msgstr ""
 
-#: models.py:487
+#: models.py:595
 msgid "Dashboard Widgets"
 msgstr ""
 
-#: models.py:491
+#: models.py:601
 msgid "Menu ID"
 msgstr ""
 
-#: models.py:492
-msgid "Menu name"
-msgstr ""
-
-#: models.py:509
+#: models.py:613
 msgid "Custom menu"
 msgstr ""
 
-#: models.py:510
+#: models.py:614
 msgid "Custom menus"
 msgstr ""
 
-#: models.py:515
+#: models.py:621
 msgid "Menu"
 msgstr ""
 
-#: models.py:520
+#: models.py:623
+msgid "Name"
+msgstr ""
+
+#: models.py:625
 msgid "Icon"
 msgstr ""
 
-#: models.py:527
+#: models.py:631
 msgid "Custom menu item"
 msgstr ""
 
-#: models.py:528
+#: models.py:632
 msgid "Custom menu items"
 msgstr ""
 
-#: settings.py:254
-msgid "German"
+#: models.py:642
+msgid "Title of type"
 msgstr ""
 
-#: settings.py:255
-msgid "English"
+#: models.py:646
+#, fuzzy
+#| msgid "Group"
+msgid "Group type"
+msgstr "Groupe"
+
+#: models.py:647
+#, fuzzy
+#| msgid "Group"
+msgid "Group types"
+msgstr "Groupe"
+
+#: models.py:656
+#, fuzzy
+#| msgid "Contact details"
+msgid "Can view system status"
+msgstr "Détails de contact"
+
+#: models.py:657
+#, fuzzy
+#| msgid "Contact details"
+msgid "Can link persons to accounts"
+msgstr "Détails de contact"
+
+#: models.py:658
+msgid "Can manage data"
+msgstr ""
+
+#: models.py:659
+#, fuzzy
+#| msgid "Contact details"
+msgid "Can impersonate"
+msgstr "Détails de contact"
+
+#: models.py:660
+msgid "Can use search"
+msgstr ""
+
+#: models.py:661
+msgid "Can change site preferences"
+msgstr ""
+
+#: models.py:662
+msgid "Can change person preferences"
 msgstr ""
 
-#: settings.py:373
+#: models.py:663
+msgid "Can change group preferences"
+msgstr ""
+
+#: preferences.py:27
 msgid "Site title"
 msgstr ""
 
-#: settings.py:374
+#: preferences.py:36
+#, fuzzy
+#| msgid "Description"
 msgid "Site description"
-msgstr ""
+msgstr "Description"
 
-#: settings.py:375
+#: preferences.py:45
 msgid "Primary colour"
 msgstr ""
 
-#: settings.py:376
+#: preferences.py:54
 msgid "Secondary colour"
 msgstr ""
 
-#: settings.py:377
-msgid "Mail out name"
+#: preferences.py:62
+msgid "Logo"
+msgstr ""
+
+#: preferences.py:70
+msgid "Favicon"
+msgstr ""
+
+#: preferences.py:78
+msgid "PWA-Icon"
 msgstr ""
 
-#: settings.py:378
+#: preferences.py:87
+#, fuzzy
+#| msgid "Last name"
+msgid "Mail out name"
+msgstr "Nom de famille"
+
+#: preferences.py:96
 msgid "Mail out address"
 msgstr ""
 
-#: settings.py:379
+#: preferences.py:106
 msgid "Link to privacy policy"
 msgstr ""
 
-#: settings.py:380
+#: preferences.py:116
 msgid "Link to imprint"
 msgstr ""
 
-#: settings.py:381
-msgid "Name format of adresses"
+#: preferences.py:126
+msgid "Name format for addressing"
 msgstr ""
 
-#: settings.py:382
-msgid "Channels to allow for notifications"
+#: preferences.py:140
+msgid "Channels to use for notifications"
 msgstr ""
 
-#: settings.py:383
+#: preferences.py:150
 msgid "Regular expression to match primary group, e.g. '^Class .*'"
 msgstr ""
 
+#: preferences.py:159
+msgid "Field on person to match primary group against"
+msgstr ""
+
+#: preferences.py:171
+msgid "Display name of the school"
+msgstr ""
+
+#: preferences.py:180
+msgid "Official name of the school, e.g. as given by supervisory authority"
+msgstr ""
+
+#: settings.py:276
+msgid "English"
+msgstr ""
+
+#: settings.py:277
+msgid "German"
+msgstr ""
+
+#: settings.py:278
+msgid "French"
+msgstr ""
+
+#: templates/403.html:10 templates/404.html:10 templates/500.html:10
+msgid "Error"
+msgstr ""
+
 #: templates/403.html:10
-msgid "Error (403): You are not allowed to access the requested page or object."
+msgid ""
+"You are not allowed to access the requested page or\n"
+"          object."
 msgstr ""
 
-#: templates/403.html:12
+#: templates/403.html:13 templates/404.html:17
 msgid ""
 "\n"
-"      If you think this is an error in AlekSIS, please contact your site\n"
-"     administrators:\n"
-"     "
+"            If you think this is an error in AlekSIS, please contact your "
+"site\n"
+"            administrators:\n"
+"          "
 msgstr ""
 
 #: templates/404.html:10
-msgid "Error (404): The requested page or object was not found."
+msgid ""
+"The requested page or object was not\n"
+"          found."
 msgstr ""
 
-#: templates/404.html:12
+#: templates/404.html:13
 msgid ""
 "\n"
-"      If you were redirected by a link on an external page,\n"
-"      it is possible that that link was outdated.\n"
-"     "
+"            If you were redirected by a link on an external page,\n"
+"            it is possible that that link was outdated.\n"
+"          "
 msgstr ""
 
-#: templates/404.html:16
+#: templates/500.html:10
+msgid ""
+"An unexpected error has\n"
+"          occured."
+msgstr ""
+
+#: templates/500.html:13
 msgid ""
 "\n"
-"      If you think this is an error in AlekSIS, please contact your site\n"
-"      administrators:\n"
-"     "
+"            Your site administrators will automatically be notified about "
+"this\n"
+"            error. You can also contact them directly:\n"
+"          "
 msgstr ""
 
-#: templates/500.html:10
-msgid "Error (500): An unexpected error has occured.."
+#: templates/503.html:10
+msgid ""
+"The maintenance mode is currently enabled. Please try again\n"
+"          later."
 msgstr ""
 
-#: templates/500.html:12
+#: templates/503.html:13
 msgid ""
 "\n"
-"      Your site administrators will automatically be notified about this\n"
-"     error.\n"
-"     "
+"            This page is currently unavailable. If this error persists, "
+"contact your site administrators:\n"
+"          "
 msgstr ""
 
-#: templates/503.html:10
-msgid "The maintenance mode is currently enabled. Please try again later."
+#: templates/core/about.html:6 templates/core/about.html:15
+msgid "About AlekSIS"
+msgstr ""
+
+#: templates/core/about.html:7
+msgid "AlekSIS – The Free School Information System"
 msgstr ""
 
-#: templates/503.html:12
+#: templates/core/about.html:17
 msgid ""
 "\n"
-"      This page is currently unavailable. If this error stays, contact your site administrators:\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 ""
+
+#: templates/core/about.html:25
+msgid "Website of AlekSIS"
+msgstr ""
+
+#: templates/core/about.html:26
+msgid "Source code"
+msgstr ""
+
+#: templates/core/about.html:35
+msgid "Licence information"
+msgstr ""
+
+#: templates/core/about.html:37
+msgid ""
+"\n"
+"              The core and the official apps of AlekSIS are licenced under "
+"the EUPL, version 1.2 or later. For licence\n"
+"              information from third-party apps, if installed, refer to the "
+"respective components below. The\n"
+"              licences are marked like this:\n"
+"            "
+msgstr ""
+
+#: templates/core/about.html:45
+msgid "Free/Open Source Licence"
+msgstr ""
+
+#: templates/core/about.html:46
+msgid "Other Licence"
+msgstr ""
+
+#: templates/core/about.html:50
+msgid "Full licence text"
+msgstr ""
+
+#: templates/core/about.html:51
+msgid "More information about the EUPL"
+msgstr ""
+
+#: templates/core/about.html:90
+#, python-format
+msgid ""
+"\n"
+"                    This app is licenced under %(licence)s.\n"
+"                  "
 msgstr ""
 
 #: templates/core/announcement/form.html:10
@@ -545,8 +817,8 @@ msgstr ""
 msgid "Actions"
 msgstr ""
 
-#: templates/core/announcement/list.html:36 templates/core/group_full.html:15
-#: templates/core/person_full.html:15
+#: templates/core/announcement/list.html:36 templates/core/group_full.html:22
+#: templates/core/person_full.html:21
 msgid "Edit"
 msgstr ""
 
@@ -586,15 +858,24 @@ msgstr ""
 msgid "Logged in as"
 msgstr ""
 
-#: templates/core/base.html:146
+#: templates/core/base.html:76 templates/search/search.html:7
+#: templates/search/search.html:22
+msgid "Search"
+msgstr ""
+
+#: templates/core/base.html:148
+msgid "About AlekSIS — The Free School Information System"
+msgstr ""
+
+#: templates/core/base.html:156
 msgid "Impress"
 msgstr ""
 
-#: templates/core/base.html:154
+#: templates/core/base.html:164
 msgid "Privacy Policy"
 msgstr ""
 
-#: templates/core/base_print.html:60
+#: templates/core/base_print.html:62
 msgid "Powered by AlekSIS"
 msgstr ""
 
@@ -606,73 +887,134 @@ msgstr ""
 msgid "Edit person"
 msgstr ""
 
-#: templates/core/edit_school.html:8 templates/core/edit_school.html:9
-msgid "Edit school"
+#: templates/core/group_full.html:28 templates/core/person_full.html:28
+msgid "Change preferences"
 msgstr ""
 
-#: templates/core/group_full.html:19
-msgid "Owners"
+#: templates/core/groups.html:14
+msgid "Create group"
 msgstr ""
 
-#: templates/core/group_full.html:22
-msgid "Members"
+#: templates/core/groups_child_groups.html:18
+msgid ""
+"\n"
+"          You can use this to assign child groups to groups. Please use the "
+"filters below to select groups you want to\n"
+"          change and click \"Next\".\n"
+"        "
 msgstr ""
 
-#: templates/core/groups.html:14
-msgid "Create group"
+#: templates/core/groups_child_groups.html:31
+msgid "Update selection"
 msgstr ""
 
-#: templates/core/index.html:4
-msgid "Home"
+#: templates/core/groups_child_groups.html:35
+msgid "Clear all filters"
+msgstr ""
+
+#: templates/core/groups_child_groups.html:39
+msgid "Currently selected groups"
+msgstr ""
+
+#: templates/core/groups_child_groups.html:52
+msgid "Start assigning child groups for this groups"
+msgstr ""
+
+#: templates/core/groups_child_groups.html:61
+msgid ""
+"\n"
+"            Please select some groups in order to go on with assigning.\n"
+"          "
+msgstr ""
+
+#: templates/core/groups_child_groups.html:72
+msgid "Current group:"
+msgstr ""
+
+#: templates/core/groups_child_groups.html:78
+msgid "Please be careful!"
+msgstr ""
+
+#: templates/core/groups_child_groups.html:79
+msgid ""
+"\n"
+"            If you click \"Back\" or \"Next\" the current group assignments "
+"are not saved.\n"
+"            If you click \"Save\", you will overwrite all existing child "
+"group relations for this group with what you\n"
+"            selected on this page.\n"
+"          "
+msgstr ""
+
+#: templates/core/groups_child_groups.html:93
+#: templates/core/groups_child_groups.html:128
+#: templates/two_factor/_wizard_actions.html:15
+#: templates/two_factor/_wizard_actions.html:20
+msgid "Back"
+msgstr ""
+
+#: templates/core/groups_child_groups.html:99
+#: templates/core/groups_child_groups.html:134
+#: templates/two_factor/_wizard_actions.html:26
+msgid "Next"
+msgstr ""
+
+#: templates/core/groups_child_groups.html:106
+#: templates/core/groups_child_groups.html:141
+#: templates/core/save_button.html:3
+msgid "Save"
 msgstr ""
 
-#: templates/core/index.html:11
-msgid "AlekSIS (School Information System)"
+#: templates/core/groups_child_groups.html:112
+#: templates/core/groups_child_groups.html:147
+msgid "Save and next"
+msgstr ""
+
+#: templates/core/index.html:4
+msgid "Home"
 msgstr ""
 
-#: templates/core/index.html:43
+#: templates/core/index.html:42
 msgid "Last activities"
 msgstr ""
 
-#: templates/core/index.html:61
+#: templates/core/index.html:60
 msgid "No activities available yet."
 msgstr ""
 
-#: templates/core/index.html:66
+#: templates/core/index.html:65
 msgid "Recent notifications"
 msgstr ""
 
-#: templates/core/index.html:82
+#: templates/core/index.html:81
 msgid "More information →"
 msgstr ""
 
-#: templates/core/index.html:89
+#: templates/core/index.html:88
 msgid "No notifications available yet."
 msgstr ""
 
-#: templates/core/no_person.html:11
+#: templates/core/no_person.html:12
 msgid ""
 "\n"
-"          Your user account is not linked to a person. This means you\n"
-"          cannot access any school-related information. Please contact\n"
-"          the managers of AlekSIS at your school.\n"
-"        "
-msgstr ""
-
-#: templates/core/offline.html:6
-msgid "No internet connection."
+"            Your administrator account is not linked to any person. "
+"Therefore,\n"
+"            a dummy person has been linked to your account.\n"
+"          "
 msgstr ""
 
-#: templates/core/offline.html:9
+#: templates/core/no_person.html:19
 msgid ""
 "\n"
-"        There was an error accessing this page. You probably don't have an internet connection. Check to see if your WiFi or mobile data is turned on and try again. If you think you are connected, please contact the system administrators:\n"
-"      "
+"            Your user account is not linked to a person. This means you\n"
+"            cannot access any school-related information. Please contact\n"
+"            the managers of AlekSIS at your school.\n"
+"          "
 msgstr ""
 
-#: templates/core/person_full.html:19
+#: templates/core/person_full.html:34
 msgid "Contact details"
-msgstr ""
+msgstr "Détails de contact"
 
 #: templates/core/persons_accounts.html:7
 #: templates/core/persons_accounts.html:9
@@ -683,7 +1025,8 @@ msgstr ""
 msgid ""
 "\n"
 "        You can use this form to assign user accounts to persons. Use the\n"
-"        dropdowns to select existing accounts; use the text fields to create new\n"
+"        dropdowns to select existing accounts; use the text fields to create "
+"new\n"
 "        accounts on-the-fly. The latter will create a new account with the\n"
 "        entered username and copy all other details from the person.\n"
 "      "
@@ -702,15 +1045,6 @@ msgstr ""
 msgid "New account"
 msgstr ""
 
-#: templates/core/save_button.html:3
-msgid "Save"
-msgstr ""
-
-#: templates/core/school_management.html:6
-#: templates/core/school_management.html:7
-msgid "School management"
-msgstr ""
-
 #: templates/core/system_status.html:12
 msgid "System checks"
 msgstr ""
@@ -722,7 +1056,8 @@ msgstr ""
 #: templates/core/system_status.html:23
 msgid ""
 "\n"
-"                Only admin and visitors from internal IPs can access thesite.\n"
+"                Only admin and visitors from internal IPs can access "
+"thesite.\n"
 "              "
 msgstr ""
 
@@ -741,7 +1076,8 @@ msgstr ""
 #: templates/core/system_status.html:47
 msgid ""
 "\n"
-"                The web server throws back debug information on errors. Do not use in production!\n"
+"                The web server throws back debug information on errors. Do "
+"not use in production!\n"
 "              "
 msgstr ""
 
@@ -752,105 +1088,97 @@ msgstr ""
 #: templates/core/system_status.html:56
 msgid ""
 "\n"
-"                Debug mode is disabled. Default error pages are displayed on errors.\n"
+"                Debug mode is disabled. Default error pages are displayed on "
+"errors.\n"
 "              "
 msgstr ""
 
-#: templates/impersonate/list_users.html:8
-msgid "Impersonate user"
-msgstr ""
-
-#: templates/martor/editor.html:27
-msgid "Uploading... please wait..."
-msgstr ""
-
-#: templates/martor/editor.html:36
-msgid "Nothing to preview"
-msgstr ""
-
-#: templates/martor/emoji.html:4
-msgid "Select Emoji to Insert"
-msgstr ""
-
-#: templates/martor/emoji.html:8
-msgid "Preparing emojis..."
+#: templates/dynamic_preferences/form.html:9
+msgid "Site preferences"
 msgstr ""
 
-#: templates/martor/guide.html:8
-msgid "Markdown Guide"
+#: templates/dynamic_preferences/form.html:11
+msgid "My preferences"
 msgstr ""
 
-#: templates/martor/guide.html:9
+#: templates/dynamic_preferences/form.html:13
 #, python-format
-msgid ""
-"This site is powered by Markdown. For full\n"
-"            documentation,\n"
-"            <a href=\"%(doc_url)s\" target=\"_blank\">click here</a>"
-msgstr ""
-
-#: templates/martor/guide.html:15 templates/martor/toolbar.html:42
-msgid "Code"
+msgid "Preferences for %(instance)s"
 msgstr ""
 
-#: templates/martor/guide.html:16
-msgid "Or"
+#: templates/dynamic_preferences/form.html:25
+msgid "Save preferences"
 msgstr ""
 
-#: templates/martor/guide.html:19
-msgid "... to Get"
+#: templates/dynamic_preferences/sections.html:7
+msgid "All"
 msgstr ""
 
-#: templates/martor/toolbar.html:3
-msgid "Bold"
+#: templates/impersonate/list_users.html:8
+msgid "Impersonate user"
 msgstr ""
 
-#: templates/martor/toolbar.html:6
-msgid "Italic"
+#: templates/offline.html:6
+msgid ""
+"No internet\n"
+"    connection."
 msgstr ""
 
-#: templates/martor/toolbar.html:10
-msgid "Horizontal Line"
+#: templates/offline.html:10
+msgid ""
+"\n"
+"      There was an error accessing this page. You probably don't have an "
+"internet connection. Check to see if your WiFi\n"
+"      or mobile data is turned on and try again. If you think you are "
+"connected, please contact the system\n"
+"      administrators:\n"
+"    "
 msgstr ""
 
-#: templates/martor/toolbar.html:15
-msgid "Heading"
+#: templates/search/search.html:8
+msgid "Global Search"
 msgstr ""
 
-#: templates/martor/toolbar.html:20 templates/martor/toolbar.html:23
-#: templates/martor/toolbar.html:26
-msgid "H"
+#: templates/search/search.html:15
+msgid "Search Term"
 msgstr ""
 
-#: templates/martor/toolbar.html:31
-msgid "Pre or Code"
+#: templates/search/search.html:26
+msgid "Results"
 msgstr ""
 
-#: templates/martor/toolbar.html:38
-msgid "Pre"
+#: templates/search/search.html:38
+msgid "No search results could be found to your search."
 msgstr ""
 
-#: templates/martor/toolbar.html:48
-msgid "Quote"
+#: templates/search/search.html:87
+msgid "Please enter a search term above."
 msgstr ""
 
-#: templates/martor/toolbar.html:52
-msgid "Unordered List"
+#: templates/templated_email/notification.email:3
+msgid "New notification for"
 msgstr ""
 
-#: templates/martor/toolbar.html:56
-msgid "Ordered List"
+#: templates/templated_email/notification.email:7
+msgid "Dear"
 msgstr ""
 
-#: templates/martor/toolbar.html:60
-msgid "URL/Link"
+#: templates/templated_email/notification.email:8
+msgid "we got a new notification for you:"
 msgstr ""
 
-#: templates/martor/toolbar.html:82
-msgid "Full Screen"
+#: templates/templated_email/notification.email:12
+msgid "More information"
 msgstr ""
 
-#: templates/martor/toolbar.html:86
-msgid "Markdown Guide (Help)"
+#: templates/templated_email/notification.email:16
+#, python-format
+msgid ""
+"\n"
+"    <p>By %(trans_sender)s at %(trans_created_at)s</p>\n"
+"\n"
+"    <i>Your AlekSIS team</i>\n"
+"    "
 msgstr ""
 
 #: templates/two_factor/_base_focus.html:6
@@ -864,15 +1192,6 @@ msgstr ""
 msgid "Cancel"
 msgstr ""
 
-#: templates/two_factor/_wizard_actions.html:15
-#: templates/two_factor/_wizard_actions.html:20
-msgid "Back"
-msgstr ""
-
-#: templates/two_factor/_wizard_actions.html:26
-msgid "Next"
-msgstr ""
-
 #: templates/two_factor/core/backup_tokens.html:5
 #: templates/two_factor/core/backup_tokens.html:9
 #: templates/two_factor/profile/profile.html:46
@@ -884,8 +1203,10 @@ msgid ""
 "\n"
 "        Backup tokens can be used when your primary and backup\n"
 "        phone numbers aren't available. The backup tokens below can be used\n"
-"        for login verification. If you've used up all your backup tokens, you\n"
-"        can generate a new set of backup tokens. Only the backup tokens shown\n"
+"        for login verification. If you've used up all your backup tokens, "
+"you\n"
+"        can generate a new set of backup tokens. Only the backup tokens "
+"shown\n"
 "        below will be valid.\n"
 "      "
 msgstr ""
@@ -909,44 +1230,50 @@ msgstr ""
 msgid "Generate Tokens"
 msgstr ""
 
-#: templates/two_factor/core/login.html:17
-msgid "Enter your credentials."
+#: templates/two_factor/core/login.html:16
+msgid ""
+"You have no permission to view this page. Please login with an other account."
+msgstr ""
+
+#: templates/two_factor/core/login.html:25
+msgid "Please login to see this page."
 msgstr ""
 
-#: templates/two_factor/core/login.html:20
+#: templates/two_factor/core/login.html:28
 msgid ""
 "We are calling your phone right now, please enter the\n"
-"            digits you hear."
+"              digits you hear."
 msgstr ""
 
-#: templates/two_factor/core/login.html:23
+#: templates/two_factor/core/login.html:31
 msgid ""
 "We sent you a text message, please enter the tokens we\n"
-"            sent."
+"              sent."
 msgstr ""
 
-#: templates/two_factor/core/login.html:26
+#: templates/two_factor/core/login.html:34
 msgid ""
 "Please enter the tokens generated by your token\n"
-"            generator."
+"              generator."
 msgstr ""
 
-#: templates/two_factor/core/login.html:30
+#: templates/two_factor/core/login.html:38
 msgid ""
 "Use this form for entering backup tokens for logging in.\n"
-"          These tokens have been generated for you to print and keep safe. Please\n"
-"          enter one of these backup tokens to login to your account."
+"            These tokens have been generated for you to print and keep safe. "
+"Please\n"
+"            enter one of these backup tokens to login to your account."
 msgstr ""
 
-#: templates/two_factor/core/login.html:47
+#: templates/two_factor/core/login.html:56
 msgid "Or, alternatively, use one of your backup phones:"
 msgstr ""
 
-#: templates/two_factor/core/login.html:57
+#: templates/two_factor/core/login.html:66
 msgid "As a last resort, you can use a backup token:"
 msgstr ""
 
-#: templates/two_factor/core/login.html:60
+#: templates/two_factor/core/login.html:69
 msgid "Use Backup Token"
 msgstr ""
 
@@ -957,7 +1284,8 @@ msgstr ""
 #: templates/two_factor/core/otp_required.html:10
 msgid ""
 "The page you requested, enforces users to verify using\n"
-"          two-factor authentication for security reasons. You need to enable these\n"
+"          two-factor authentication for security reasons. You need to enable "
+"these\n"
 "          security features in order to access this page."
 msgstr ""
 
@@ -1034,7 +1362,8 @@ msgstr ""
 #: templates/two_factor/core/setup.html:50
 msgid ""
 "\n"
-"            We are calling your phone right now, please enter the digits you hear.\n"
+"            We are calling your phone right now, please enter the digits you "
+"hear.\n"
 "          "
 msgstr ""
 
@@ -1048,9 +1377,12 @@ msgstr ""
 #: templates/two_factor/core/setup.html:63
 msgid ""
 "\n"
-"          We've encountered an issue with the selected authentication method. Please\n"
-"          go back and verify that you entered your information correctly, try\n"
-"          again, or use a different authentication method instead. If the issue\n"
+"          We've encountered an issue with the selected authentication "
+"method. Please\n"
+"          go back and verify that you entered your information correctly, "
+"try\n"
+"          again, or use a different authentication method instead. If the "
+"issue\n"
 "          persists, contact the site administrator.\n"
 "        "
 msgstr ""
@@ -1072,7 +1404,8 @@ msgstr ""
 #: templates/two_factor/core/setup_complete.html:14
 msgid ""
 "\n"
-"        Congratulations, you've successfully enabled two-factor authentication.\n"
+"        Congratulations, you've successfully enabled two-factor "
+"authentication.\n"
 "      "
 msgstr ""
 
@@ -1090,7 +1423,8 @@ msgstr ""
 msgid ""
 "\n"
 "          However, it might happen that you don't have access to\n"
-"          your primary token device. To enable account recovery, generate backup codes\n"
+"          your primary token device. To enable account recovery, generate "
+"backup codes\n"
 "          or add a phone number.\n"
 "        "
 msgstr ""
@@ -1108,7 +1442,9 @@ msgid "Disable Two-Factor Authentication"
 msgstr ""
 
 #: templates/two_factor/profile/disable.html:12
-msgid "You are about to disable two-factor authentication. This weakens your account security, are you sure?"
+msgid ""
+"You are about to disable two-factor authentication. This weakens your "
+"account security, are you sure?"
 msgstr ""
 
 #: templates/two_factor/profile/disable.html:26
@@ -1187,38 +1523,34 @@ msgid ""
 "      "
 msgstr ""
 
-#: util/notifications.py:66
+#: util/notifications.py:65
 msgid "E-Mail"
 msgstr ""
 
-#: util/notifications.py:67
+#: util/notifications.py:66
 msgid "SMS"
 msgstr ""
 
-#: views.py:172
-msgid "The person has been saved."
+#: views.py:212
+msgid "The child groups were successfully saved."
 msgstr ""
 
-#: views.py:195
-msgid "The group has been saved."
-msgstr ""
-
-#: views.py:236
-msgid "The school has been saved."
-msgstr ""
-
-#: views.py:255
-msgid "The term has been saved."
+#: views.py:240
+msgid "The person has been saved."
 msgstr ""
 
-#: views.py:272
-msgid "You are not allowed to mark notifications from other users as read!"
+#: views.py:276
+msgid "The group has been saved."
 msgstr ""
 
-#: views.py:307
+#: views.py:348
 msgid "The announcement has been saved."
 msgstr ""
 
-#: views.py:320
+#: views.py:364
 msgid "The announcement has been deleted."
 msgstr ""
+
+#: views.py:435
+msgid "The preferences have been saved successfully."
+msgstr ""
diff --git a/aleksis/core/locale/fr/LC_MESSAGES/djangojs.po b/aleksis/core/locale/fr/LC_MESSAGES/djangojs.po
index bebbb1f20d222d57029259420b0d5092f1969c14..417efd1fc19835a2763ca93f09e98863c9d77c61 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: 2020-03-30 09:18+0000\n"
+"POT-Creation-Date: 2020-05-03 10:36+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,14 +18,14 @@ msgstr ""
 "Content-Transfer-Encoding: 8bit\n"
 "Plural-Forms: nplurals=2; plural=(n > 1);\n"
 
-#: static/js/main.js:21
+#: aleksis/core/static/js/main.js:21
 msgid "Today"
 msgstr ""
 
-#: static/js/main.js:22
+#: aleksis/core/static/js/main.js:22
 msgid "Cancel"
 msgstr ""
 
-#: static/js/main.js:23
+#: aleksis/core/static/js/main.js:23
 msgid "OK"
 msgstr ""
diff --git a/aleksis/core/locale/la/LC_MESSAGES/django.po b/aleksis/core/locale/la/LC_MESSAGES/django.po
index 753ec75d839f32e2238bca0315c607df5559d70e..ea6778d9a100251312966073db19d4a1cf7002e3 100644
--- a/aleksis/core/locale/la/LC_MESSAGES/django.po
+++ b/aleksis/core/locale/la/LC_MESSAGES/django.po
@@ -7,9 +7,9 @@ msgid ""
 msgstr ""
 "Project-Id-Version: PACKAGE VERSION\n"
 "Report-Msgid-Bugs-To: \n"
-"POT-Creation-Date: 2020-03-30 09:18+0000\n"
-"PO-Revision-Date: 2020-04-14 18:42+0000\n"
-"Last-Translator: Jonathan Weth <teckids@jonathanweth.de>\n"
+"POT-Creation-Date: 2020-05-04 15:39+0200\n"
+"PO-Revision-Date: 2020-04-27 13:03+0000\n"
+"Last-Translator: Julian <leuckerj@gmail.com>\n"
 "Language-Team: Latin <https://translate.edugit.org/projects/aleksis/aleksis/"
 "la/>\n"
 "Language: la\n"
@@ -17,72 +17,98 @@ msgstr ""
 "Content-Type: text/plain; charset=UTF-8\n"
 "Content-Transfer-Encoding: 8bit\n"
 "Plural-Forms: nplurals=2; plural=n != 1;\n"
-"X-Generator: Weblate 3.8\n"
+"X-Generator: Weblate 4.0.1\n"
 
-#: forms.py:38 forms.py:93
+#: forms.py:46
 msgid "You cannot set a new username when also selecting an existing user."
 msgstr ""
 
-#: forms.py:41 forms.py:96
+#: forms.py:50
 msgid "This username is already in use."
 msgstr ""
 
+#: forms.py:74
+msgid "Base data"
+msgstr ""
+
+#: forms.py:80
+#, fuzzy
+#| msgid "E-mail address"
+msgid "Address"
+msgstr "Inscriptio electronica"
+
+#: forms.py:81
+msgid "Contact data"
+msgstr ""
+
 #: forms.py:83
+msgid "Advanced personal data"
+msgstr ""
+
+#: forms.py:116
 msgid "New user"
 msgstr ""
 
-#: forms.py:83
+#: forms.py:116
+#, fuzzy
+#| msgid "Persons and accounts"
 msgid "Create a new account"
-msgstr ""
+msgstr "Personae et computi"
 
-#: forms.py:149 forms.py:152
+#: forms.py:128
+#, fuzzy
+#| msgid "Data management"
+msgid "Common data"
+msgstr "Adminstratio datarum"
+
+#: forms.py:129 forms.py:169 menus.py:141 models.py:54
+#: templates/core/persons.html:8 templates/core/persons.html:9
+msgid "Persons"
+msgstr "personae"
+
+#: forms.py:162 forms.py:165 models.py:31
 msgid "Date"
 msgstr "dies"
 
-#: forms.py:150 forms.py:153
+#: forms.py:163 forms.py:166 models.py:39
 msgid "Time"
 msgstr "tempus"
 
-#: forms.py:155 menus.py:127 models.py:95 templates/core/persons.html:8
-#: templates/core/persons.html:9
-msgid "Persons"
-msgstr "personae"
-
-#: forms.py:156 menus.py:133 models.py:232 templates/core/groups.html:8
-#: templates/core/groups.html:9 templates/core/person_full.html:79
+#: forms.py:171 menus.py:149 models.py:253 templates/core/groups.html:8
+#: templates/core/groups.html:9 templates/core/person_full.html:106
 msgid "Groups"
-msgstr ""
+msgstr "Greges"
 
-#: forms.py:160
+#: forms.py:175
 msgid "From when until when should the announcement be displayed?"
 msgstr ""
 
-#: forms.py:163
+#: forms.py:178
 msgid "Who should see the announcement?"
-msgstr ""
+msgstr "Quis nuntium videatne?"
 
-#: forms.py:164
+#: forms.py:179
 msgid "Write your announcement:"
 msgstr "Scribe nuntium:"
 
-#: forms.py:203
-msgid "You are not allowed to create announcements which are only valid in the past."
+#: forms.py:216
+msgid ""
+"You are not allowed to create announcements which are only valid in the past."
 msgstr ""
 
-#: forms.py:207
+#: forms.py:220
 msgid "The from date and time must be earlier then the until date and time."
 msgstr ""
 
-#: forms.py:215
+#: forms.py:229
 msgid "You need at least one recipient."
 msgstr ""
 
-#: menus.py:7 templates/registration/login.html:21
-#: templates/two_factor/core/login.html:6
+#: menus.py:7 templates/two_factor/core/login.html:6
 #: templates/two_factor/core/login.html:10
-#: templates/two_factor/core/login.html:64
+#: templates/two_factor/core/login.html:73
 msgid "Login"
-msgstr ""
+msgstr "nomen profiteri"
 
 #: menus.py:13
 msgid "Dashboard"
@@ -94,441 +120,724 @@ msgstr ""
 
 #: menus.py:26
 msgid "Stop impersonation"
-msgstr ""
+msgstr "Simulandum aliquem finire"
 
 #: menus.py:35 templates/core/base.html:56
 msgid "Logout"
-msgstr ""
+msgstr "nomen retractare"
 
 #: menus.py:41
-msgid "Two factor auth"
+msgid "2FA"
 msgstr ""
 
-#: menus.py:52
+#: menus.py:47
+msgid "Me"
+msgstr ""
+
+#: menus.py:56 templates/dynamic_preferences/form.html:5
+msgid "Preferences"
+msgstr ""
+
+#: menus.py:67
 msgid "Admin"
 msgstr "Administratio"
 
-#: menus.py:61 models.py:395 templates/core/announcement/list.html:7
+#: menus.py:75 models.py:487 templates/core/announcement/list.html:7
 #: templates/core/announcement/list.html:8
 msgid "Announcements"
 msgstr "Nuntii"
 
-#: menus.py:70 templates/core/data_management.html:6
+#: menus.py:86 templates/core/data_management.html:6
 #: templates/core/data_management.html:7
 msgid "Data management"
 msgstr "Adminstratio datarum"
 
-#: menus.py:79 templates/core/system_status.html:5
+#: menus.py:94 templates/core/system_status.html:5
 #: templates/core/system_status.html:7
 msgid "System status"
 msgstr "Status systemae"
 
-#: menus.py:88
+#: menus.py:105
 msgid "Impersonation"
-msgstr ""
+msgstr "Simulare aliquem"
 
-#: menus.py:97
-msgid "Manage school"
-msgstr "Administra scholam"
+#: menus.py:113
+#, fuzzy
+#| msgid "Notification"
+msgid "Configuration"
+msgstr "Nuntius"
 
-#: menus.py:106
+#: menus.py:124
 msgid "Backend Admin"
 msgstr ""
 
-#: menus.py:117
+#: menus.py:132
 msgid "People"
 msgstr "Personae"
 
-#: menus.py:139
+#: menus.py:157
 msgid "Persons and accounts"
 msgstr "Personae et computi"
 
-#: menus.py:152
-msgid "Edit school information"
-msgstr "Muta informationes scolae"
+#: menus.py:168
+msgid "Groups and child groups"
+msgstr ""
 
-#: menus.py:153 templates/core/edit_schoolterm.html:8
-#: templates/core/edit_schoolterm.html:9
-msgid "Edit school term"
-msgstr "Muta anum scolae"
+#: menus.py:183 templates/core/groups_child_groups.html:7
+#: templates/core/groups_child_groups.html:9
+msgid "Assign child groups to groups"
+msgstr ""
 
-#: models.py:31 models.py:517
-msgid "Name"
-msgstr "Nomen"
+#: models.py:29
+msgid "Boolean (Yes/No)"
+msgstr ""
 
-#: models.py:33
-msgid "Official name"
-msgstr "Officialis nomen"
+#: models.py:30
+msgid "Text (one line)"
+msgstr ""
 
-#: models.py:35
-msgid "Official name of the school, e.g. as given by supervisory authority"
-msgstr "Officialis nomen scolae, e. g."
+#: models.py:32
+msgid "Date and time"
+msgstr ""
 
-#: models.py:38
-msgid "School logo"
-msgstr "Imago scolae"
+#: models.py:33
+msgid "Decimal number"
+msgstr ""
 
-#: models.py:51
-msgid "School"
-msgstr "Scola"
+#: models.py:34 models.py:95
+msgid "E-mail address"
+msgstr "Inscriptio electronica"
 
-#: models.py:52
-msgid "Schools"
+#: models.py:35
+msgid "Integer"
 msgstr ""
 
-#: models.py:60
-msgid "Visible caption of the term"
-msgstr ""
+#: models.py:36
+#, fuzzy
+#| msgid "E-mail address"
+msgid "IP address"
+msgstr "Inscriptio electronica"
 
-#: models.py:62
-msgid "Effective start date of term"
+#: models.py:37
+msgid "Boolean or empty (Yes/No/Neither)"
 msgstr ""
 
-#: models.py:63
-msgid "Effective end date of term"
+#: models.py:38
+msgid "Text (multi-line)"
 msgstr ""
 
-#: models.py:83
-msgid "School term"
-msgstr "Anus scolae"
-
-#: models.py:84
-msgid "School terms"
-msgstr "ani scolae"
+#: models.py:40
+msgid "URL / Link"
+msgstr ""
 
-#: models.py:94 templates/core/persons_accounts.html:36
+#: models.py:53 templates/core/persons_accounts.html:36
 msgid "Person"
 msgstr "Persona"
 
-#: models.py:97
+#: models.py:56
+#, fuzzy
+#| msgid "E-mail address"
+msgid "Can view address"
+msgstr "Inscriptio electronica"
+
+#: models.py:57
+#, fuzzy
+#| msgid "E-mail address"
+msgid "Can view contact details"
+msgstr "Inscriptio electronica"
+
+#: models.py:58
+#, fuzzy
+#| msgid "E-mail address"
+msgid "Can view photo"
+msgstr "Inscriptio electronica"
+
+#: models.py:59
+#, fuzzy
+#| msgid "Persons and accounts"
+msgid "Can view persons groups"
+msgstr "Personae et computi"
+
+#: models.py:60
+#, fuzzy
+#| msgid "Stop impersonation"
+msgid "Can view personal details"
+msgstr "Simulandum aliquem finire"
+
+#: models.py:65
 msgid "female"
 msgstr "femininum"
 
-#: models.py:97
+#: models.py:65
 msgid "male"
 msgstr "maskulinum"
 
-#: models.py:102
-msgid "Is person active?"
+#: models.py:73
+msgid "Linked user"
 msgstr ""
 
-#: models.py:104
+#: models.py:75
+#, fuzzy
+#| msgid "Impersonation"
+msgid "Is person active?"
+msgstr "Simulare aliquem"
+
+#: models.py:77
 msgid "First name"
 msgstr "Primus nomen"
 
-#: models.py:105
+#: models.py:78
 msgid "Last name"
 msgstr "Secondus nomen"
 
-#: models.py:107
+#: models.py:80
 msgid "Additional name(s)"
 msgstr "addita nomines"
 
-#: models.py:111
+#: models.py:84 models.py:260
 msgid "Short name"
 msgstr "Breve nomen"
 
-#: models.py:114
+#: models.py:87
 msgid "Street"
 msgstr "Via"
 
-#: models.py:115
+#: models.py:88
 msgid "Street number"
 msgstr "Numerus domini"
 
-#: models.py:116
+#: models.py:89
 msgid "Postal code"
 msgstr "Numerus directorius"
 
-#: models.py:117
+#: models.py:90
 msgid "Place"
 msgstr "Urbs"
 
-#: models.py:119
+#: models.py:92
 msgid "Home phone"
 msgstr "Numerus telephoni domi"
 
-#: models.py:120
+#: models.py:93
 msgid "Mobile phone"
 msgstr "Numerus telephoni mobilis"
 
-#: models.py:122
-msgid "E-mail address"
-msgstr "Inscriptio electronica"
-
-#: models.py:124
+#: models.py:97
 msgid "Date of birth"
-msgstr ""
+msgstr "Dies natalis"
 
-#: models.py:125
+#: models.py:98
 msgid "Sex"
-msgstr ""
+msgstr "Genus"
 
-#: models.py:127
+#: models.py:100
 msgid "Photo"
+msgstr "Photographia"
+
+#: models.py:105
+msgid "Guardians / Parents"
+msgstr "Parentes"
+
+#: models.py:112
+msgid "Primary group"
 msgstr ""
 
-#: models.py:131 models.py:249
-msgid "Reference ID of import source"
+#: models.py:115 models.py:346 models.py:370 models.py:455 models.py:643
+msgid "Description"
+msgstr "Descriptio"
+
+#: models.py:233
+msgid "Title of field"
 msgstr ""
 
-#: models.py:140
-msgid "Guardians / Parents"
+#: models.py:235
+msgid "Type of field"
 msgstr ""
 
-#: models.py:231
+#: models.py:239
+#, fuzzy
+#| msgid "Additional name(s)"
+msgid "Addtitional field for groups"
+msgstr "addita nomines"
+
+#: models.py:240
+#, fuzzy
+#| msgid "Additional name(s)"
+msgid "Addtitional fields for groups"
+msgstr "addita nomines"
+
+#: models.py:252
 msgid "Group"
+msgstr "Grex"
+
+#: models.py:254
+msgid "Can assign child groups to groups"
 msgstr ""
 
-#: models.py:234
-msgid "Long name of group"
+#: models.py:258
+#, fuzzy
+#| msgid "Last name"
+msgid "Long name"
+msgstr "Secondus nomen"
+
+#: models.py:268 templates/core/group_full.html:37
+msgid "Members"
 msgstr ""
 
-#: models.py:235
-msgid "Short name of group"
+#: models.py:271 templates/core/group_full.html:34
+msgid "Owners"
 msgstr ""
 
-#: models.py:244
+#: models.py:278
 msgid "Parent groups"
 msgstr ""
 
-#: models.py:268 models.py:285 models.py:363
-#: templates/core/announcement/list.html:18
-msgid "Title"
+#: models.py:286
+msgid "Type of group"
 msgstr ""
 
-#: models.py:269 models.py:286 models.py:364
-msgid "Description"
+#: models.py:290
+#, fuzzy
+#| msgid "Additional name(s)"
+msgid "Additional fields"
+msgstr "addita nomines"
+
+#: models.py:342
+msgid "User"
 msgstr ""
 
-#: models.py:271
+#: models.py:345 models.py:369 models.py:454
+#: templates/core/announcement/list.html:18
+msgid "Title"
+msgstr "Titulus"
+
+#: models.py:348
 msgid "Application"
 msgstr ""
 
-#: models.py:277
+#: models.py:354
 msgid "Activity"
 msgstr ""
 
-#: models.py:278
+#: models.py:355
 msgid "Activities"
 msgstr ""
 
-#: models.py:282
+#: models.py:361
 msgid "Sender"
+msgstr "Mittens"
+
+#: models.py:366
+msgid "Recipient"
 msgstr ""
 
-#: models.py:287 models.py:365 models.py:518
+#: models.py:371 models.py:624
 msgid "Link"
 msgstr ""
 
-#: models.py:289
+#: models.py:373
 msgid "Read"
 msgstr ""
 
-#: models.py:290
+#: models.py:374
 msgid "Sent"
 msgstr ""
 
-#: models.py:301
+#: models.py:387
+#, fuzzy
+#| msgid "Notifications"
 msgid "Notification"
-msgstr ""
+msgstr "Nuntii"
 
-#: models.py:302
+#: models.py:388
 msgid "Notifications"
+msgstr "Nuntii"
+
+#: models.py:456
+msgid "Link to detailed view"
 msgstr ""
 
-#: models.py:368
+#: models.py:459
 msgid "Date and time from when to show"
 msgstr ""
 
-#: models.py:371
+#: models.py:462
 msgid "Date and time until when to show"
 msgstr ""
 
-#: models.py:394
+#: models.py:486
+#, fuzzy
+#| msgid "Announcements"
 msgid "Announcement"
-msgstr ""
+msgstr "Nuntii"
 
-#: models.py:422
+#: models.py:524
+#, fuzzy
+#| msgid "Announcements"
 msgid "Announcement recipient"
-msgstr ""
+msgstr "Nuntii"
 
-#: models.py:423
+#: models.py:525
+#, fuzzy
+#| msgid "Announcements"
 msgid "Announcement recipients"
-msgstr ""
+msgstr "Nuntii"
 
-#: models.py:473
+#: models.py:575
+#, fuzzy
+#| msgid "Site title"
 msgid "Widget Title"
-msgstr ""
+msgstr "Titulus paginae"
 
-#: models.py:474
+#: models.py:576
 msgid "Activate Widget"
 msgstr ""
 
-#: models.py:486
+#: models.py:594
+#, fuzzy
+#| msgid "Dashboard"
 msgid "Dashboard Widget"
-msgstr ""
+msgstr "Forum"
 
-#: models.py:487
+#: models.py:595
+#, fuzzy
+#| msgid "Dashboard"
 msgid "Dashboard Widgets"
-msgstr ""
+msgstr "Forum"
 
-#: models.py:491
+#: models.py:601
 msgid "Menu ID"
 msgstr ""
 
-#: models.py:492
-msgid "Menu name"
-msgstr ""
-
-#: models.py:509
+#: models.py:613
 msgid "Custom menu"
 msgstr ""
 
-#: models.py:510
+#: models.py:614
 msgid "Custom menus"
 msgstr ""
 
-#: models.py:515
+#: models.py:621
 msgid "Menu"
 msgstr ""
 
-#: models.py:520
+#: models.py:623
+msgid "Name"
+msgstr "Nomen"
+
+#: models.py:625
 msgid "Icon"
-msgstr ""
+msgstr "Nota"
 
-#: models.py:527
+#: models.py:631
 msgid "Custom menu item"
 msgstr ""
 
-#: models.py:528
+#: models.py:632
 msgid "Custom menu items"
 msgstr ""
 
-#: settings.py:254
-msgid "German"
+#: models.py:642
+msgid "Title of type"
 msgstr ""
 
-#: settings.py:255
-msgid "English"
+#: models.py:646
+#, fuzzy
+#| msgid "Group"
+msgid "Group type"
+msgstr "Grex"
+
+#: models.py:647
+#, fuzzy
+#| msgid "Groups"
+msgid "Group types"
+msgstr "Greges"
+
+#: models.py:656
+#, fuzzy
+#| msgid "System status"
+msgid "Can view system status"
+msgstr "Status systemae"
+
+#: models.py:657
+#, fuzzy
+#| msgid "Persons and accounts"
+msgid "Can link persons to accounts"
+msgstr "Personae et computi"
+
+#: models.py:658
+#, fuzzy
+#| msgid "Data management"
+msgid "Can manage data"
+msgstr "Adminstratio datarum"
+
+#: models.py:659
+#, fuzzy
+#| msgid "Stop impersonation"
+msgid "Can impersonate"
+msgstr "Simulandum aliquem finire"
+
+#: models.py:660
+msgid "Can use search"
 msgstr ""
 
-#: settings.py:373
-msgid "Site title"
+#: models.py:661
+msgid "Can change site preferences"
 msgstr ""
 
-#: settings.py:374
-msgid "Site description"
+#: models.py:662
+msgid "Can change person preferences"
 msgstr ""
 
-#: settings.py:375
+#: models.py:663
+msgid "Can change group preferences"
+msgstr ""
+
+#: preferences.py:27
+msgid "Site title"
+msgstr "Titulus paginae"
+
+#: preferences.py:36
+msgid "Site description"
+msgstr "Descriptio paginae"
+
+#: preferences.py:45
 msgid "Primary colour"
 msgstr ""
 
-#: settings.py:376
+#: preferences.py:54
 msgid "Secondary colour"
 msgstr ""
 
-#: settings.py:377
-msgid "Mail out name"
+#: preferences.py:62
+#, fuzzy
+#| msgid "Logout"
+msgid "Logo"
+msgstr "nomen retractare"
+
+#: preferences.py:70
+msgid "Favicon"
 msgstr ""
 
-#: settings.py:378
+#: preferences.py:78
+#, fuzzy
+#| msgid "Icon"
+msgid "PWA-Icon"
+msgstr "Nota"
+
+#: preferences.py:87
+#, fuzzy
+#| msgid "Last name"
+msgid "Mail out name"
+msgstr "Secondus nomen"
+
+#: preferences.py:96
+#, fuzzy
+#| msgid "E-mail address"
 msgid "Mail out address"
-msgstr ""
+msgstr "Inscriptio electronica"
 
-#: settings.py:379
+#: preferences.py:106
 msgid "Link to privacy policy"
 msgstr ""
 
-#: settings.py:380
+#: preferences.py:116
 msgid "Link to imprint"
 msgstr ""
 
-#: settings.py:381
-msgid "Name format of adresses"
+#: preferences.py:126
+msgid "Name format for addressing"
 msgstr ""
 
-#: settings.py:382
-msgid "Channels to allow for notifications"
+#: preferences.py:140
+msgid "Channels to use for notifications"
 msgstr ""
 
-#: settings.py:383
+#: preferences.py:150
 msgid "Regular expression to match primary group, e.g. '^Class .*'"
 msgstr ""
 
+#: preferences.py:159
+msgid "Field on person to match primary group against"
+msgstr ""
+
+#: preferences.py:171
+msgid "Display name of the school"
+msgstr ""
+
+#: preferences.py:180
+msgid "Official name of the school, e.g. as given by supervisory authority"
+msgstr "Officialis nomen scolae, e. g."
+
+#: settings.py:276
+msgid "English"
+msgstr "Britannicus"
+
+#: settings.py:277
+msgid "German"
+msgstr "Germanus"
+
+#: settings.py:278
+msgid "French"
+msgstr ""
+
+#: templates/403.html:10 templates/404.html:10 templates/500.html:10
+msgid "Error"
+msgstr ""
+
 #: templates/403.html:10
-msgid "Error (403): You are not allowed to access the requested page or object."
+msgid ""
+"You are not allowed to access the requested page or\n"
+"          object."
 msgstr ""
 
-#: templates/403.html:12
+#: templates/403.html:13 templates/404.html:17
 msgid ""
 "\n"
-"      If you think this is an error in AlekSIS, please contact your site\n"
-"     administrators:\n"
-"     "
+"            If you think this is an error in AlekSIS, please contact your "
+"site\n"
+"            administrators:\n"
+"          "
 msgstr ""
 
 #: templates/404.html:10
-msgid "Error (404): The requested page or object was not found."
+msgid ""
+"The requested page or object was not\n"
+"          found."
+msgstr ""
+
+#: templates/404.html:13
+msgid ""
+"\n"
+"            If you were redirected by a link on an external page,\n"
+"            it is possible that that link was outdated.\n"
+"          "
 msgstr ""
 
-#: templates/404.html:12
+#: templates/500.html:10
+msgid ""
+"An unexpected error has\n"
+"          occured."
+msgstr ""
+
+#: templates/500.html:13
 msgid ""
 "\n"
-"      If you were redirected by a link on an external page,\n"
-"      it is possible that that link was outdated.\n"
-"     "
+"            Your site administrators will automatically be notified about "
+"this\n"
+"            error. You can also contact them directly:\n"
+"          "
+msgstr ""
+
+#: templates/503.html:10
+msgid ""
+"The maintenance mode is currently enabled. Please try again\n"
+"          later."
 msgstr ""
 
-#: templates/404.html:16
+#: templates/503.html:13
 msgid ""
 "\n"
-"      If you think this is an error in AlekSIS, please contact your site\n"
-"      administrators:\n"
-"     "
+"            This page is currently unavailable. If this error persists, "
+"contact your site administrators:\n"
+"          "
 msgstr ""
 
-#: templates/500.html:10
-msgid "Error (500): An unexpected error has occured.."
+#: templates/core/about.html:6 templates/core/about.html:15
+msgid "About AlekSIS"
 msgstr ""
 
-#: templates/500.html:12
+#: templates/core/about.html:7
+msgid "AlekSIS – The Free School Information System"
+msgstr ""
+
+#: templates/core/about.html:17
 msgid ""
 "\n"
-"      Your site administrators will automatically be notified about this\n"
-"     error.\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 ""
 
-#: templates/503.html:10
-msgid "The maintenance mode is currently enabled. Please try again later."
+#: templates/core/about.html:25
+msgid "Website of AlekSIS"
+msgstr ""
+
+#: templates/core/about.html:26
+msgid "Source code"
 msgstr ""
 
-#: templates/503.html:12
+#: templates/core/about.html:35
+#, fuzzy
+#| msgid "Edit school information"
+msgid "Licence information"
+msgstr "Muta informationes scolae"
+
+#: templates/core/about.html:37
+msgid ""
+"\n"
+"              The core and the official apps of AlekSIS are licenced under "
+"the EUPL, version 1.2 or later. For licence\n"
+"              information from third-party apps, if installed, refer to the "
+"respective components below. The\n"
+"              licences are marked like this:\n"
+"            "
+msgstr ""
+
+#: templates/core/about.html:45
+msgid "Free/Open Source Licence"
+msgstr ""
+
+#: templates/core/about.html:46
+msgid "Other Licence"
+msgstr ""
+
+#: templates/core/about.html:50
+msgid "Full licence text"
+msgstr ""
+
+#: templates/core/about.html:51
+#, fuzzy
+#| msgid "Edit school information"
+msgid "More information about the EUPL"
+msgstr "Muta informationes scolae"
+
+#: templates/core/about.html:90
+#, python-format
 msgid ""
 "\n"
-"      This page is currently unavailable. If this error stays, contact your site administrators:\n"
-"     "
+"                    This app is licenced under %(licence)s.\n"
+"                  "
 msgstr ""
 
 #: templates/core/announcement/form.html:10
 #: templates/core/announcement/form.html:17
+#, fuzzy
+#| msgid "Announcements"
 msgid "Edit announcement"
-msgstr ""
+msgstr "Nuntii"
 
 #: templates/core/announcement/form.html:12
+#, fuzzy
+#| msgid "Announcements"
 msgid "Publish announcement"
-msgstr ""
+msgstr "Nuntii"
 
 #: templates/core/announcement/form.html:19
 #: templates/core/announcement/list.html:13
+#, fuzzy
+#| msgid "Who should see the announcement?"
 msgid "Publish new announcement"
-msgstr ""
+msgstr "Quis nuntium videatne?"
 
 #: templates/core/announcement/form.html:30
+#, fuzzy
+#| msgid "Who should see the announcement?"
 msgid "Save und publish announcement"
-msgstr ""
+msgstr "Quis nuntium videatne?"
 
 #: templates/core/announcement/list.html:19
 msgid "Valid from"
@@ -543,11 +852,13 @@ msgid "Recipients"
 msgstr ""
 
 #: templates/core/announcement/list.html:22
+#, fuzzy
+#| msgid "Notifications"
 msgid "Actions"
-msgstr ""
+msgstr "Nuntii"
 
-#: templates/core/announcement/list.html:36 templates/core/group_full.html:15
-#: templates/core/person_full.html:15
+#: templates/core/announcement/list.html:36 templates/core/group_full.html:22
+#: templates/core/person_full.html:21
 msgid "Edit"
 msgstr ""
 
@@ -556,8 +867,10 @@ msgid "Delete"
 msgstr ""
 
 #: templates/core/announcement/list.html:50
+#, fuzzy
+#| msgid "Write your announcement:"
 msgid "There are no announcements."
-msgstr ""
+msgstr "Scribe nuntium:"
 
 #: templates/core/announcements.html:9 templates/core/announcements.html:36
 #, python-format
@@ -587,15 +900,24 @@ msgstr ""
 msgid "Logged in as"
 msgstr ""
 
-#: templates/core/base.html:146
+#: templates/core/base.html:76 templates/search/search.html:7
+#: templates/search/search.html:22
+msgid "Search"
+msgstr ""
+
+#: templates/core/base.html:148
+msgid "About AlekSIS — The Free School Information System"
+msgstr ""
+
+#: templates/core/base.html:156
 msgid "Impress"
 msgstr ""
 
-#: templates/core/base.html:154
+#: templates/core/base.html:164
 msgid "Privacy Policy"
 msgstr ""
 
-#: templates/core/base_print.html:60
+#: templates/core/base_print.html:62
 msgid "Powered by AlekSIS"
 msgstr ""
 
@@ -607,84 +929,152 @@ msgstr ""
 msgid "Edit person"
 msgstr ""
 
-#: templates/core/edit_school.html:8 templates/core/edit_school.html:9
-msgid "Edit school"
+#: templates/core/group_full.html:28 templates/core/person_full.html:28
+msgid "Change preferences"
 msgstr ""
 
-#: templates/core/group_full.html:19
-msgid "Owners"
+#: templates/core/groups.html:14
+msgid "Create group"
 msgstr ""
 
-#: templates/core/group_full.html:22
-msgid "Members"
+#: templates/core/groups_child_groups.html:18
+msgid ""
+"\n"
+"          You can use this to assign child groups to groups. Please use the "
+"filters below to select groups you want to\n"
+"          change and click \"Next\".\n"
+"        "
 msgstr ""
 
-#: templates/core/groups.html:14
-msgid "Create group"
+#: templates/core/groups_child_groups.html:31
+msgid "Update selection"
 msgstr ""
 
-#: templates/core/index.html:4
-msgid "Home"
+#: templates/core/groups_child_groups.html:35
+msgid "Clear all filters"
+msgstr ""
+
+#: templates/core/groups_child_groups.html:39
+msgid "Currently selected groups"
+msgstr ""
+
+#: templates/core/groups_child_groups.html:52
+msgid "Start assigning child groups for this groups"
+msgstr ""
+
+#: templates/core/groups_child_groups.html:61
+msgid ""
+"\n"
+"            Please select some groups in order to go on with assigning.\n"
+"          "
+msgstr ""
+
+#: templates/core/groups_child_groups.html:72
+msgid "Current group:"
+msgstr ""
+
+#: templates/core/groups_child_groups.html:78
+msgid "Please be careful!"
+msgstr ""
+
+#: templates/core/groups_child_groups.html:79
+msgid ""
+"\n"
+"            If you click \"Back\" or \"Next\" the current group assignments "
+"are not saved.\n"
+"            If you click \"Save\", you will overwrite all existing child "
+"group relations for this group with what you\n"
+"            selected on this page.\n"
+"          "
+msgstr ""
+
+#: templates/core/groups_child_groups.html:93
+#: templates/core/groups_child_groups.html:128
+#: templates/two_factor/_wizard_actions.html:15
+#: templates/two_factor/_wizard_actions.html:20
+msgid "Back"
+msgstr ""
+
+#: templates/core/groups_child_groups.html:99
+#: templates/core/groups_child_groups.html:134
+#: templates/two_factor/_wizard_actions.html:26
+msgid "Next"
+msgstr ""
+
+#: templates/core/groups_child_groups.html:106
+#: templates/core/groups_child_groups.html:141
+#: templates/core/save_button.html:3
+msgid "Save"
 msgstr ""
 
-#: templates/core/index.html:11
-msgid "AlekSIS (School Information System)"
+#: templates/core/groups_child_groups.html:112
+#: templates/core/groups_child_groups.html:147
+msgid "Save and next"
 msgstr ""
 
-#: templates/core/index.html:43
+#: templates/core/index.html:4
+msgid "Home"
+msgstr ""
+
+#: templates/core/index.html:42
 msgid "Last activities"
 msgstr ""
 
-#: templates/core/index.html:61
+#: templates/core/index.html:60
 msgid "No activities available yet."
 msgstr ""
 
-#: templates/core/index.html:66
+#: templates/core/index.html:65
+#, fuzzy
+#| msgid "Notifications"
 msgid "Recent notifications"
-msgstr ""
+msgstr "Nuntii"
 
-#: templates/core/index.html:82
+#: templates/core/index.html:81
+#, fuzzy
+#| msgid "Edit school information"
 msgid "More information →"
-msgstr ""
+msgstr "Muta informationes scolae"
 
-#: templates/core/index.html:89
+#: templates/core/index.html:88
 msgid "No notifications available yet."
 msgstr ""
 
-#: templates/core/no_person.html:11
+#: templates/core/no_person.html:12
 msgid ""
 "\n"
-"          Your user account is not linked to a person. This means you\n"
-"          cannot access any school-related information. Please contact\n"
-"          the managers of AlekSIS at your school.\n"
-"        "
-msgstr ""
-
-#: templates/core/offline.html:6
-msgid "No internet connection."
+"            Your administrator account is not linked to any person. "
+"Therefore,\n"
+"            a dummy person has been linked to your account.\n"
+"          "
 msgstr ""
 
-#: templates/core/offline.html:9
+#: templates/core/no_person.html:19
 msgid ""
 "\n"
-"        There was an error accessing this page. You probably don't have an internet connection. Check to see if your WiFi or mobile data is turned on and try again. If you think you are connected, please contact the system administrators:\n"
-"      "
+"            Your user account is not linked to a person. This means you\n"
+"            cannot access any school-related information. Please contact\n"
+"            the managers of AlekSIS at your school.\n"
+"          "
 msgstr ""
 
-#: templates/core/person_full.html:19
+#: templates/core/person_full.html:34
 msgid "Contact details"
 msgstr ""
 
 #: templates/core/persons_accounts.html:7
 #: templates/core/persons_accounts.html:9
+#, fuzzy
+#| msgid "Persons and accounts"
 msgid "Link persons to accounts"
-msgstr ""
+msgstr "Personae et computi"
 
 #: templates/core/persons_accounts.html:16
 msgid ""
 "\n"
 "        You can use this form to assign user accounts to persons. Use the\n"
-"        dropdowns to select existing accounts; use the text fields to create new\n"
+"        dropdowns to select existing accounts; use the text fields to create "
+"new\n"
 "        accounts on-the-fly. The latter will create a new account with the\n"
 "        entered username and copy all other details from the person.\n"
 "      "
@@ -703,18 +1093,11 @@ msgstr ""
 msgid "New account"
 msgstr ""
 
-#: templates/core/save_button.html:3
-msgid "Save"
-msgstr ""
-
-#: templates/core/school_management.html:6
-#: templates/core/school_management.html:7
-msgid "School management"
-msgstr ""
-
 #: templates/core/system_status.html:12
+#, fuzzy
+#| msgid "System status"
 msgid "System checks"
-msgstr ""
+msgstr "Status systemae"
 
 #: templates/core/system_status.html:21
 msgid "Maintenance mode enabled"
@@ -723,7 +1106,8 @@ msgstr ""
 #: templates/core/system_status.html:23
 msgid ""
 "\n"
-"                Only admin and visitors from internal IPs can access thesite.\n"
+"                Only admin and visitors from internal IPs can access "
+"thesite.\n"
 "              "
 msgstr ""
 
@@ -742,7 +1126,8 @@ msgstr ""
 #: templates/core/system_status.html:47
 msgid ""
 "\n"
-"                The web server throws back debug information on errors. Do not use in production!\n"
+"                The web server throws back debug information on errors. Do "
+"not use in production!\n"
 "              "
 msgstr ""
 
@@ -753,105 +1138,103 @@ msgstr ""
 #: templates/core/system_status.html:56
 msgid ""
 "\n"
-"                Debug mode is disabled. Default error pages are displayed on errors.\n"
+"                Debug mode is disabled. Default error pages are displayed on "
+"errors.\n"
 "              "
 msgstr ""
 
-#: templates/impersonate/list_users.html:8
-msgid "Impersonate user"
-msgstr ""
-
-#: templates/martor/editor.html:27
-msgid "Uploading... please wait..."
-msgstr ""
-
-#: templates/martor/editor.html:36
-msgid "Nothing to preview"
+#: templates/dynamic_preferences/form.html:9
+msgid "Site preferences"
 msgstr ""
 
-#: templates/martor/emoji.html:4
-msgid "Select Emoji to Insert"
+#: templates/dynamic_preferences/form.html:11
+msgid "My preferences"
 msgstr ""
 
-#: templates/martor/emoji.html:8
-msgid "Preparing emojis..."
-msgstr ""
-
-#: templates/martor/guide.html:8
-msgid "Markdown Guide"
-msgstr ""
-
-#: templates/martor/guide.html:9
+#: templates/dynamic_preferences/form.html:13
 #, python-format
-msgid ""
-"This site is powered by Markdown. For full\n"
-"            documentation,\n"
-"            <a href=\"%(doc_url)s\" target=\"_blank\">click here</a>"
+msgid "Preferences for %(instance)s"
 msgstr ""
 
-#: templates/martor/guide.html:15 templates/martor/toolbar.html:42
-msgid "Code"
+#: templates/dynamic_preferences/form.html:25
+msgid "Save preferences"
 msgstr ""
 
-#: templates/martor/guide.html:16
-msgid "Or"
+#: templates/dynamic_preferences/sections.html:7
+msgid "All"
 msgstr ""
 
-#: templates/martor/guide.html:19
-msgid "... to Get"
-msgstr ""
-
-#: templates/martor/toolbar.html:3
-msgid "Bold"
-msgstr ""
+#: templates/impersonate/list_users.html:8
+#, fuzzy
+#| msgid "Impersonation"
+msgid "Impersonate user"
+msgstr "Simulare aliquem"
 
-#: templates/martor/toolbar.html:6
-msgid "Italic"
+#: templates/offline.html:6
+msgid ""
+"No internet\n"
+"    connection."
 msgstr ""
 
-#: templates/martor/toolbar.html:10
-msgid "Horizontal Line"
+#: templates/offline.html:10
+msgid ""
+"\n"
+"      There was an error accessing this page. You probably don't have an "
+"internet connection. Check to see if your WiFi\n"
+"      or mobile data is turned on and try again. If you think you are "
+"connected, please contact the system\n"
+"      administrators:\n"
+"    "
 msgstr ""
 
-#: templates/martor/toolbar.html:15
-msgid "Heading"
+#: templates/search/search.html:8
+msgid "Global Search"
 msgstr ""
 
-#: templates/martor/toolbar.html:20 templates/martor/toolbar.html:23
-#: templates/martor/toolbar.html:26
-msgid "H"
+#: templates/search/search.html:15
+msgid "Search Term"
 msgstr ""
 
-#: templates/martor/toolbar.html:31
-msgid "Pre or Code"
+#: templates/search/search.html:26
+msgid "Results"
 msgstr ""
 
-#: templates/martor/toolbar.html:38
-msgid "Pre"
+#: templates/search/search.html:38
+msgid "No search results could be found to your search."
 msgstr ""
 
-#: templates/martor/toolbar.html:48
-msgid "Quote"
+#: templates/search/search.html:87
+msgid "Please enter a search term above."
 msgstr ""
 
-#: templates/martor/toolbar.html:52
-msgid "Unordered List"
-msgstr ""
+#: templates/templated_email/notification.email:3
+#, fuzzy
+#| msgid "Notification"
+msgid "New notification for"
+msgstr "Nuntius"
 
-#: templates/martor/toolbar.html:56
-msgid "Ordered List"
+#: templates/templated_email/notification.email:7
+msgid "Dear"
 msgstr ""
 
-#: templates/martor/toolbar.html:60
-msgid "URL/Link"
+#: templates/templated_email/notification.email:8
+msgid "we got a new notification for you:"
 msgstr ""
 
-#: templates/martor/toolbar.html:82
-msgid "Full Screen"
-msgstr ""
+#: templates/templated_email/notification.email:12
+#, fuzzy
+#| msgid "Edit school information"
+msgid "More information"
+msgstr "Muta informationes scolae"
 
-#: templates/martor/toolbar.html:86
-msgid "Markdown Guide (Help)"
+#: templates/templated_email/notification.email:16
+#, python-format
+msgid ""
+"\n"
+"    <p>By %(trans_sender)s at %(trans_created_at)s</p>\n"
+"\n"
+"    <i>Your AlekSIS team</i>\n"
+"    "
 msgstr ""
 
 #: templates/two_factor/_base_focus.html:6
@@ -865,15 +1248,6 @@ msgstr ""
 msgid "Cancel"
 msgstr ""
 
-#: templates/two_factor/_wizard_actions.html:15
-#: templates/two_factor/_wizard_actions.html:20
-msgid "Back"
-msgstr ""
-
-#: templates/two_factor/_wizard_actions.html:26
-msgid "Next"
-msgstr ""
-
 #: templates/two_factor/core/backup_tokens.html:5
 #: templates/two_factor/core/backup_tokens.html:9
 #: templates/two_factor/profile/profile.html:46
@@ -885,8 +1259,10 @@ msgid ""
 "\n"
 "        Backup tokens can be used when your primary and backup\n"
 "        phone numbers aren't available. The backup tokens below can be used\n"
-"        for login verification. If you've used up all your backup tokens, you\n"
-"        can generate a new set of backup tokens. Only the backup tokens shown\n"
+"        for login verification. If you've used up all your backup tokens, "
+"you\n"
+"        can generate a new set of backup tokens. Only the backup tokens "
+"shown\n"
 "        below will be valid.\n"
 "      "
 msgstr ""
@@ -910,44 +1286,50 @@ msgstr ""
 msgid "Generate Tokens"
 msgstr ""
 
-#: templates/two_factor/core/login.html:17
-msgid "Enter your credentials."
+#: templates/two_factor/core/login.html:16
+msgid ""
+"You have no permission to view this page. Please login with an other account."
+msgstr ""
+
+#: templates/two_factor/core/login.html:25
+msgid "Please login to see this page."
 msgstr ""
 
-#: templates/two_factor/core/login.html:20
+#: templates/two_factor/core/login.html:28
 msgid ""
 "We are calling your phone right now, please enter the\n"
-"            digits you hear."
+"              digits you hear."
 msgstr ""
 
-#: templates/two_factor/core/login.html:23
+#: templates/two_factor/core/login.html:31
 msgid ""
 "We sent you a text message, please enter the tokens we\n"
-"            sent."
+"              sent."
 msgstr ""
 
-#: templates/two_factor/core/login.html:26
+#: templates/two_factor/core/login.html:34
 msgid ""
 "Please enter the tokens generated by your token\n"
-"            generator."
+"              generator."
 msgstr ""
 
-#: templates/two_factor/core/login.html:30
+#: templates/two_factor/core/login.html:38
 msgid ""
 "Use this form for entering backup tokens for logging in.\n"
-"          These tokens have been generated for you to print and keep safe. Please\n"
-"          enter one of these backup tokens to login to your account."
+"            These tokens have been generated for you to print and keep safe. "
+"Please\n"
+"            enter one of these backup tokens to login to your account."
 msgstr ""
 
-#: templates/two_factor/core/login.html:47
+#: templates/two_factor/core/login.html:56
 msgid "Or, alternatively, use one of your backup phones:"
 msgstr ""
 
-#: templates/two_factor/core/login.html:57
+#: templates/two_factor/core/login.html:66
 msgid "As a last resort, you can use a backup token:"
 msgstr ""
 
-#: templates/two_factor/core/login.html:60
+#: templates/two_factor/core/login.html:69
 msgid "Use Backup Token"
 msgstr ""
 
@@ -958,7 +1340,8 @@ msgstr ""
 #: templates/two_factor/core/otp_required.html:10
 msgid ""
 "The page you requested, enforces users to verify using\n"
-"          two-factor authentication for security reasons. You need to enable these\n"
+"          two-factor authentication for security reasons. You need to enable "
+"these\n"
 "          security features in order to access this page."
 msgstr ""
 
@@ -1035,7 +1418,8 @@ msgstr ""
 #: templates/two_factor/core/setup.html:50
 msgid ""
 "\n"
-"            We are calling your phone right now, please enter the digits you hear.\n"
+"            We are calling your phone right now, please enter the digits you "
+"hear.\n"
 "          "
 msgstr ""
 
@@ -1049,9 +1433,12 @@ msgstr ""
 #: templates/two_factor/core/setup.html:63
 msgid ""
 "\n"
-"          We've encountered an issue with the selected authentication method. Please\n"
-"          go back and verify that you entered your information correctly, try\n"
-"          again, or use a different authentication method instead. If the issue\n"
+"          We've encountered an issue with the selected authentication "
+"method. Please\n"
+"          go back and verify that you entered your information correctly, "
+"try\n"
+"          again, or use a different authentication method instead. If the "
+"issue\n"
 "          persists, contact the site administrator.\n"
 "        "
 msgstr ""
@@ -1073,7 +1460,8 @@ msgstr ""
 #: templates/two_factor/core/setup_complete.html:14
 msgid ""
 "\n"
-"        Congratulations, you've successfully enabled two-factor authentication.\n"
+"        Congratulations, you've successfully enabled two-factor "
+"authentication.\n"
 "      "
 msgstr ""
 
@@ -1091,7 +1479,8 @@ msgstr ""
 msgid ""
 "\n"
 "          However, it might happen that you don't have access to\n"
-"          your primary token device. To enable account recovery, generate backup codes\n"
+"          your primary token device. To enable account recovery, generate "
+"backup codes\n"
 "          or add a phone number.\n"
 "        "
 msgstr ""
@@ -1109,7 +1498,9 @@ msgid "Disable Two-Factor Authentication"
 msgstr ""
 
 #: templates/two_factor/profile/disable.html:12
-msgid "You are about to disable two-factor authentication. This weakens your account security, are you sure?"
+msgid ""
+"You are about to disable two-factor authentication. This weakens your "
+"account security, are you sure?"
 msgstr ""
 
 #: templates/two_factor/profile/disable.html:26
@@ -1188,38 +1579,66 @@ msgid ""
 "      "
 msgstr ""
 
-#: util/notifications.py:66
+#: util/notifications.py:65
 msgid "E-Mail"
 msgstr ""
 
-#: util/notifications.py:67
+#: util/notifications.py:66
 msgid "SMS"
 msgstr ""
 
-#: views.py:172
+#: views.py:212
+msgid "The child groups were successfully saved."
+msgstr ""
+
+#: views.py:240
 msgid "The person has been saved."
 msgstr ""
 
-#: views.py:195
+#: views.py:276
 msgid "The group has been saved."
 msgstr ""
 
-#: views.py:236
-msgid "The school has been saved."
+#: views.py:348
+msgid "The announcement has been saved."
 msgstr ""
 
-#: views.py:255
-msgid "The term has been saved."
+#: views.py:364
+msgid "The announcement has been deleted."
 msgstr ""
 
-#: views.py:272
-msgid "You are not allowed to mark notifications from other users as read!"
+#: views.py:435
+msgid "The preferences have been saved successfully."
 msgstr ""
 
-#: views.py:307
-msgid "The announcement has been saved."
-msgstr ""
+#, fuzzy
+#~| msgid "Short name"
+#~ msgid "School name"
+#~ msgstr "Breve nomen"
 
-#: views.py:320
-msgid "The announcement has been deleted."
-msgstr ""
+#~ msgid "School logo"
+#~ msgstr "Imago scolae"
+
+#~ msgid "Manage school"
+#~ msgstr "Administra scholam"
+
+#~ msgid "Edit school information"
+#~ msgstr "Muta informationes scolae"
+
+#~ msgid "Edit school term"
+#~ msgstr "Muta anum scolae"
+
+#~ msgid "Official name"
+#~ msgstr "Officialis nomen"
+
+#~ msgid "School"
+#~ msgstr "Scola"
+
+#~ msgid "Schools"
+#~ msgstr "Scholae"
+
+#~ msgid "School term"
+#~ msgstr "Anus scolae"
+
+#~ msgid "School terms"
+#~ msgstr "ani scolae"
diff --git a/aleksis/core/locale/la/LC_MESSAGES/djangojs.po b/aleksis/core/locale/la/LC_MESSAGES/djangojs.po
index d62e52377a4ce6af7272b5568c5da2a4698fb057..21309c0096e0f1ec402dfa7c4e2f8604b96e2466 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: 2020-03-30 09:18+0000\n"
+"POT-Creation-Date: 2020-04-28 13:31+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"
diff --git a/aleksis/core/locale/nb_NO/LC_MESSAGES/django.po b/aleksis/core/locale/nb_NO/LC_MESSAGES/django.po
index 495b614c5c6694495737ed677af23b37c0e15153..3e169fa0130f90c88bfaba22972f8bf0a7163a7f 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: 2020-03-30 09:18+0000\n"
+"POT-Creation-Date: 2020-05-04 15:39+0200\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,68 +17,88 @@ msgstr ""
 "Content-Type: text/plain; charset=UTF-8\n"
 "Content-Transfer-Encoding: 8bit\n"
 
-#: forms.py:38 forms.py:93
+#: forms.py:46
 msgid "You cannot set a new username when also selecting an existing user."
 msgstr ""
 
-#: forms.py:41 forms.py:96
+#: forms.py:50
 msgid "This username is already in use."
 msgstr ""
 
+#: forms.py:74
+msgid "Base data"
+msgstr ""
+
+#: forms.py:80
+msgid "Address"
+msgstr ""
+
+#: forms.py:81
+msgid "Contact data"
+msgstr ""
+
 #: forms.py:83
+msgid "Advanced personal data"
+msgstr ""
+
+#: forms.py:116
 msgid "New user"
 msgstr ""
 
-#: forms.py:83
+#: forms.py:116
 msgid "Create a new account"
 msgstr ""
 
-#: forms.py:149 forms.py:152
-msgid "Date"
+#: forms.py:128
+msgid "Common data"
 msgstr ""
 
-#: forms.py:150 forms.py:153
-msgid "Time"
+#: forms.py:129 forms.py:169 menus.py:141 models.py:54
+#: templates/core/persons.html:8 templates/core/persons.html:9
+msgid "Persons"
 msgstr ""
 
-#: forms.py:155 menus.py:127 models.py:95 templates/core/persons.html:8
-#: templates/core/persons.html:9
-msgid "Persons"
+#: forms.py:162 forms.py:165 models.py:31
+msgid "Date"
 msgstr ""
 
-#: forms.py:156 menus.py:133 models.py:232 templates/core/groups.html:8
-#: templates/core/groups.html:9 templates/core/person_full.html:79
+#: forms.py:163 forms.py:166 models.py:39
+msgid "Time"
+msgstr ""
+
+#: forms.py:171 menus.py:149 models.py:253 templates/core/groups.html:8
+#: templates/core/groups.html:9 templates/core/person_full.html:106
 msgid "Groups"
 msgstr ""
 
-#: forms.py:160
+#: forms.py:175
 msgid "From when until when should the announcement be displayed?"
 msgstr ""
 
-#: forms.py:163
+#: forms.py:178
 msgid "Who should see the announcement?"
 msgstr ""
 
-#: forms.py:164
+#: forms.py:179
 msgid "Write your announcement:"
 msgstr ""
 
-#: forms.py:203
-msgid "You are not allowed to create announcements which are only valid in the past."
+#: forms.py:216
+msgid ""
+"You are not allowed to create announcements which are only valid in the past."
 msgstr ""
 
-#: forms.py:207
+#: forms.py:220
 msgid "The from date and time must be earlier then the until date and time."
 msgstr ""
 
-#: forms.py:215
+#: forms.py:229
 msgid "You need at least one recipient."
 msgstr ""
 
-#: menus.py:7 templates/registration/login.html:21
-#: templates/two_factor/core/login.html:6
+#: menus.py:7 templates/two_factor/core/login.html:6
 #: templates/two_factor/core/login.html:10
-#: templates/two_factor/core/login.html:64
+#: templates/two_factor/core/login.html:73
 msgid "Login"
 msgstr ""
 
@@ -99,415 +119,628 @@ msgid "Logout"
 msgstr ""
 
 #: menus.py:41
-msgid "Two factor auth"
+msgid "2FA"
 msgstr ""
 
-#: menus.py:52
+#: menus.py:47
+msgid "Me"
+msgstr ""
+
+#: menus.py:56 templates/dynamic_preferences/form.html:5
+msgid "Preferences"
+msgstr ""
+
+#: menus.py:67
 msgid "Admin"
 msgstr ""
 
-#: menus.py:61 models.py:395 templates/core/announcement/list.html:7
+#: menus.py:75 models.py:487 templates/core/announcement/list.html:7
 #: templates/core/announcement/list.html:8
 msgid "Announcements"
 msgstr ""
 
-#: menus.py:70 templates/core/data_management.html:6
+#: menus.py:86 templates/core/data_management.html:6
 #: templates/core/data_management.html:7
 msgid "Data management"
 msgstr ""
 
-#: menus.py:79 templates/core/system_status.html:5
+#: menus.py:94 templates/core/system_status.html:5
 #: templates/core/system_status.html:7
 msgid "System status"
 msgstr ""
 
-#: menus.py:88
+#: menus.py:105
 msgid "Impersonation"
 msgstr ""
 
-#: menus.py:97
-msgid "Manage school"
+#: menus.py:113
+msgid "Configuration"
 msgstr ""
 
-#: menus.py:106
+#: menus.py:124
 msgid "Backend Admin"
 msgstr ""
 
-#: menus.py:117
+#: menus.py:132
 msgid "People"
 msgstr ""
 
-#: menus.py:139
+#: menus.py:157
 msgid "Persons and accounts"
 msgstr ""
 
-#: menus.py:152
-msgid "Edit school information"
+#: menus.py:168
+msgid "Groups and child groups"
 msgstr ""
 
-#: menus.py:153 templates/core/edit_schoolterm.html:8
-#: templates/core/edit_schoolterm.html:9
-msgid "Edit school term"
+#: menus.py:183 templates/core/groups_child_groups.html:7
+#: templates/core/groups_child_groups.html:9
+msgid "Assign child groups to groups"
 msgstr ""
 
-#: models.py:31 models.py:517
-msgid "Name"
+#: models.py:29
+msgid "Boolean (Yes/No)"
+msgstr ""
+
+#: models.py:30
+msgid "Text (one line)"
+msgstr ""
+
+#: models.py:32
+msgid "Date and time"
 msgstr ""
 
 #: models.py:33
-msgid "Official name"
+msgid "Decimal number"
+msgstr ""
+
+#: models.py:34 models.py:95
+msgid "E-mail address"
 msgstr ""
 
 #: models.py:35
-msgid "Official name of the school, e.g. as given by supervisory authority"
+msgid "Integer"
 msgstr ""
 
-#: models.py:38
-msgid "School logo"
+#: models.py:36
+msgid "IP address"
 msgstr ""
 
-#: models.py:51
-msgid "School"
+#: models.py:37
+msgid "Boolean or empty (Yes/No/Neither)"
 msgstr ""
 
-#: models.py:52
-msgid "Schools"
+#: models.py:38
+msgid "Text (multi-line)"
 msgstr ""
 
-#: models.py:60
-msgid "Visible caption of the term"
+#: models.py:40
+msgid "URL / Link"
 msgstr ""
 
-#: models.py:62
-msgid "Effective start date of term"
+#: models.py:53 templates/core/persons_accounts.html:36
+msgid "Person"
 msgstr ""
 
-#: models.py:63
-msgid "Effective end date of term"
+#: models.py:56
+msgid "Can view address"
 msgstr ""
 
-#: models.py:83
-msgid "School term"
+#: models.py:57
+msgid "Can view contact details"
 msgstr ""
 
-#: models.py:84
-msgid "School terms"
+#: models.py:58
+msgid "Can view photo"
 msgstr ""
 
-#: models.py:94 templates/core/persons_accounts.html:36
-msgid "Person"
+#: models.py:59
+msgid "Can view persons groups"
 msgstr ""
 
-#: models.py:97
+#: models.py:60
+msgid "Can view personal details"
+msgstr ""
+
+#: models.py:65
 msgid "female"
 msgstr ""
 
-#: models.py:97
+#: models.py:65
 msgid "male"
 msgstr ""
 
-#: models.py:102
+#: models.py:73
+msgid "Linked user"
+msgstr ""
+
+#: models.py:75
 msgid "Is person active?"
 msgstr ""
 
-#: models.py:104
+#: models.py:77
 msgid "First name"
 msgstr ""
 
-#: models.py:105
+#: models.py:78
 msgid "Last name"
 msgstr ""
 
-#: models.py:107
+#: models.py:80
 msgid "Additional name(s)"
 msgstr ""
 
-#: models.py:111
+#: models.py:84 models.py:260
 msgid "Short name"
 msgstr ""
 
-#: models.py:114
+#: models.py:87
 msgid "Street"
 msgstr ""
 
-#: models.py:115
+#: models.py:88
 msgid "Street number"
 msgstr ""
 
-#: models.py:116
+#: models.py:89
 msgid "Postal code"
 msgstr ""
 
-#: models.py:117
+#: models.py:90
 msgid "Place"
 msgstr ""
 
-#: models.py:119
+#: models.py:92
 msgid "Home phone"
 msgstr ""
 
-#: models.py:120
+#: models.py:93
 msgid "Mobile phone"
 msgstr ""
 
-#: models.py:122
-msgid "E-mail address"
-msgstr ""
-
-#: models.py:124
+#: models.py:97
 msgid "Date of birth"
 msgstr ""
 
-#: models.py:125
+#: models.py:98
 msgid "Sex"
 msgstr ""
 
-#: models.py:127
+#: models.py:100
 msgid "Photo"
 msgstr ""
 
-#: models.py:131 models.py:249
-msgid "Reference ID of import source"
+#: models.py:105
+msgid "Guardians / Parents"
 msgstr ""
 
-#: models.py:140
-msgid "Guardians / Parents"
+#: models.py:112
+msgid "Primary group"
 msgstr ""
 
-#: models.py:231
-msgid "Group"
+#: models.py:115 models.py:346 models.py:370 models.py:455 models.py:643
+msgid "Description"
 msgstr ""
 
-#: models.py:234
-msgid "Long name of group"
+#: models.py:233
+msgid "Title of field"
 msgstr ""
 
 #: models.py:235
-msgid "Short name of group"
+msgid "Type of field"
+msgstr ""
+
+#: models.py:239
+msgid "Addtitional field for groups"
 msgstr ""
 
-#: models.py:244
+#: models.py:240
+msgid "Addtitional fields for groups"
+msgstr ""
+
+#: models.py:252
+msgid "Group"
+msgstr ""
+
+#: models.py:254
+msgid "Can assign child groups to groups"
+msgstr ""
+
+#: models.py:258
+msgid "Long name"
+msgstr ""
+
+#: models.py:268 templates/core/group_full.html:37
+msgid "Members"
+msgstr ""
+
+#: models.py:271 templates/core/group_full.html:34
+msgid "Owners"
+msgstr ""
+
+#: models.py:278
 msgid "Parent groups"
 msgstr ""
 
-#: models.py:268 models.py:285 models.py:363
-#: templates/core/announcement/list.html:18
-msgid "Title"
+#: models.py:286
+msgid "Type of group"
 msgstr ""
 
-#: models.py:269 models.py:286 models.py:364
-msgid "Description"
+#: models.py:290
+msgid "Additional fields"
 msgstr ""
 
-#: models.py:271
+#: models.py:342
+msgid "User"
+msgstr ""
+
+#: models.py:345 models.py:369 models.py:454
+#: templates/core/announcement/list.html:18
+msgid "Title"
+msgstr ""
+
+#: models.py:348
 msgid "Application"
 msgstr ""
 
-#: models.py:277
+#: models.py:354
 msgid "Activity"
 msgstr ""
 
-#: models.py:278
+#: models.py:355
 msgid "Activities"
 msgstr ""
 
-#: models.py:282
+#: models.py:361
 msgid "Sender"
 msgstr ""
 
-#: models.py:287 models.py:365 models.py:518
+#: models.py:366
+msgid "Recipient"
+msgstr ""
+
+#: models.py:371 models.py:624
 msgid "Link"
 msgstr ""
 
-#: models.py:289
+#: models.py:373
 msgid "Read"
 msgstr ""
 
-#: models.py:290
+#: models.py:374
 msgid "Sent"
 msgstr ""
 
-#: models.py:301
+#: models.py:387
 msgid "Notification"
 msgstr ""
 
-#: models.py:302
+#: models.py:388
 msgid "Notifications"
 msgstr ""
 
-#: models.py:368
+#: models.py:456
+msgid "Link to detailed view"
+msgstr ""
+
+#: models.py:459
 msgid "Date and time from when to show"
 msgstr ""
 
-#: models.py:371
+#: models.py:462
 msgid "Date and time until when to show"
 msgstr ""
 
-#: models.py:394
+#: models.py:486
 msgid "Announcement"
 msgstr ""
 
-#: models.py:422
+#: models.py:524
 msgid "Announcement recipient"
 msgstr ""
 
-#: models.py:423
+#: models.py:525
 msgid "Announcement recipients"
 msgstr ""
 
-#: models.py:473
+#: models.py:575
 msgid "Widget Title"
 msgstr ""
 
-#: models.py:474
+#: models.py:576
 msgid "Activate Widget"
 msgstr ""
 
-#: models.py:486
+#: models.py:594
 msgid "Dashboard Widget"
 msgstr ""
 
-#: models.py:487
+#: models.py:595
 msgid "Dashboard Widgets"
 msgstr ""
 
-#: models.py:491
+#: models.py:601
 msgid "Menu ID"
 msgstr ""
 
-#: models.py:492
-msgid "Menu name"
-msgstr ""
-
-#: models.py:509
+#: models.py:613
 msgid "Custom menu"
 msgstr ""
 
-#: models.py:510
+#: models.py:614
 msgid "Custom menus"
 msgstr ""
 
-#: models.py:515
+#: models.py:621
 msgid "Menu"
 msgstr ""
 
-#: models.py:520
+#: models.py:623
+msgid "Name"
+msgstr ""
+
+#: models.py:625
 msgid "Icon"
 msgstr ""
 
-#: models.py:527
+#: models.py:631
 msgid "Custom menu item"
 msgstr ""
 
-#: models.py:528
+#: models.py:632
 msgid "Custom menu items"
 msgstr ""
 
-#: settings.py:254
-msgid "German"
+#: models.py:642
+msgid "Title of type"
 msgstr ""
 
-#: settings.py:255
-msgid "English"
+#: models.py:646
+msgid "Group type"
+msgstr ""
+
+#: models.py:647
+msgid "Group types"
+msgstr ""
+
+#: models.py:656
+msgid "Can view system status"
+msgstr ""
+
+#: models.py:657
+msgid "Can link persons to accounts"
+msgstr ""
+
+#: models.py:658
+msgid "Can manage data"
+msgstr ""
+
+#: models.py:659
+msgid "Can impersonate"
+msgstr ""
+
+#: models.py:660
+msgid "Can use search"
+msgstr ""
+
+#: models.py:661
+msgid "Can change site preferences"
+msgstr ""
+
+#: models.py:662
+msgid "Can change person preferences"
+msgstr ""
+
+#: models.py:663
+msgid "Can change group preferences"
 msgstr ""
 
-#: settings.py:373
+#: preferences.py:27
 msgid "Site title"
 msgstr ""
 
-#: settings.py:374
+#: preferences.py:36
 msgid "Site description"
 msgstr ""
 
-#: settings.py:375
+#: preferences.py:45
 msgid "Primary colour"
 msgstr ""
 
-#: settings.py:376
+#: preferences.py:54
 msgid "Secondary colour"
 msgstr ""
 
-#: settings.py:377
+#: preferences.py:62
+msgid "Logo"
+msgstr ""
+
+#: preferences.py:70
+msgid "Favicon"
+msgstr ""
+
+#: preferences.py:78
+msgid "PWA-Icon"
+msgstr ""
+
+#: preferences.py:87
 msgid "Mail out name"
 msgstr ""
 
-#: settings.py:378
+#: preferences.py:96
 msgid "Mail out address"
 msgstr ""
 
-#: settings.py:379
+#: preferences.py:106
 msgid "Link to privacy policy"
 msgstr ""
 
-#: settings.py:380
+#: preferences.py:116
 msgid "Link to imprint"
 msgstr ""
 
-#: settings.py:381
-msgid "Name format of adresses"
+#: preferences.py:126
+msgid "Name format for addressing"
 msgstr ""
 
-#: settings.py:382
-msgid "Channels to allow for notifications"
+#: preferences.py:140
+msgid "Channels to use for notifications"
 msgstr ""
 
-#: settings.py:383
+#: preferences.py:150
 msgid "Regular expression to match primary group, e.g. '^Class .*'"
 msgstr ""
 
+#: preferences.py:159
+msgid "Field on person to match primary group against"
+msgstr ""
+
+#: preferences.py:171
+msgid "Display name of the school"
+msgstr ""
+
+#: preferences.py:180
+msgid "Official name of the school, e.g. as given by supervisory authority"
+msgstr ""
+
+#: settings.py:276
+msgid "English"
+msgstr ""
+
+#: settings.py:277
+msgid "German"
+msgstr ""
+
+#: settings.py:278
+msgid "French"
+msgstr ""
+
+#: templates/403.html:10 templates/404.html:10 templates/500.html:10
+msgid "Error"
+msgstr ""
+
 #: templates/403.html:10
-msgid "Error (403): You are not allowed to access the requested page or object."
+msgid ""
+"You are not allowed to access the requested page or\n"
+"          object."
 msgstr ""
 
-#: templates/403.html:12
+#: templates/403.html:13 templates/404.html:17
 msgid ""
 "\n"
-"      If you think this is an error in AlekSIS, please contact your site\n"
-"     administrators:\n"
-"     "
+"            If you think this is an error in AlekSIS, please contact your "
+"site\n"
+"            administrators:\n"
+"          "
 msgstr ""
 
 #: templates/404.html:10
-msgid "Error (404): The requested page or object was not found."
+msgid ""
+"The requested page or object was not\n"
+"          found."
 msgstr ""
 
-#: templates/404.html:12
+#: templates/404.html:13
 msgid ""
 "\n"
-"      If you were redirected by a link on an external page,\n"
-"      it is possible that that link was outdated.\n"
-"     "
+"            If you were redirected by a link on an external page,\n"
+"            it is possible that that link was outdated.\n"
+"          "
+msgstr ""
+
+#: templates/500.html:10
+msgid ""
+"An unexpected error has\n"
+"          occured."
 msgstr ""
 
-#: templates/404.html:16
+#: templates/500.html:13
 msgid ""
 "\n"
-"      If you think this is an error in AlekSIS, please contact your site\n"
-"      administrators:\n"
-"     "
+"            Your site administrators will automatically be notified about "
+"this\n"
+"            error. You can also contact them directly:\n"
+"          "
 msgstr ""
 
-#: templates/500.html:10
-msgid "Error (500): An unexpected error has occured.."
+#: templates/503.html:10
+msgid ""
+"The maintenance mode is currently enabled. Please try again\n"
+"          later."
 msgstr ""
 
-#: templates/500.html:12
+#: templates/503.html:13
 msgid ""
 "\n"
-"      Your site administrators will automatically be notified about this\n"
-"     error.\n"
-"     "
+"            This page is currently unavailable. If this error persists, "
+"contact your site administrators:\n"
+"          "
 msgstr ""
 
-#: templates/503.html:10
-msgid "The maintenance mode is currently enabled. Please try again later."
+#: templates/core/about.html:6 templates/core/about.html:15
+msgid "About AlekSIS"
+msgstr ""
+
+#: templates/core/about.html:7
+msgid "AlekSIS – The Free School Information System"
+msgstr ""
+
+#: templates/core/about.html:17
+msgid ""
+"\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 ""
+
+#: templates/core/about.html:25
+msgid "Website of AlekSIS"
+msgstr ""
+
+#: templates/core/about.html:26
+msgid "Source code"
 msgstr ""
 
-#: templates/503.html:12
+#: templates/core/about.html:35
+msgid "Licence information"
+msgstr ""
+
+#: templates/core/about.html:37
+msgid ""
+"\n"
+"              The core and the official apps of AlekSIS are licenced under "
+"the EUPL, version 1.2 or later. For licence\n"
+"              information from third-party apps, if installed, refer to the "
+"respective components below. The\n"
+"              licences are marked like this:\n"
+"            "
+msgstr ""
+
+#: templates/core/about.html:45
+msgid "Free/Open Source Licence"
+msgstr ""
+
+#: templates/core/about.html:46
+msgid "Other Licence"
+msgstr ""
+
+#: templates/core/about.html:50
+msgid "Full licence text"
+msgstr ""
+
+#: templates/core/about.html:51
+msgid "More information about the EUPL"
+msgstr ""
+
+#: templates/core/about.html:90
+#, python-format
 msgid ""
 "\n"
-"      This page is currently unavailable. If this error stays, contact your site administrators:\n"
-"     "
+"                    This app is licenced under %(licence)s.\n"
+"                  "
 msgstr ""
 
 #: templates/core/announcement/form.html:10
@@ -544,8 +777,8 @@ msgstr ""
 msgid "Actions"
 msgstr ""
 
-#: templates/core/announcement/list.html:36 templates/core/group_full.html:15
-#: templates/core/person_full.html:15
+#: templates/core/announcement/list.html:36 templates/core/group_full.html:22
+#: templates/core/person_full.html:21
 msgid "Edit"
 msgstr ""
 
@@ -585,15 +818,24 @@ msgstr ""
 msgid "Logged in as"
 msgstr ""
 
-#: templates/core/base.html:146
+#: templates/core/base.html:76 templates/search/search.html:7
+#: templates/search/search.html:22
+msgid "Search"
+msgstr ""
+
+#: templates/core/base.html:148
+msgid "About AlekSIS — The Free School Information System"
+msgstr ""
+
+#: templates/core/base.html:156
 msgid "Impress"
 msgstr ""
 
-#: templates/core/base.html:154
+#: templates/core/base.html:164
 msgid "Privacy Policy"
 msgstr ""
 
-#: templates/core/base_print.html:60
+#: templates/core/base_print.html:62
 msgid "Powered by AlekSIS"
 msgstr ""
 
@@ -605,71 +847,132 @@ msgstr ""
 msgid "Edit person"
 msgstr ""
 
-#: templates/core/edit_school.html:8 templates/core/edit_school.html:9
-msgid "Edit school"
+#: templates/core/group_full.html:28 templates/core/person_full.html:28
+msgid "Change preferences"
 msgstr ""
 
-#: templates/core/group_full.html:19
-msgid "Owners"
+#: templates/core/groups.html:14
+msgid "Create group"
 msgstr ""
 
-#: templates/core/group_full.html:22
-msgid "Members"
+#: templates/core/groups_child_groups.html:18
+msgid ""
+"\n"
+"          You can use this to assign child groups to groups. Please use the "
+"filters below to select groups you want to\n"
+"          change and click \"Next\".\n"
+"        "
 msgstr ""
 
-#: templates/core/groups.html:14
-msgid "Create group"
+#: templates/core/groups_child_groups.html:31
+msgid "Update selection"
 msgstr ""
 
-#: templates/core/index.html:4
-msgid "Home"
+#: templates/core/groups_child_groups.html:35
+msgid "Clear all filters"
+msgstr ""
+
+#: templates/core/groups_child_groups.html:39
+msgid "Currently selected groups"
+msgstr ""
+
+#: templates/core/groups_child_groups.html:52
+msgid "Start assigning child groups for this groups"
+msgstr ""
+
+#: templates/core/groups_child_groups.html:61
+msgid ""
+"\n"
+"            Please select some groups in order to go on with assigning.\n"
+"          "
+msgstr ""
+
+#: templates/core/groups_child_groups.html:72
+msgid "Current group:"
+msgstr ""
+
+#: templates/core/groups_child_groups.html:78
+msgid "Please be careful!"
+msgstr ""
+
+#: templates/core/groups_child_groups.html:79
+msgid ""
+"\n"
+"            If you click \"Back\" or \"Next\" the current group assignments "
+"are not saved.\n"
+"            If you click \"Save\", you will overwrite all existing child "
+"group relations for this group with what you\n"
+"            selected on this page.\n"
+"          "
+msgstr ""
+
+#: templates/core/groups_child_groups.html:93
+#: templates/core/groups_child_groups.html:128
+#: templates/two_factor/_wizard_actions.html:15
+#: templates/two_factor/_wizard_actions.html:20
+msgid "Back"
 msgstr ""
 
-#: templates/core/index.html:11
-msgid "AlekSIS (School Information System)"
+#: templates/core/groups_child_groups.html:99
+#: templates/core/groups_child_groups.html:134
+#: templates/two_factor/_wizard_actions.html:26
+msgid "Next"
+msgstr ""
+
+#: templates/core/groups_child_groups.html:106
+#: templates/core/groups_child_groups.html:141
+#: templates/core/save_button.html:3
+msgid "Save"
+msgstr ""
+
+#: templates/core/groups_child_groups.html:112
+#: templates/core/groups_child_groups.html:147
+msgid "Save and next"
+msgstr ""
+
+#: templates/core/index.html:4
+msgid "Home"
 msgstr ""
 
-#: templates/core/index.html:43
+#: templates/core/index.html:42
 msgid "Last activities"
 msgstr ""
 
-#: templates/core/index.html:61
+#: templates/core/index.html:60
 msgid "No activities available yet."
 msgstr ""
 
-#: templates/core/index.html:66
+#: templates/core/index.html:65
 msgid "Recent notifications"
 msgstr ""
 
-#: templates/core/index.html:82
+#: templates/core/index.html:81
 msgid "More information →"
 msgstr ""
 
-#: templates/core/index.html:89
+#: templates/core/index.html:88
 msgid "No notifications available yet."
 msgstr ""
 
-#: templates/core/no_person.html:11
+#: templates/core/no_person.html:12
 msgid ""
 "\n"
-"          Your user account is not linked to a person. This means you\n"
-"          cannot access any school-related information. Please contact\n"
-"          the managers of AlekSIS at your school.\n"
-"        "
-msgstr ""
-
-#: templates/core/offline.html:6
-msgid "No internet connection."
+"            Your administrator account is not linked to any person. "
+"Therefore,\n"
+"            a dummy person has been linked to your account.\n"
+"          "
 msgstr ""
 
-#: templates/core/offline.html:9
+#: templates/core/no_person.html:19
 msgid ""
 "\n"
-"        There was an error accessing this page. You probably don't have an internet connection. Check to see if your WiFi or mobile data is turned on and try again. If you think you are connected, please contact the system administrators:\n"
-"      "
+"            Your user account is not linked to a person. This means you\n"
+"            cannot access any school-related information. Please contact\n"
+"            the managers of AlekSIS at your school.\n"
+"          "
 msgstr ""
 
-#: templates/core/person_full.html:19
+#: templates/core/person_full.html:34
 msgid "Contact details"
 msgstr ""
 
@@ -682,7 +985,8 @@ msgstr ""
 msgid ""
 "\n"
 "        You can use this form to assign user accounts to persons. Use the\n"
-"        dropdowns to select existing accounts; use the text fields to create new\n"
+"        dropdowns to select existing accounts; use the text fields to create "
+"new\n"
 "        accounts on-the-fly. The latter will create a new account with the\n"
 "        entered username and copy all other details from the person.\n"
 "      "
@@ -701,15 +1005,6 @@ msgstr ""
 msgid "New account"
 msgstr ""
 
-#: templates/core/save_button.html:3
-msgid "Save"
-msgstr ""
-
-#: templates/core/school_management.html:6
-#: templates/core/school_management.html:7
-msgid "School management"
-msgstr ""
-
 #: templates/core/system_status.html:12
 msgid "System checks"
 msgstr ""
@@ -721,7 +1016,8 @@ msgstr ""
 #: templates/core/system_status.html:23
 msgid ""
 "\n"
-"                Only admin and visitors from internal IPs can access thesite.\n"
+"                Only admin and visitors from internal IPs can access "
+"thesite.\n"
 "              "
 msgstr ""
 
@@ -740,7 +1036,8 @@ msgstr ""
 #: templates/core/system_status.html:47
 msgid ""
 "\n"
-"                The web server throws back debug information on errors. Do not use in production!\n"
+"                The web server throws back debug information on errors. Do "
+"not use in production!\n"
 "              "
 msgstr ""
 
@@ -751,105 +1048,97 @@ msgstr ""
 #: templates/core/system_status.html:56
 msgid ""
 "\n"
-"                Debug mode is disabled. Default error pages are displayed on errors.\n"
+"                Debug mode is disabled. Default error pages are displayed on "
+"errors.\n"
 "              "
 msgstr ""
 
-#: templates/impersonate/list_users.html:8
-msgid "Impersonate user"
-msgstr ""
-
-#: templates/martor/editor.html:27
-msgid "Uploading... please wait..."
-msgstr ""
-
-#: templates/martor/editor.html:36
-msgid "Nothing to preview"
-msgstr ""
-
-#: templates/martor/emoji.html:4
-msgid "Select Emoji to Insert"
+#: templates/dynamic_preferences/form.html:9
+msgid "Site preferences"
 msgstr ""
 
-#: templates/martor/emoji.html:8
-msgid "Preparing emojis..."
+#: templates/dynamic_preferences/form.html:11
+msgid "My preferences"
 msgstr ""
 
-#: templates/martor/guide.html:8
-msgid "Markdown Guide"
-msgstr ""
-
-#: templates/martor/guide.html:9
+#: templates/dynamic_preferences/form.html:13
 #, python-format
-msgid ""
-"This site is powered by Markdown. For full\n"
-"            documentation,\n"
-"            <a href=\"%(doc_url)s\" target=\"_blank\">click here</a>"
-msgstr ""
-
-#: templates/martor/guide.html:15 templates/martor/toolbar.html:42
-msgid "Code"
+msgid "Preferences for %(instance)s"
 msgstr ""
 
-#: templates/martor/guide.html:16
-msgid "Or"
+#: templates/dynamic_preferences/form.html:25
+msgid "Save preferences"
 msgstr ""
 
-#: templates/martor/guide.html:19
-msgid "... to Get"
+#: templates/dynamic_preferences/sections.html:7
+msgid "All"
 msgstr ""
 
-#: templates/martor/toolbar.html:3
-msgid "Bold"
+#: templates/impersonate/list_users.html:8
+msgid "Impersonate user"
 msgstr ""
 
-#: templates/martor/toolbar.html:6
-msgid "Italic"
+#: templates/offline.html:6
+msgid ""
+"No internet\n"
+"    connection."
 msgstr ""
 
-#: templates/martor/toolbar.html:10
-msgid "Horizontal Line"
+#: templates/offline.html:10
+msgid ""
+"\n"
+"      There was an error accessing this page. You probably don't have an "
+"internet connection. Check to see if your WiFi\n"
+"      or mobile data is turned on and try again. If you think you are "
+"connected, please contact the system\n"
+"      administrators:\n"
+"    "
 msgstr ""
 
-#: templates/martor/toolbar.html:15
-msgid "Heading"
+#: templates/search/search.html:8
+msgid "Global Search"
 msgstr ""
 
-#: templates/martor/toolbar.html:20 templates/martor/toolbar.html:23
-#: templates/martor/toolbar.html:26
-msgid "H"
+#: templates/search/search.html:15
+msgid "Search Term"
 msgstr ""
 
-#: templates/martor/toolbar.html:31
-msgid "Pre or Code"
+#: templates/search/search.html:26
+msgid "Results"
 msgstr ""
 
-#: templates/martor/toolbar.html:38
-msgid "Pre"
+#: templates/search/search.html:38
+msgid "No search results could be found to your search."
 msgstr ""
 
-#: templates/martor/toolbar.html:48
-msgid "Quote"
+#: templates/search/search.html:87
+msgid "Please enter a search term above."
 msgstr ""
 
-#: templates/martor/toolbar.html:52
-msgid "Unordered List"
+#: templates/templated_email/notification.email:3
+msgid "New notification for"
 msgstr ""
 
-#: templates/martor/toolbar.html:56
-msgid "Ordered List"
+#: templates/templated_email/notification.email:7
+msgid "Dear"
 msgstr ""
 
-#: templates/martor/toolbar.html:60
-msgid "URL/Link"
+#: templates/templated_email/notification.email:8
+msgid "we got a new notification for you:"
 msgstr ""
 
-#: templates/martor/toolbar.html:82
-msgid "Full Screen"
+#: templates/templated_email/notification.email:12
+msgid "More information"
 msgstr ""
 
-#: templates/martor/toolbar.html:86
-msgid "Markdown Guide (Help)"
+#: templates/templated_email/notification.email:16
+#, python-format
+msgid ""
+"\n"
+"    <p>By %(trans_sender)s at %(trans_created_at)s</p>\n"
+"\n"
+"    <i>Your AlekSIS team</i>\n"
+"    "
 msgstr ""
 
 #: templates/two_factor/_base_focus.html:6
@@ -863,15 +1152,6 @@ msgstr ""
 msgid "Cancel"
 msgstr ""
 
-#: templates/two_factor/_wizard_actions.html:15
-#: templates/two_factor/_wizard_actions.html:20
-msgid "Back"
-msgstr ""
-
-#: templates/two_factor/_wizard_actions.html:26
-msgid "Next"
-msgstr ""
-
 #: templates/two_factor/core/backup_tokens.html:5
 #: templates/two_factor/core/backup_tokens.html:9
 #: templates/two_factor/profile/profile.html:46
@@ -883,8 +1163,10 @@ msgid ""
 "\n"
 "        Backup tokens can be used when your primary and backup\n"
 "        phone numbers aren't available. The backup tokens below can be used\n"
-"        for login verification. If you've used up all your backup tokens, you\n"
-"        can generate a new set of backup tokens. Only the backup tokens shown\n"
+"        for login verification. If you've used up all your backup tokens, "
+"you\n"
+"        can generate a new set of backup tokens. Only the backup tokens "
+"shown\n"
 "        below will be valid.\n"
 "      "
 msgstr ""
@@ -908,44 +1190,50 @@ msgstr ""
 msgid "Generate Tokens"
 msgstr ""
 
-#: templates/two_factor/core/login.html:17
-msgid "Enter your credentials."
+#: templates/two_factor/core/login.html:16
+msgid ""
+"You have no permission to view this page. Please login with an other account."
+msgstr ""
+
+#: templates/two_factor/core/login.html:25
+msgid "Please login to see this page."
 msgstr ""
 
-#: templates/two_factor/core/login.html:20
+#: templates/two_factor/core/login.html:28
 msgid ""
 "We are calling your phone right now, please enter the\n"
-"            digits you hear."
+"              digits you hear."
 msgstr ""
 
-#: templates/two_factor/core/login.html:23
+#: templates/two_factor/core/login.html:31
 msgid ""
 "We sent you a text message, please enter the tokens we\n"
-"            sent."
+"              sent."
 msgstr ""
 
-#: templates/two_factor/core/login.html:26
+#: templates/two_factor/core/login.html:34
 msgid ""
 "Please enter the tokens generated by your token\n"
-"            generator."
+"              generator."
 msgstr ""
 
-#: templates/two_factor/core/login.html:30
+#: templates/two_factor/core/login.html:38
 msgid ""
 "Use this form for entering backup tokens for logging in.\n"
-"          These tokens have been generated for you to print and keep safe. Please\n"
-"          enter one of these backup tokens to login to your account."
+"            These tokens have been generated for you to print and keep safe. "
+"Please\n"
+"            enter one of these backup tokens to login to your account."
 msgstr ""
 
-#: templates/two_factor/core/login.html:47
+#: templates/two_factor/core/login.html:56
 msgid "Or, alternatively, use one of your backup phones:"
 msgstr ""
 
-#: templates/two_factor/core/login.html:57
+#: templates/two_factor/core/login.html:66
 msgid "As a last resort, you can use a backup token:"
 msgstr ""
 
-#: templates/two_factor/core/login.html:60
+#: templates/two_factor/core/login.html:69
 msgid "Use Backup Token"
 msgstr ""
 
@@ -956,7 +1244,8 @@ msgstr ""
 #: templates/two_factor/core/otp_required.html:10
 msgid ""
 "The page you requested, enforces users to verify using\n"
-"          two-factor authentication for security reasons. You need to enable these\n"
+"          two-factor authentication for security reasons. You need to enable "
+"these\n"
 "          security features in order to access this page."
 msgstr ""
 
@@ -1033,7 +1322,8 @@ msgstr ""
 #: templates/two_factor/core/setup.html:50
 msgid ""
 "\n"
-"            We are calling your phone right now, please enter the digits you hear.\n"
+"            We are calling your phone right now, please enter the digits you "
+"hear.\n"
 "          "
 msgstr ""
 
@@ -1047,9 +1337,12 @@ msgstr ""
 #: templates/two_factor/core/setup.html:63
 msgid ""
 "\n"
-"          We've encountered an issue with the selected authentication method. Please\n"
-"          go back and verify that you entered your information correctly, try\n"
-"          again, or use a different authentication method instead. If the issue\n"
+"          We've encountered an issue with the selected authentication "
+"method. Please\n"
+"          go back and verify that you entered your information correctly, "
+"try\n"
+"          again, or use a different authentication method instead. If the "
+"issue\n"
 "          persists, contact the site administrator.\n"
 "        "
 msgstr ""
@@ -1071,7 +1364,8 @@ msgstr ""
 #: templates/two_factor/core/setup_complete.html:14
 msgid ""
 "\n"
-"        Congratulations, you've successfully enabled two-factor authentication.\n"
+"        Congratulations, you've successfully enabled two-factor "
+"authentication.\n"
 "      "
 msgstr ""
 
@@ -1089,7 +1383,8 @@ msgstr ""
 msgid ""
 "\n"
 "          However, it might happen that you don't have access to\n"
-"          your primary token device. To enable account recovery, generate backup codes\n"
+"          your primary token device. To enable account recovery, generate "
+"backup codes\n"
 "          or add a phone number.\n"
 "        "
 msgstr ""
@@ -1107,7 +1402,9 @@ msgid "Disable Two-Factor Authentication"
 msgstr ""
 
 #: templates/two_factor/profile/disable.html:12
-msgid "You are about to disable two-factor authentication. This weakens your account security, are you sure?"
+msgid ""
+"You are about to disable two-factor authentication. This weakens your "
+"account security, are you sure?"
 msgstr ""
 
 #: templates/two_factor/profile/disable.html:26
@@ -1186,38 +1483,34 @@ msgid ""
 "      "
 msgstr ""
 
-#: util/notifications.py:66
+#: util/notifications.py:65
 msgid "E-Mail"
 msgstr ""
 
-#: util/notifications.py:67
+#: util/notifications.py:66
 msgid "SMS"
 msgstr ""
 
-#: views.py:172
-msgid "The person has been saved."
-msgstr ""
-
-#: views.py:195
-msgid "The group has been saved."
-msgstr ""
-
-#: views.py:236
-msgid "The school has been saved."
+#: views.py:212
+msgid "The child groups were successfully saved."
 msgstr ""
 
-#: views.py:255
-msgid "The term has been saved."
+#: views.py:240
+msgid "The person has been saved."
 msgstr ""
 
-#: views.py:272
-msgid "You are not allowed to mark notifications from other users as read!"
+#: views.py:276
+msgid "The group has been saved."
 msgstr ""
 
-#: views.py:307
+#: views.py:348
 msgid "The announcement has been saved."
 msgstr ""
 
-#: views.py:320
+#: views.py:364
 msgid "The announcement has been deleted."
 msgstr ""
+
+#: views.py:435
+msgid "The preferences have been saved successfully."
+msgstr ""
diff --git a/aleksis/core/locale/nb_NO/LC_MESSAGES/djangojs.po b/aleksis/core/locale/nb_NO/LC_MESSAGES/djangojs.po
index d62e52377a4ce6af7272b5568c5da2a4698fb057..21309c0096e0f1ec402dfa7c4e2f8604b96e2466 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: 2020-03-30 09:18+0000\n"
+"POT-Creation-Date: 2020-04-28 13:31+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"
diff --git a/aleksis/core/locale/tr_TR/LC_MESSAGES/django.po b/aleksis/core/locale/tr_TR/LC_MESSAGES/django.po
index 495b614c5c6694495737ed677af23b37c0e15153..3e169fa0130f90c88bfaba22972f8bf0a7163a7f 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: 2020-03-30 09:18+0000\n"
+"POT-Creation-Date: 2020-05-04 15:39+0200\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,68 +17,88 @@ msgstr ""
 "Content-Type: text/plain; charset=UTF-8\n"
 "Content-Transfer-Encoding: 8bit\n"
 
-#: forms.py:38 forms.py:93
+#: forms.py:46
 msgid "You cannot set a new username when also selecting an existing user."
 msgstr ""
 
-#: forms.py:41 forms.py:96
+#: forms.py:50
 msgid "This username is already in use."
 msgstr ""
 
+#: forms.py:74
+msgid "Base data"
+msgstr ""
+
+#: forms.py:80
+msgid "Address"
+msgstr ""
+
+#: forms.py:81
+msgid "Contact data"
+msgstr ""
+
 #: forms.py:83
+msgid "Advanced personal data"
+msgstr ""
+
+#: forms.py:116
 msgid "New user"
 msgstr ""
 
-#: forms.py:83
+#: forms.py:116
 msgid "Create a new account"
 msgstr ""
 
-#: forms.py:149 forms.py:152
-msgid "Date"
+#: forms.py:128
+msgid "Common data"
 msgstr ""
 
-#: forms.py:150 forms.py:153
-msgid "Time"
+#: forms.py:129 forms.py:169 menus.py:141 models.py:54
+#: templates/core/persons.html:8 templates/core/persons.html:9
+msgid "Persons"
 msgstr ""
 
-#: forms.py:155 menus.py:127 models.py:95 templates/core/persons.html:8
-#: templates/core/persons.html:9
-msgid "Persons"
+#: forms.py:162 forms.py:165 models.py:31
+msgid "Date"
 msgstr ""
 
-#: forms.py:156 menus.py:133 models.py:232 templates/core/groups.html:8
-#: templates/core/groups.html:9 templates/core/person_full.html:79
+#: forms.py:163 forms.py:166 models.py:39
+msgid "Time"
+msgstr ""
+
+#: forms.py:171 menus.py:149 models.py:253 templates/core/groups.html:8
+#: templates/core/groups.html:9 templates/core/person_full.html:106
 msgid "Groups"
 msgstr ""
 
-#: forms.py:160
+#: forms.py:175
 msgid "From when until when should the announcement be displayed?"
 msgstr ""
 
-#: forms.py:163
+#: forms.py:178
 msgid "Who should see the announcement?"
 msgstr ""
 
-#: forms.py:164
+#: forms.py:179
 msgid "Write your announcement:"
 msgstr ""
 
-#: forms.py:203
-msgid "You are not allowed to create announcements which are only valid in the past."
+#: forms.py:216
+msgid ""
+"You are not allowed to create announcements which are only valid in the past."
 msgstr ""
 
-#: forms.py:207
+#: forms.py:220
 msgid "The from date and time must be earlier then the until date and time."
 msgstr ""
 
-#: forms.py:215
+#: forms.py:229
 msgid "You need at least one recipient."
 msgstr ""
 
-#: menus.py:7 templates/registration/login.html:21
-#: templates/two_factor/core/login.html:6
+#: menus.py:7 templates/two_factor/core/login.html:6
 #: templates/two_factor/core/login.html:10
-#: templates/two_factor/core/login.html:64
+#: templates/two_factor/core/login.html:73
 msgid "Login"
 msgstr ""
 
@@ -99,415 +119,628 @@ msgid "Logout"
 msgstr ""
 
 #: menus.py:41
-msgid "Two factor auth"
+msgid "2FA"
 msgstr ""
 
-#: menus.py:52
+#: menus.py:47
+msgid "Me"
+msgstr ""
+
+#: menus.py:56 templates/dynamic_preferences/form.html:5
+msgid "Preferences"
+msgstr ""
+
+#: menus.py:67
 msgid "Admin"
 msgstr ""
 
-#: menus.py:61 models.py:395 templates/core/announcement/list.html:7
+#: menus.py:75 models.py:487 templates/core/announcement/list.html:7
 #: templates/core/announcement/list.html:8
 msgid "Announcements"
 msgstr ""
 
-#: menus.py:70 templates/core/data_management.html:6
+#: menus.py:86 templates/core/data_management.html:6
 #: templates/core/data_management.html:7
 msgid "Data management"
 msgstr ""
 
-#: menus.py:79 templates/core/system_status.html:5
+#: menus.py:94 templates/core/system_status.html:5
 #: templates/core/system_status.html:7
 msgid "System status"
 msgstr ""
 
-#: menus.py:88
+#: menus.py:105
 msgid "Impersonation"
 msgstr ""
 
-#: menus.py:97
-msgid "Manage school"
+#: menus.py:113
+msgid "Configuration"
 msgstr ""
 
-#: menus.py:106
+#: menus.py:124
 msgid "Backend Admin"
 msgstr ""
 
-#: menus.py:117
+#: menus.py:132
 msgid "People"
 msgstr ""
 
-#: menus.py:139
+#: menus.py:157
 msgid "Persons and accounts"
 msgstr ""
 
-#: menus.py:152
-msgid "Edit school information"
+#: menus.py:168
+msgid "Groups and child groups"
 msgstr ""
 
-#: menus.py:153 templates/core/edit_schoolterm.html:8
-#: templates/core/edit_schoolterm.html:9
-msgid "Edit school term"
+#: menus.py:183 templates/core/groups_child_groups.html:7
+#: templates/core/groups_child_groups.html:9
+msgid "Assign child groups to groups"
 msgstr ""
 
-#: models.py:31 models.py:517
-msgid "Name"
+#: models.py:29
+msgid "Boolean (Yes/No)"
+msgstr ""
+
+#: models.py:30
+msgid "Text (one line)"
+msgstr ""
+
+#: models.py:32
+msgid "Date and time"
 msgstr ""
 
 #: models.py:33
-msgid "Official name"
+msgid "Decimal number"
+msgstr ""
+
+#: models.py:34 models.py:95
+msgid "E-mail address"
 msgstr ""
 
 #: models.py:35
-msgid "Official name of the school, e.g. as given by supervisory authority"
+msgid "Integer"
 msgstr ""
 
-#: models.py:38
-msgid "School logo"
+#: models.py:36
+msgid "IP address"
 msgstr ""
 
-#: models.py:51
-msgid "School"
+#: models.py:37
+msgid "Boolean or empty (Yes/No/Neither)"
 msgstr ""
 
-#: models.py:52
-msgid "Schools"
+#: models.py:38
+msgid "Text (multi-line)"
 msgstr ""
 
-#: models.py:60
-msgid "Visible caption of the term"
+#: models.py:40
+msgid "URL / Link"
 msgstr ""
 
-#: models.py:62
-msgid "Effective start date of term"
+#: models.py:53 templates/core/persons_accounts.html:36
+msgid "Person"
 msgstr ""
 
-#: models.py:63
-msgid "Effective end date of term"
+#: models.py:56
+msgid "Can view address"
 msgstr ""
 
-#: models.py:83
-msgid "School term"
+#: models.py:57
+msgid "Can view contact details"
 msgstr ""
 
-#: models.py:84
-msgid "School terms"
+#: models.py:58
+msgid "Can view photo"
 msgstr ""
 
-#: models.py:94 templates/core/persons_accounts.html:36
-msgid "Person"
+#: models.py:59
+msgid "Can view persons groups"
 msgstr ""
 
-#: models.py:97
+#: models.py:60
+msgid "Can view personal details"
+msgstr ""
+
+#: models.py:65
 msgid "female"
 msgstr ""
 
-#: models.py:97
+#: models.py:65
 msgid "male"
 msgstr ""
 
-#: models.py:102
+#: models.py:73
+msgid "Linked user"
+msgstr ""
+
+#: models.py:75
 msgid "Is person active?"
 msgstr ""
 
-#: models.py:104
+#: models.py:77
 msgid "First name"
 msgstr ""
 
-#: models.py:105
+#: models.py:78
 msgid "Last name"
 msgstr ""
 
-#: models.py:107
+#: models.py:80
 msgid "Additional name(s)"
 msgstr ""
 
-#: models.py:111
+#: models.py:84 models.py:260
 msgid "Short name"
 msgstr ""
 
-#: models.py:114
+#: models.py:87
 msgid "Street"
 msgstr ""
 
-#: models.py:115
+#: models.py:88
 msgid "Street number"
 msgstr ""
 
-#: models.py:116
+#: models.py:89
 msgid "Postal code"
 msgstr ""
 
-#: models.py:117
+#: models.py:90
 msgid "Place"
 msgstr ""
 
-#: models.py:119
+#: models.py:92
 msgid "Home phone"
 msgstr ""
 
-#: models.py:120
+#: models.py:93
 msgid "Mobile phone"
 msgstr ""
 
-#: models.py:122
-msgid "E-mail address"
-msgstr ""
-
-#: models.py:124
+#: models.py:97
 msgid "Date of birth"
 msgstr ""
 
-#: models.py:125
+#: models.py:98
 msgid "Sex"
 msgstr ""
 
-#: models.py:127
+#: models.py:100
 msgid "Photo"
 msgstr ""
 
-#: models.py:131 models.py:249
-msgid "Reference ID of import source"
+#: models.py:105
+msgid "Guardians / Parents"
 msgstr ""
 
-#: models.py:140
-msgid "Guardians / Parents"
+#: models.py:112
+msgid "Primary group"
 msgstr ""
 
-#: models.py:231
-msgid "Group"
+#: models.py:115 models.py:346 models.py:370 models.py:455 models.py:643
+msgid "Description"
 msgstr ""
 
-#: models.py:234
-msgid "Long name of group"
+#: models.py:233
+msgid "Title of field"
 msgstr ""
 
 #: models.py:235
-msgid "Short name of group"
+msgid "Type of field"
+msgstr ""
+
+#: models.py:239
+msgid "Addtitional field for groups"
 msgstr ""
 
-#: models.py:244
+#: models.py:240
+msgid "Addtitional fields for groups"
+msgstr ""
+
+#: models.py:252
+msgid "Group"
+msgstr ""
+
+#: models.py:254
+msgid "Can assign child groups to groups"
+msgstr ""
+
+#: models.py:258
+msgid "Long name"
+msgstr ""
+
+#: models.py:268 templates/core/group_full.html:37
+msgid "Members"
+msgstr ""
+
+#: models.py:271 templates/core/group_full.html:34
+msgid "Owners"
+msgstr ""
+
+#: models.py:278
 msgid "Parent groups"
 msgstr ""
 
-#: models.py:268 models.py:285 models.py:363
-#: templates/core/announcement/list.html:18
-msgid "Title"
+#: models.py:286
+msgid "Type of group"
 msgstr ""
 
-#: models.py:269 models.py:286 models.py:364
-msgid "Description"
+#: models.py:290
+msgid "Additional fields"
 msgstr ""
 
-#: models.py:271
+#: models.py:342
+msgid "User"
+msgstr ""
+
+#: models.py:345 models.py:369 models.py:454
+#: templates/core/announcement/list.html:18
+msgid "Title"
+msgstr ""
+
+#: models.py:348
 msgid "Application"
 msgstr ""
 
-#: models.py:277
+#: models.py:354
 msgid "Activity"
 msgstr ""
 
-#: models.py:278
+#: models.py:355
 msgid "Activities"
 msgstr ""
 
-#: models.py:282
+#: models.py:361
 msgid "Sender"
 msgstr ""
 
-#: models.py:287 models.py:365 models.py:518
+#: models.py:366
+msgid "Recipient"
+msgstr ""
+
+#: models.py:371 models.py:624
 msgid "Link"
 msgstr ""
 
-#: models.py:289
+#: models.py:373
 msgid "Read"
 msgstr ""
 
-#: models.py:290
+#: models.py:374
 msgid "Sent"
 msgstr ""
 
-#: models.py:301
+#: models.py:387
 msgid "Notification"
 msgstr ""
 
-#: models.py:302
+#: models.py:388
 msgid "Notifications"
 msgstr ""
 
-#: models.py:368
+#: models.py:456
+msgid "Link to detailed view"
+msgstr ""
+
+#: models.py:459
 msgid "Date and time from when to show"
 msgstr ""
 
-#: models.py:371
+#: models.py:462
 msgid "Date and time until when to show"
 msgstr ""
 
-#: models.py:394
+#: models.py:486
 msgid "Announcement"
 msgstr ""
 
-#: models.py:422
+#: models.py:524
 msgid "Announcement recipient"
 msgstr ""
 
-#: models.py:423
+#: models.py:525
 msgid "Announcement recipients"
 msgstr ""
 
-#: models.py:473
+#: models.py:575
 msgid "Widget Title"
 msgstr ""
 
-#: models.py:474
+#: models.py:576
 msgid "Activate Widget"
 msgstr ""
 
-#: models.py:486
+#: models.py:594
 msgid "Dashboard Widget"
 msgstr ""
 
-#: models.py:487
+#: models.py:595
 msgid "Dashboard Widgets"
 msgstr ""
 
-#: models.py:491
+#: models.py:601
 msgid "Menu ID"
 msgstr ""
 
-#: models.py:492
-msgid "Menu name"
-msgstr ""
-
-#: models.py:509
+#: models.py:613
 msgid "Custom menu"
 msgstr ""
 
-#: models.py:510
+#: models.py:614
 msgid "Custom menus"
 msgstr ""
 
-#: models.py:515
+#: models.py:621
 msgid "Menu"
 msgstr ""
 
-#: models.py:520
+#: models.py:623
+msgid "Name"
+msgstr ""
+
+#: models.py:625
 msgid "Icon"
 msgstr ""
 
-#: models.py:527
+#: models.py:631
 msgid "Custom menu item"
 msgstr ""
 
-#: models.py:528
+#: models.py:632
 msgid "Custom menu items"
 msgstr ""
 
-#: settings.py:254
-msgid "German"
+#: models.py:642
+msgid "Title of type"
 msgstr ""
 
-#: settings.py:255
-msgid "English"
+#: models.py:646
+msgid "Group type"
+msgstr ""
+
+#: models.py:647
+msgid "Group types"
+msgstr ""
+
+#: models.py:656
+msgid "Can view system status"
+msgstr ""
+
+#: models.py:657
+msgid "Can link persons to accounts"
+msgstr ""
+
+#: models.py:658
+msgid "Can manage data"
+msgstr ""
+
+#: models.py:659
+msgid "Can impersonate"
+msgstr ""
+
+#: models.py:660
+msgid "Can use search"
+msgstr ""
+
+#: models.py:661
+msgid "Can change site preferences"
+msgstr ""
+
+#: models.py:662
+msgid "Can change person preferences"
+msgstr ""
+
+#: models.py:663
+msgid "Can change group preferences"
 msgstr ""
 
-#: settings.py:373
+#: preferences.py:27
 msgid "Site title"
 msgstr ""
 
-#: settings.py:374
+#: preferences.py:36
 msgid "Site description"
 msgstr ""
 
-#: settings.py:375
+#: preferences.py:45
 msgid "Primary colour"
 msgstr ""
 
-#: settings.py:376
+#: preferences.py:54
 msgid "Secondary colour"
 msgstr ""
 
-#: settings.py:377
+#: preferences.py:62
+msgid "Logo"
+msgstr ""
+
+#: preferences.py:70
+msgid "Favicon"
+msgstr ""
+
+#: preferences.py:78
+msgid "PWA-Icon"
+msgstr ""
+
+#: preferences.py:87
 msgid "Mail out name"
 msgstr ""
 
-#: settings.py:378
+#: preferences.py:96
 msgid "Mail out address"
 msgstr ""
 
-#: settings.py:379
+#: preferences.py:106
 msgid "Link to privacy policy"
 msgstr ""
 
-#: settings.py:380
+#: preferences.py:116
 msgid "Link to imprint"
 msgstr ""
 
-#: settings.py:381
-msgid "Name format of adresses"
+#: preferences.py:126
+msgid "Name format for addressing"
 msgstr ""
 
-#: settings.py:382
-msgid "Channels to allow for notifications"
+#: preferences.py:140
+msgid "Channels to use for notifications"
 msgstr ""
 
-#: settings.py:383
+#: preferences.py:150
 msgid "Regular expression to match primary group, e.g. '^Class .*'"
 msgstr ""
 
+#: preferences.py:159
+msgid "Field on person to match primary group against"
+msgstr ""
+
+#: preferences.py:171
+msgid "Display name of the school"
+msgstr ""
+
+#: preferences.py:180
+msgid "Official name of the school, e.g. as given by supervisory authority"
+msgstr ""
+
+#: settings.py:276
+msgid "English"
+msgstr ""
+
+#: settings.py:277
+msgid "German"
+msgstr ""
+
+#: settings.py:278
+msgid "French"
+msgstr ""
+
+#: templates/403.html:10 templates/404.html:10 templates/500.html:10
+msgid "Error"
+msgstr ""
+
 #: templates/403.html:10
-msgid "Error (403): You are not allowed to access the requested page or object."
+msgid ""
+"You are not allowed to access the requested page or\n"
+"          object."
 msgstr ""
 
-#: templates/403.html:12
+#: templates/403.html:13 templates/404.html:17
 msgid ""
 "\n"
-"      If you think this is an error in AlekSIS, please contact your site\n"
-"     administrators:\n"
-"     "
+"            If you think this is an error in AlekSIS, please contact your "
+"site\n"
+"            administrators:\n"
+"          "
 msgstr ""
 
 #: templates/404.html:10
-msgid "Error (404): The requested page or object was not found."
+msgid ""
+"The requested page or object was not\n"
+"          found."
 msgstr ""
 
-#: templates/404.html:12
+#: templates/404.html:13
 msgid ""
 "\n"
-"      If you were redirected by a link on an external page,\n"
-"      it is possible that that link was outdated.\n"
-"     "
+"            If you were redirected by a link on an external page,\n"
+"            it is possible that that link was outdated.\n"
+"          "
+msgstr ""
+
+#: templates/500.html:10
+msgid ""
+"An unexpected error has\n"
+"          occured."
 msgstr ""
 
-#: templates/404.html:16
+#: templates/500.html:13
 msgid ""
 "\n"
-"      If you think this is an error in AlekSIS, please contact your site\n"
-"      administrators:\n"
-"     "
+"            Your site administrators will automatically be notified about "
+"this\n"
+"            error. You can also contact them directly:\n"
+"          "
 msgstr ""
 
-#: templates/500.html:10
-msgid "Error (500): An unexpected error has occured.."
+#: templates/503.html:10
+msgid ""
+"The maintenance mode is currently enabled. Please try again\n"
+"          later."
 msgstr ""
 
-#: templates/500.html:12
+#: templates/503.html:13
 msgid ""
 "\n"
-"      Your site administrators will automatically be notified about this\n"
-"     error.\n"
-"     "
+"            This page is currently unavailable. If this error persists, "
+"contact your site administrators:\n"
+"          "
 msgstr ""
 
-#: templates/503.html:10
-msgid "The maintenance mode is currently enabled. Please try again later."
+#: templates/core/about.html:6 templates/core/about.html:15
+msgid "About AlekSIS"
+msgstr ""
+
+#: templates/core/about.html:7
+msgid "AlekSIS – The Free School Information System"
+msgstr ""
+
+#: templates/core/about.html:17
+msgid ""
+"\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 ""
+
+#: templates/core/about.html:25
+msgid "Website of AlekSIS"
+msgstr ""
+
+#: templates/core/about.html:26
+msgid "Source code"
 msgstr ""
 
-#: templates/503.html:12
+#: templates/core/about.html:35
+msgid "Licence information"
+msgstr ""
+
+#: templates/core/about.html:37
+msgid ""
+"\n"
+"              The core and the official apps of AlekSIS are licenced under "
+"the EUPL, version 1.2 or later. For licence\n"
+"              information from third-party apps, if installed, refer to the "
+"respective components below. The\n"
+"              licences are marked like this:\n"
+"            "
+msgstr ""
+
+#: templates/core/about.html:45
+msgid "Free/Open Source Licence"
+msgstr ""
+
+#: templates/core/about.html:46
+msgid "Other Licence"
+msgstr ""
+
+#: templates/core/about.html:50
+msgid "Full licence text"
+msgstr ""
+
+#: templates/core/about.html:51
+msgid "More information about the EUPL"
+msgstr ""
+
+#: templates/core/about.html:90
+#, python-format
 msgid ""
 "\n"
-"      This page is currently unavailable. If this error stays, contact your site administrators:\n"
-"     "
+"                    This app is licenced under %(licence)s.\n"
+"                  "
 msgstr ""
 
 #: templates/core/announcement/form.html:10
@@ -544,8 +777,8 @@ msgstr ""
 msgid "Actions"
 msgstr ""
 
-#: templates/core/announcement/list.html:36 templates/core/group_full.html:15
-#: templates/core/person_full.html:15
+#: templates/core/announcement/list.html:36 templates/core/group_full.html:22
+#: templates/core/person_full.html:21
 msgid "Edit"
 msgstr ""
 
@@ -585,15 +818,24 @@ msgstr ""
 msgid "Logged in as"
 msgstr ""
 
-#: templates/core/base.html:146
+#: templates/core/base.html:76 templates/search/search.html:7
+#: templates/search/search.html:22
+msgid "Search"
+msgstr ""
+
+#: templates/core/base.html:148
+msgid "About AlekSIS — The Free School Information System"
+msgstr ""
+
+#: templates/core/base.html:156
 msgid "Impress"
 msgstr ""
 
-#: templates/core/base.html:154
+#: templates/core/base.html:164
 msgid "Privacy Policy"
 msgstr ""
 
-#: templates/core/base_print.html:60
+#: templates/core/base_print.html:62
 msgid "Powered by AlekSIS"
 msgstr ""
 
@@ -605,71 +847,132 @@ msgstr ""
 msgid "Edit person"
 msgstr ""
 
-#: templates/core/edit_school.html:8 templates/core/edit_school.html:9
-msgid "Edit school"
+#: templates/core/group_full.html:28 templates/core/person_full.html:28
+msgid "Change preferences"
 msgstr ""
 
-#: templates/core/group_full.html:19
-msgid "Owners"
+#: templates/core/groups.html:14
+msgid "Create group"
 msgstr ""
 
-#: templates/core/group_full.html:22
-msgid "Members"
+#: templates/core/groups_child_groups.html:18
+msgid ""
+"\n"
+"          You can use this to assign child groups to groups. Please use the "
+"filters below to select groups you want to\n"
+"          change and click \"Next\".\n"
+"        "
 msgstr ""
 
-#: templates/core/groups.html:14
-msgid "Create group"
+#: templates/core/groups_child_groups.html:31
+msgid "Update selection"
 msgstr ""
 
-#: templates/core/index.html:4
-msgid "Home"
+#: templates/core/groups_child_groups.html:35
+msgid "Clear all filters"
+msgstr ""
+
+#: templates/core/groups_child_groups.html:39
+msgid "Currently selected groups"
+msgstr ""
+
+#: templates/core/groups_child_groups.html:52
+msgid "Start assigning child groups for this groups"
+msgstr ""
+
+#: templates/core/groups_child_groups.html:61
+msgid ""
+"\n"
+"            Please select some groups in order to go on with assigning.\n"
+"          "
+msgstr ""
+
+#: templates/core/groups_child_groups.html:72
+msgid "Current group:"
+msgstr ""
+
+#: templates/core/groups_child_groups.html:78
+msgid "Please be careful!"
+msgstr ""
+
+#: templates/core/groups_child_groups.html:79
+msgid ""
+"\n"
+"            If you click \"Back\" or \"Next\" the current group assignments "
+"are not saved.\n"
+"            If you click \"Save\", you will overwrite all existing child "
+"group relations for this group with what you\n"
+"            selected on this page.\n"
+"          "
+msgstr ""
+
+#: templates/core/groups_child_groups.html:93
+#: templates/core/groups_child_groups.html:128
+#: templates/two_factor/_wizard_actions.html:15
+#: templates/two_factor/_wizard_actions.html:20
+msgid "Back"
 msgstr ""
 
-#: templates/core/index.html:11
-msgid "AlekSIS (School Information System)"
+#: templates/core/groups_child_groups.html:99
+#: templates/core/groups_child_groups.html:134
+#: templates/two_factor/_wizard_actions.html:26
+msgid "Next"
+msgstr ""
+
+#: templates/core/groups_child_groups.html:106
+#: templates/core/groups_child_groups.html:141
+#: templates/core/save_button.html:3
+msgid "Save"
+msgstr ""
+
+#: templates/core/groups_child_groups.html:112
+#: templates/core/groups_child_groups.html:147
+msgid "Save and next"
+msgstr ""
+
+#: templates/core/index.html:4
+msgid "Home"
 msgstr ""
 
-#: templates/core/index.html:43
+#: templates/core/index.html:42
 msgid "Last activities"
 msgstr ""
 
-#: templates/core/index.html:61
+#: templates/core/index.html:60
 msgid "No activities available yet."
 msgstr ""
 
-#: templates/core/index.html:66
+#: templates/core/index.html:65
 msgid "Recent notifications"
 msgstr ""
 
-#: templates/core/index.html:82
+#: templates/core/index.html:81
 msgid "More information →"
 msgstr ""
 
-#: templates/core/index.html:89
+#: templates/core/index.html:88
 msgid "No notifications available yet."
 msgstr ""
 
-#: templates/core/no_person.html:11
+#: templates/core/no_person.html:12
 msgid ""
 "\n"
-"          Your user account is not linked to a person. This means you\n"
-"          cannot access any school-related information. Please contact\n"
-"          the managers of AlekSIS at your school.\n"
-"        "
-msgstr ""
-
-#: templates/core/offline.html:6
-msgid "No internet connection."
+"            Your administrator account is not linked to any person. "
+"Therefore,\n"
+"            a dummy person has been linked to your account.\n"
+"          "
 msgstr ""
 
-#: templates/core/offline.html:9
+#: templates/core/no_person.html:19
 msgid ""
 "\n"
-"        There was an error accessing this page. You probably don't have an internet connection. Check to see if your WiFi or mobile data is turned on and try again. If you think you are connected, please contact the system administrators:\n"
-"      "
+"            Your user account is not linked to a person. This means you\n"
+"            cannot access any school-related information. Please contact\n"
+"            the managers of AlekSIS at your school.\n"
+"          "
 msgstr ""
 
-#: templates/core/person_full.html:19
+#: templates/core/person_full.html:34
 msgid "Contact details"
 msgstr ""
 
@@ -682,7 +985,8 @@ msgstr ""
 msgid ""
 "\n"
 "        You can use this form to assign user accounts to persons. Use the\n"
-"        dropdowns to select existing accounts; use the text fields to create new\n"
+"        dropdowns to select existing accounts; use the text fields to create "
+"new\n"
 "        accounts on-the-fly. The latter will create a new account with the\n"
 "        entered username and copy all other details from the person.\n"
 "      "
@@ -701,15 +1005,6 @@ msgstr ""
 msgid "New account"
 msgstr ""
 
-#: templates/core/save_button.html:3
-msgid "Save"
-msgstr ""
-
-#: templates/core/school_management.html:6
-#: templates/core/school_management.html:7
-msgid "School management"
-msgstr ""
-
 #: templates/core/system_status.html:12
 msgid "System checks"
 msgstr ""
@@ -721,7 +1016,8 @@ msgstr ""
 #: templates/core/system_status.html:23
 msgid ""
 "\n"
-"                Only admin and visitors from internal IPs can access thesite.\n"
+"                Only admin and visitors from internal IPs can access "
+"thesite.\n"
 "              "
 msgstr ""
 
@@ -740,7 +1036,8 @@ msgstr ""
 #: templates/core/system_status.html:47
 msgid ""
 "\n"
-"                The web server throws back debug information on errors. Do not use in production!\n"
+"                The web server throws back debug information on errors. Do "
+"not use in production!\n"
 "              "
 msgstr ""
 
@@ -751,105 +1048,97 @@ msgstr ""
 #: templates/core/system_status.html:56
 msgid ""
 "\n"
-"                Debug mode is disabled. Default error pages are displayed on errors.\n"
+"                Debug mode is disabled. Default error pages are displayed on "
+"errors.\n"
 "              "
 msgstr ""
 
-#: templates/impersonate/list_users.html:8
-msgid "Impersonate user"
-msgstr ""
-
-#: templates/martor/editor.html:27
-msgid "Uploading... please wait..."
-msgstr ""
-
-#: templates/martor/editor.html:36
-msgid "Nothing to preview"
-msgstr ""
-
-#: templates/martor/emoji.html:4
-msgid "Select Emoji to Insert"
+#: templates/dynamic_preferences/form.html:9
+msgid "Site preferences"
 msgstr ""
 
-#: templates/martor/emoji.html:8
-msgid "Preparing emojis..."
+#: templates/dynamic_preferences/form.html:11
+msgid "My preferences"
 msgstr ""
 
-#: templates/martor/guide.html:8
-msgid "Markdown Guide"
-msgstr ""
-
-#: templates/martor/guide.html:9
+#: templates/dynamic_preferences/form.html:13
 #, python-format
-msgid ""
-"This site is powered by Markdown. For full\n"
-"            documentation,\n"
-"            <a href=\"%(doc_url)s\" target=\"_blank\">click here</a>"
-msgstr ""
-
-#: templates/martor/guide.html:15 templates/martor/toolbar.html:42
-msgid "Code"
+msgid "Preferences for %(instance)s"
 msgstr ""
 
-#: templates/martor/guide.html:16
-msgid "Or"
+#: templates/dynamic_preferences/form.html:25
+msgid "Save preferences"
 msgstr ""
 
-#: templates/martor/guide.html:19
-msgid "... to Get"
+#: templates/dynamic_preferences/sections.html:7
+msgid "All"
 msgstr ""
 
-#: templates/martor/toolbar.html:3
-msgid "Bold"
+#: templates/impersonate/list_users.html:8
+msgid "Impersonate user"
 msgstr ""
 
-#: templates/martor/toolbar.html:6
-msgid "Italic"
+#: templates/offline.html:6
+msgid ""
+"No internet\n"
+"    connection."
 msgstr ""
 
-#: templates/martor/toolbar.html:10
-msgid "Horizontal Line"
+#: templates/offline.html:10
+msgid ""
+"\n"
+"      There was an error accessing this page. You probably don't have an "
+"internet connection. Check to see if your WiFi\n"
+"      or mobile data is turned on and try again. If you think you are "
+"connected, please contact the system\n"
+"      administrators:\n"
+"    "
 msgstr ""
 
-#: templates/martor/toolbar.html:15
-msgid "Heading"
+#: templates/search/search.html:8
+msgid "Global Search"
 msgstr ""
 
-#: templates/martor/toolbar.html:20 templates/martor/toolbar.html:23
-#: templates/martor/toolbar.html:26
-msgid "H"
+#: templates/search/search.html:15
+msgid "Search Term"
 msgstr ""
 
-#: templates/martor/toolbar.html:31
-msgid "Pre or Code"
+#: templates/search/search.html:26
+msgid "Results"
 msgstr ""
 
-#: templates/martor/toolbar.html:38
-msgid "Pre"
+#: templates/search/search.html:38
+msgid "No search results could be found to your search."
 msgstr ""
 
-#: templates/martor/toolbar.html:48
-msgid "Quote"
+#: templates/search/search.html:87
+msgid "Please enter a search term above."
 msgstr ""
 
-#: templates/martor/toolbar.html:52
-msgid "Unordered List"
+#: templates/templated_email/notification.email:3
+msgid "New notification for"
 msgstr ""
 
-#: templates/martor/toolbar.html:56
-msgid "Ordered List"
+#: templates/templated_email/notification.email:7
+msgid "Dear"
 msgstr ""
 
-#: templates/martor/toolbar.html:60
-msgid "URL/Link"
+#: templates/templated_email/notification.email:8
+msgid "we got a new notification for you:"
 msgstr ""
 
-#: templates/martor/toolbar.html:82
-msgid "Full Screen"
+#: templates/templated_email/notification.email:12
+msgid "More information"
 msgstr ""
 
-#: templates/martor/toolbar.html:86
-msgid "Markdown Guide (Help)"
+#: templates/templated_email/notification.email:16
+#, python-format
+msgid ""
+"\n"
+"    <p>By %(trans_sender)s at %(trans_created_at)s</p>\n"
+"\n"
+"    <i>Your AlekSIS team</i>\n"
+"    "
 msgstr ""
 
 #: templates/two_factor/_base_focus.html:6
@@ -863,15 +1152,6 @@ msgstr ""
 msgid "Cancel"
 msgstr ""
 
-#: templates/two_factor/_wizard_actions.html:15
-#: templates/two_factor/_wizard_actions.html:20
-msgid "Back"
-msgstr ""
-
-#: templates/two_factor/_wizard_actions.html:26
-msgid "Next"
-msgstr ""
-
 #: templates/two_factor/core/backup_tokens.html:5
 #: templates/two_factor/core/backup_tokens.html:9
 #: templates/two_factor/profile/profile.html:46
@@ -883,8 +1163,10 @@ msgid ""
 "\n"
 "        Backup tokens can be used when your primary and backup\n"
 "        phone numbers aren't available. The backup tokens below can be used\n"
-"        for login verification. If you've used up all your backup tokens, you\n"
-"        can generate a new set of backup tokens. Only the backup tokens shown\n"
+"        for login verification. If you've used up all your backup tokens, "
+"you\n"
+"        can generate a new set of backup tokens. Only the backup tokens "
+"shown\n"
 "        below will be valid.\n"
 "      "
 msgstr ""
@@ -908,44 +1190,50 @@ msgstr ""
 msgid "Generate Tokens"
 msgstr ""
 
-#: templates/two_factor/core/login.html:17
-msgid "Enter your credentials."
+#: templates/two_factor/core/login.html:16
+msgid ""
+"You have no permission to view this page. Please login with an other account."
+msgstr ""
+
+#: templates/two_factor/core/login.html:25
+msgid "Please login to see this page."
 msgstr ""
 
-#: templates/two_factor/core/login.html:20
+#: templates/two_factor/core/login.html:28
 msgid ""
 "We are calling your phone right now, please enter the\n"
-"            digits you hear."
+"              digits you hear."
 msgstr ""
 
-#: templates/two_factor/core/login.html:23
+#: templates/two_factor/core/login.html:31
 msgid ""
 "We sent you a text message, please enter the tokens we\n"
-"            sent."
+"              sent."
 msgstr ""
 
-#: templates/two_factor/core/login.html:26
+#: templates/two_factor/core/login.html:34
 msgid ""
 "Please enter the tokens generated by your token\n"
-"            generator."
+"              generator."
 msgstr ""
 
-#: templates/two_factor/core/login.html:30
+#: templates/two_factor/core/login.html:38
 msgid ""
 "Use this form for entering backup tokens for logging in.\n"
-"          These tokens have been generated for you to print and keep safe. Please\n"
-"          enter one of these backup tokens to login to your account."
+"            These tokens have been generated for you to print and keep safe. "
+"Please\n"
+"            enter one of these backup tokens to login to your account."
 msgstr ""
 
-#: templates/two_factor/core/login.html:47
+#: templates/two_factor/core/login.html:56
 msgid "Or, alternatively, use one of your backup phones:"
 msgstr ""
 
-#: templates/two_factor/core/login.html:57
+#: templates/two_factor/core/login.html:66
 msgid "As a last resort, you can use a backup token:"
 msgstr ""
 
-#: templates/two_factor/core/login.html:60
+#: templates/two_factor/core/login.html:69
 msgid "Use Backup Token"
 msgstr ""
 
@@ -956,7 +1244,8 @@ msgstr ""
 #: templates/two_factor/core/otp_required.html:10
 msgid ""
 "The page you requested, enforces users to verify using\n"
-"          two-factor authentication for security reasons. You need to enable these\n"
+"          two-factor authentication for security reasons. You need to enable "
+"these\n"
 "          security features in order to access this page."
 msgstr ""
 
@@ -1033,7 +1322,8 @@ msgstr ""
 #: templates/two_factor/core/setup.html:50
 msgid ""
 "\n"
-"            We are calling your phone right now, please enter the digits you hear.\n"
+"            We are calling your phone right now, please enter the digits you "
+"hear.\n"
 "          "
 msgstr ""
 
@@ -1047,9 +1337,12 @@ msgstr ""
 #: templates/two_factor/core/setup.html:63
 msgid ""
 "\n"
-"          We've encountered an issue with the selected authentication method. Please\n"
-"          go back and verify that you entered your information correctly, try\n"
-"          again, or use a different authentication method instead. If the issue\n"
+"          We've encountered an issue with the selected authentication "
+"method. Please\n"
+"          go back and verify that you entered your information correctly, "
+"try\n"
+"          again, or use a different authentication method instead. If the "
+"issue\n"
 "          persists, contact the site administrator.\n"
 "        "
 msgstr ""
@@ -1071,7 +1364,8 @@ msgstr ""
 #: templates/two_factor/core/setup_complete.html:14
 msgid ""
 "\n"
-"        Congratulations, you've successfully enabled two-factor authentication.\n"
+"        Congratulations, you've successfully enabled two-factor "
+"authentication.\n"
 "      "
 msgstr ""
 
@@ -1089,7 +1383,8 @@ msgstr ""
 msgid ""
 "\n"
 "          However, it might happen that you don't have access to\n"
-"          your primary token device. To enable account recovery, generate backup codes\n"
+"          your primary token device. To enable account recovery, generate "
+"backup codes\n"
 "          or add a phone number.\n"
 "        "
 msgstr ""
@@ -1107,7 +1402,9 @@ msgid "Disable Two-Factor Authentication"
 msgstr ""
 
 #: templates/two_factor/profile/disable.html:12
-msgid "You are about to disable two-factor authentication. This weakens your account security, are you sure?"
+msgid ""
+"You are about to disable two-factor authentication. This weakens your "
+"account security, are you sure?"
 msgstr ""
 
 #: templates/two_factor/profile/disable.html:26
@@ -1186,38 +1483,34 @@ msgid ""
 "      "
 msgstr ""
 
-#: util/notifications.py:66
+#: util/notifications.py:65
 msgid "E-Mail"
 msgstr ""
 
-#: util/notifications.py:67
+#: util/notifications.py:66
 msgid "SMS"
 msgstr ""
 
-#: views.py:172
-msgid "The person has been saved."
-msgstr ""
-
-#: views.py:195
-msgid "The group has been saved."
-msgstr ""
-
-#: views.py:236
-msgid "The school has been saved."
+#: views.py:212
+msgid "The child groups were successfully saved."
 msgstr ""
 
-#: views.py:255
-msgid "The term has been saved."
+#: views.py:240
+msgid "The person has been saved."
 msgstr ""
 
-#: views.py:272
-msgid "You are not allowed to mark notifications from other users as read!"
+#: views.py:276
+msgid "The group has been saved."
 msgstr ""
 
-#: views.py:307
+#: views.py:348
 msgid "The announcement has been saved."
 msgstr ""
 
-#: views.py:320
+#: views.py:364
 msgid "The announcement has been deleted."
 msgstr ""
+
+#: views.py:435
+msgid "The preferences have been saved successfully."
+msgstr ""
diff --git a/aleksis/core/locale/tr_TR/LC_MESSAGES/djangojs.po b/aleksis/core/locale/tr_TR/LC_MESSAGES/djangojs.po
index d62e52377a4ce6af7272b5568c5da2a4698fb057..21309c0096e0f1ec402dfa7c4e2f8604b96e2466 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: 2020-03-30 09:18+0000\n"
+"POT-Creation-Date: 2020-04-28 13:31+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"
diff --git a/aleksis/core/managers.py b/aleksis/core/managers.py
new file mode 100644
index 0000000000000000000000000000000000000000..a0871078015df8e117a09e23a593b384f7b25dc2
--- /dev/null
+++ b/aleksis/core/managers.py
@@ -0,0 +1,81 @@
+from datetime import date
+from typing import Union
+
+from django.contrib.sites.managers import CurrentSiteManager as _CurrentSiteManager
+from django.db.models import QuerySet
+
+from calendarweek import CalendarWeek
+
+
+class CurrentSiteManagerWithoutMigrations(_CurrentSiteManager):
+    """CurrentSiteManager for auto-generating managers just by query sets."""
+
+    use_in_migrations = False
+
+
+class DateRangeQuerySetMixin:
+    """QuerySet with custom query methods for models with date ranges.
+
+    Filterable fields: date_start, date_end
+    """
+
+    def within_dates(self, start: date, end: date):
+        """Filter for all objects within a date range."""
+        return self.filter(date_start__lte=end, date_end__gte=start)
+
+    def in_week(self, wanted_week: CalendarWeek):
+        """Filter for all objects within a calendar week."""
+        return self.within_dates(wanted_week[0], wanted_week[6])
+
+    def on_day(self, day: date):
+        """Filter for all objects on a certain day."""
+        return self.within_dates(day, day)
+
+
+class SchoolTermQuerySet(QuerySet, DateRangeQuerySetMixin):
+    """Custom query set for school terms."""
+
+
+class SchoolTermRelatedQuerySet(QuerySet):
+    """Custom query set for all models related to school terms."""
+
+    def within_dates(self, start: date, end: date) -> "SchoolTermRelatedQuerySet":
+        """Filter for all objects within a date range."""
+        return self.filter(school_term__date_start__lte=end, school_term__date_end__gte=start)
+
+    def in_week(self, wanted_week: CalendarWeek) -> "SchoolTermRelatedQuerySet":
+        """Filter for all objects within a calendar week."""
+        return self.within_dates(wanted_week[0], wanted_week[6])
+
+    def on_day(self, day: date) -> "SchoolTermRelatedQuerySet":
+        """Filter for all objects on a certain day."""
+        return self.within_dates(day, day)
+
+    def for_school_term(self, school_term: "SchoolTerm") -> "SchoolTermRelatedQuerySet":
+        return self.filter(school_term=school_term)
+
+    def for_current_school_term_or_all(self) -> "SchoolTermRelatedQuerySet":
+        """Get all objects related to current school term.
+
+        If there is no current school term, it will return all objects.
+        """
+        from aleksis.core.models import SchoolTerm
+
+        current_school_term = SchoolTerm.current
+        if current_school_term:
+            return self.for_school_term(current_school_term)
+        else:
+            return self
+
+    def for_current_school_term_or_none(self) -> Union["SchoolTermRelatedQuerySet", None]:
+        """Get all objects related to current school term.
+
+        If there is no current school term, it will return `None`.
+        """
+        from aleksis.core.models import SchoolTerm
+
+        current_school_term = SchoolTerm.current
+        if current_school_term:
+            return self.for_school_term(current_school_term)
+        else:
+            return None
diff --git a/aleksis/core/menus.py b/aleksis/core/menus.py
index f04baef7eb3b0f4b560aeaa919f29d8222751564..2e71c357384c1bc7e4ffa4ff1485f4edc4ffc7de 100644
--- a/aleksis/core/menus.py
+++ b/aleksis/core/menus.py
@@ -38,13 +38,10 @@ MENUS = {
                     "validators": ["menu_generator.validators.is_authenticated"],
                 },
                 {
-                    "name": _("Two factor auth"),
+                    "name": _("2FA"),
                     "url": "two_factor:profile",
                     "icon": "phonelink_lock",
-                    "validators": [
-                        "menu_generator.validators.is_authenticated",
-                        lambda request: "two_factor" in settings.INSTALLED_APPS,
-                    ],
+                    "validators": ["menu_generator.validators.is_authenticated",],
                 },
                 {
                     "name": _("Me"),
@@ -55,6 +52,15 @@ MENUS = {
                         "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",
+                    ],
+                },
             ],
         },
         {
@@ -62,8 +68,7 @@ MENUS = {
             "url": "#",
             "icon": "security",
             "validators": [
-                "menu_generator.validators.is_authenticated",
-                "menu_generator.validators.is_superuser",
+                ("aleksis.core.util.predicates.permission_validator", "core.view_admin_menu"),
             ],
             "submenu": [
                 {
@@ -71,8 +76,21 @@ MENUS = {
                     "url": "announcements",
                     "icon": "announcement",
                     "validators": [
-                        "menu_generator.validators.is_authenticated",
-                        "menu_generator.validators.is_superuser",
+                        (
+                            "aleksis.core.util.predicates.permission_validator",
+                            "core.view_announcements",
+                        ),
+                    ],
+                },
+                {
+                    "name": _("School terms"),
+                    "url": "school_terms",
+                    "icon": "date_range",
+                    "validators": [
+                        (
+                            "aleksis.core.util.predicates.permission_validator",
+                            "core.view_schoolterm",
+                        ),
                     ],
                 },
                 {
@@ -80,8 +98,7 @@ MENUS = {
                     "url": "data_management",
                     "icon": "view_list",
                     "validators": [
-                        "menu_generator.validators.is_authenticated",
-                        "menu_generator.validators.is_superuser",
+                        ("aleksis.core.util.predicates.permission_validator", "core.manage_data"),
                     ],
                 },
                 {
@@ -89,8 +106,10 @@ MENUS = {
                     "url": "system_status",
                     "icon": "power_settings_new",
                     "validators": [
-                        "menu_generator.validators.is_authenticated",
-                        "menu_generator.validators.is_superuser",
+                        (
+                            "aleksis.core.util.predicates.permission_validator",
+                            "core.view_system_status",
+                        ),
                     ],
                 },
                 {
@@ -98,27 +117,25 @@ MENUS = {
                     "url": "impersonate-list",
                     "icon": "people",
                     "validators": [
-                        "menu_generator.validators.is_authenticated",
-                        "menu_generator.validators.is_superuser",
+                        ("aleksis.core.util.predicates.permission_validator", "core.impersonate"),
                     ],
                 },
                 {
-                    "name": _("Manage school"),
-                    "url": "school_management",
-                    "icon": "school",
+                    "name": _("Configuration"),
+                    "url": "preferences_site",
+                    "icon": "settings",
                     "validators": [
-                        "menu_generator.validators.is_authenticated",
-                        "menu_generator.validators.is_superuser",
+                        (
+                            "aleksis.core.util.predicates.permission_validator",
+                            "core.change_site_preferences",
+                        ),
                     ],
                 },
                 {
                     "name": _("Backend Admin"),
                     "url": "admin:index",
                     "icon": "settings",
-                    "validators": [
-                        "menu_generator.validators.is_authenticated",
-                        "menu_generator.validators.is_superuser",
-                    ],
+                    "validators": ["menu_generator.validators.is_superuser",],
                 },
             ],
         },
@@ -128,37 +145,82 @@ MENUS = {
             "icon": "people",
             "root": True,
             "validators": [
-                "menu_generator.validators.is_authenticated",
-                "aleksis.core.util.core_helpers.has_person",
+                ("aleksis.core.util.predicates.permission_validator", "core.view_people_menu")
             ],
             "submenu": [
                 {
                     "name": _("Persons"),
                     "url": "persons",
                     "icon": "person",
-                    "validators": ["menu_generator.validators.is_authenticated"],
+                    "validators": [
+                        ("aleksis.core.util.predicates.permission_validator", "core.view_persons")
+                    ],
                 },
                 {
                     "name": _("Groups"),
                     "url": "groups",
                     "icon": "group",
-                    "validators": ["menu_generator.validators.is_authenticated"],
+                    "validators": [
+                        ("aleksis.core.util.predicates.permission_validator", "core.view_groups")
+                    ],
+                },
+                {
+                    "name": _("Group types"),
+                    "url": "group_types",
+                    "icon": "category",
+                    "validators": [
+                        (
+                            "aleksis.core.util.predicates.permission_validator",
+                            "core.view_group_type",
+                        )
+                    ],
                 },
                 {
                     "name": _("Persons and accounts"),
                     "url": "persons_accounts",
                     "icon": "person_add",
                     "validators": [
-                        "menu_generator.validators.is_authenticated",
-                        "menu_generator.validators.is_superuser",
+                        (
+                            "aleksis.core.util.predicates.permission_validator",
+                            "core.link_persons_accounts",
+                        )
+                    ],
+                },
+                {
+                    "name": _("Groups and child groups"),
+                    "url": "groups_child_groups",
+                    "icon": "group_add",
+                    "validators": [
+                        (
+                            "aleksis.core.util.predicates.permission_validator",
+                            "core.assign_child_groups_to_groups",
+                        )
+                    ],
+                },
+                {
+                    "name": _("Additional fields"),
+                    "url": "additional_fields",
+                    "icon": "style",
+                    "validators": [
+                        (
+                            "aleksis.core.util.predicates.permission_validator",
+                            "core.view_additionalfield",
+                        )
                     ],
                 },
             ],
         },
     ],
-    "DATA_MANAGEMENT_MENU": [],
-    "SCHOOL_MANAGEMENT_MENU": [
-        {"name": _("Edit school information"), "url": "edit_school_information", },
-        {"name": _("Edit school term"), "url": "edit_school_term", },
+    "DATA_MANAGEMENT_MENU": [
+        {
+            "name": _("Assign child groups to groups"),
+            "url": "groups_child_groups",
+            "validators": [
+                (
+                    "aleksis.core.util.predicates.permission_validator",
+                    "core.assign_child_groups_to_groups",
+                )
+            ],
+        },
     ],
 }
diff --git a/aleksis/core/migrations/0001_initial.py b/aleksis/core/migrations/0001_initial.py
index 15eb9d297b01a571c60120dd9ce64fb8bbeca446..4dd6486f478a00e23a42876d302bbbe954648084 100644
--- a/aleksis/core/migrations/0001_initial.py
+++ b/aleksis/core/migrations/0001_initial.py
@@ -1,8 +1,11 @@
-# Generated by Django 3.0.2 on 2020-01-03 19:18
+# Generated by Django 3.0.5 on 2020-05-04 14:16
 
 import aleksis.core.mixins
+import aleksis.core.util.core_helpers
+import datetime
 from django.conf import settings
 import django.contrib.postgres.fields.jsonb
+import django.contrib.sites.managers
 from django.db import migrations, models
 import django.db.models.deletion
 import image_cropping.fields
@@ -10,116 +13,330 @@ import phonenumber_field.modelfields
 
 
 class Migration(migrations.Migration):
+
     initial = True
 
     dependencies = [
         migrations.swappable_dependency(settings.AUTH_USER_MODEL),
-        ('otp_yubikey', '0001_initial'),
+        ('contenttypes', '0002_remove_content_type_name'),
+        ('sites', '0002_alter_domain_unique'),
     ]
 
     operations = [
         migrations.CreateModel(
-            name='Group',
+            name='GlobalPermissions',
             fields=[
                 ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
-                ('name', models.CharField(max_length=60, unique=True, verbose_name='Long name of group')),
-                ('short_name', models.CharField(max_length=16, unique=True, verbose_name='Short name of group')),
                 ('extended_data', django.contrib.postgres.fields.jsonb.JSONField(default=dict, editable=False)),
             ],
             options={
-                'ordering': ['short_name', 'name'],
+                'permissions': (('view_system_status', 'Can view system status'), ('link_persons_accounts', 'Can link persons to accounts'), ('manage_data', 'Can manage data'), ('impersonate', 'Can impersonate'), ('search', 'Can use search'), ('change_site_preferences', 'Can change site preferences'), ('change_person_preferences', 'Can change person preferences'), ('change_group_preferences', 'Can change group preferences')),
+                'managed': False,
+            },
+            managers=[
+                ('objects', django.contrib.sites.managers.CurrentSiteManager()),
+            ],
+        ),
+        migrations.CreateModel(
+            name='AdditionalField',
+            fields=[
+                ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+                ('extended_data', django.contrib.postgres.fields.jsonb.JSONField(default=dict, editable=False)),
+                ('title', models.CharField(max_length=255, verbose_name='Title of field')),
+                ('field_type', models.CharField(choices=[('BooleanField', 'Boolean (Yes/No)'), ('CharField', 'Text (one line)'), ('DateField', 'Date'), ('DateTimeField', 'Date and time'), ('DecimalField', 'Decimal number'), ('EmailField', 'E-mail address'), ('IntegerField', 'Integer'), ('GenericIPAddressField', 'IP address'), ('NullBooleanField', 'Boolean or empty (Yes/No/Neither)'), ('TextField', 'Text (multi-line)'), ('TimeField', 'Time'), ('URLField', 'URL / Link')], max_length=50, verbose_name='Type of field')),
+                ('site', models.ForeignKey(default=1, editable=False, on_delete=django.db.models.deletion.CASCADE, to='sites.Site')),
+            ],
+            options={
+                'verbose_name': 'Addtitional field for groups',
+                'verbose_name_plural': 'Addtitional fields for groups',
+            },
+            managers=[
+                ('objects', django.contrib.sites.managers.CurrentSiteManager()),
+            ],
+        ),
+        migrations.CreateModel(
+            name='Announcement',
+            fields=[
+                ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+                ('extended_data', django.contrib.postgres.fields.jsonb.JSONField(default=dict, editable=False)),
+                ('title', models.CharField(max_length=150, verbose_name='Title')),
+                ('description', models.TextField(blank=True, max_length=500, verbose_name='Description')),
+                ('link', models.URLField(blank=True, verbose_name='Link to detailed view')),
+                ('valid_from', models.DateTimeField(default=datetime.datetime.now, verbose_name='Date and time from when to show')),
+                ('valid_until', models.DateTimeField(default=aleksis.core.util.core_helpers.now_tomorrow, verbose_name='Date and time until when to show')),
+                ('site', models.ForeignKey(default=1, editable=False, on_delete=django.db.models.deletion.CASCADE, to='sites.Site')),
+            ],
+            options={
+                'verbose_name': 'Announcement',
+                'verbose_name_plural': 'Announcements',
             },
-            bases=(aleksis.core.mixins.ExtensibleModel,),
         ),
         migrations.CreateModel(
-            name='School',
+            name='CustomMenu',
             fields=[
                 ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
-                ('name', models.CharField(max_length=30, verbose_name='Name')),
-                ('name_official',
-                 models.CharField(help_text='Official name of the school, e.g. as given by supervisory authority',
-                                  max_length=200, verbose_name='Official name')),
-                ('logo',
-                 image_cropping.fields.ImageCropField(blank=True, null=True, upload_to='', verbose_name='School logo')),
-                ('logo_cropping',
-                 image_cropping.fields.ImageRatioField('logo', '600x600', adapt_rotation=False, allow_fullsize=False,
-                                                       free_crop=False, help_text=None, hide_image_field=False,
-                                                       size_warning=True, verbose_name='logo cropping')),
                 ('extended_data', django.contrib.postgres.fields.jsonb.JSONField(default=dict, editable=False)),
+                ('name', models.CharField(max_length=100, unique=True, verbose_name='Menu ID')),
+                ('site', models.ForeignKey(default=1, editable=False, on_delete=django.db.models.deletion.CASCADE, to='sites.Site')),
             ],
             options={
-                'ordering': ['name', 'name_official'],
+                'verbose_name': 'Custom menu',
+                'verbose_name_plural': 'Custom menus',
             },
+            managers=[
+                ('objects', django.contrib.sites.managers.CurrentSiteManager()),
+            ],
         ),
         migrations.CreateModel(
-            name='SchoolTerm',
+            name='Group',
             fields=[
                 ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
-                ('caption', models.CharField(max_length=30, verbose_name='Visible caption of the term')),
-                ('date_start', models.DateField(null=True, verbose_name='Effective start date of term')),
-                ('date_end', models.DateField(null=True, verbose_name='Effective end date of term')),
-                ('current', models.NullBooleanField(default=None, unique=True)),
                 ('extended_data', django.contrib.postgres.fields.jsonb.JSONField(default=dict, editable=False)),
+                ('name', models.CharField(max_length=255, unique=True, verbose_name='Long name')),
+                ('short_name', models.CharField(blank=True, max_length=255, null=True, unique=True, verbose_name='Short name')),
+                ('additional_fields', models.ManyToManyField(to='core.AdditionalField', verbose_name='Additional fields')),
+            ],
+            options={
+                'verbose_name': 'Group',
+                'verbose_name_plural': 'Groups',
+                'ordering': ['short_name', 'name'],
+                'permissions': (('assign_child_groups_to_groups', 'Can assign child groups to groups'),),
+            },
+            managers=[
+                ('objects', django.contrib.sites.managers.CurrentSiteManager()),
             ],
         ),
         migrations.CreateModel(
             name='Person',
             fields=[
                 ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+                ('extended_data', django.contrib.postgres.fields.jsonb.JSONField(default=dict, editable=False)),
                 ('is_active', models.BooleanField(default=True, verbose_name='Is person active?')),
-                ('first_name', models.CharField(max_length=30, verbose_name='First name')),
-                ('last_name', models.CharField(max_length=30, verbose_name='Last name')),
-                ('additional_name', models.CharField(blank=True, max_length=30, verbose_name='Additional name(s)')),
-                ('short_name',
-                 models.CharField(blank=True, max_length=5, null=True, unique=True, verbose_name='Short name')),
-                ('street', models.CharField(blank=True, max_length=30, verbose_name='Street')),
-                ('housenumber', models.CharField(blank=True, max_length=10, verbose_name='Street number')),
-                ('postal_code', models.CharField(blank=True, max_length=5, verbose_name='Postal code')),
-                ('place', models.CharField(blank=True, max_length=30, verbose_name='Place')),
-                ('phone_number', phonenumber_field.modelfields.PhoneNumberField(blank=True, max_length=128, region=None,
-                                                                                verbose_name='Home phone')),
-                ('mobile_number',
-                 phonenumber_field.modelfields.PhoneNumberField(blank=True, max_length=128, region=None,
-                                                                verbose_name='Mobile phone')),
+                ('first_name', models.CharField(max_length=255, verbose_name='First name')),
+                ('last_name', models.CharField(max_length=255, verbose_name='Last name')),
+                ('additional_name', models.CharField(blank=True, max_length=255, verbose_name='Additional name(s)')),
+                ('short_name', models.CharField(blank=True, max_length=255, null=True, unique=True, verbose_name='Short name')),
+                ('street', models.CharField(blank=True, max_length=255, verbose_name='Street')),
+                ('housenumber', models.CharField(blank=True, max_length=255, verbose_name='Street number')),
+                ('postal_code', models.CharField(blank=True, max_length=255, verbose_name='Postal code')),
+                ('place', models.CharField(blank=True, max_length=255, verbose_name='Place')),
+                ('phone_number', phonenumber_field.modelfields.PhoneNumberField(blank=True, max_length=128, region=None, verbose_name='Home phone')),
+                ('mobile_number', phonenumber_field.modelfields.PhoneNumberField(blank=True, max_length=128, region=None, verbose_name='Mobile phone')),
                 ('email', models.EmailField(blank=True, max_length=254, verbose_name='E-mail address')),
                 ('date_of_birth', models.DateField(blank=True, null=True, verbose_name='Date of birth')),
-                ('sex', models.CharField(blank=True, choices=[('f', 'female'), ('m', 'male')], max_length=1,
-                                         verbose_name='Sex')),
-                ('photo',
-                 image_cropping.fields.ImageCropField(blank=True, null=True, upload_to='', verbose_name='Photo')),
-                ('photo_cropping',
-                 image_cropping.fields.ImageRatioField('photo', '600x800', adapt_rotation=False, allow_fullsize=False,
-                                                       free_crop=False, help_text=None, hide_image_field=False,
-                                                       size_warning=True, verbose_name='photo cropping')),
-                ('import_ref', models.CharField(blank=True, editable=False, max_length=64, null=True, unique=True,
-                                                verbose_name='Reference ID of import source')),
-                ('guardians', models.ManyToManyField(blank=True, related_name='children', to='core.Person',
-                                                     verbose_name='Guardians / Parents')),
-                ('primary_group',
-                 models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to='core.Group')),
-                ('user', models.OneToOneField(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL,
-                                              related_name='person', to=settings.AUTH_USER_MODEL)),
-                ('extended_data', django.contrib.postgres.fields.jsonb.JSONField(default=dict, editable=False)),
+                ('sex', models.CharField(blank=True, choices=[('f', 'female'), ('m', 'male')], max_length=1, verbose_name='Sex')),
+                ('photo', image_cropping.fields.ImageCropField(blank=True, null=True, upload_to='', verbose_name='Photo')),
+                ('photo_cropping', image_cropping.fields.ImageRatioField('photo', '600x800', adapt_rotation=False, allow_fullsize=False, free_crop=False, help_text=None, hide_image_field=False, size_warning=True, verbose_name='photo cropping')),
+                ('description', models.TextField(blank=True, verbose_name='Description')),
+                ('guardians', models.ManyToManyField(blank=True, related_name='children', to='core.Person', verbose_name='Guardians / Parents')),
+                ('primary_group', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='core.Group', verbose_name='Primary group')),
+                ('site', models.ForeignKey(default=1, editable=False, on_delete=django.db.models.deletion.CASCADE, to='sites.Site')),
+                ('user', models.OneToOneField(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='person', to=settings.AUTH_USER_MODEL, verbose_name='Linked user')),
             ],
             options={
+                'verbose_name': 'Person',
+                'verbose_name_plural': 'Persons',
                 'ordering': ['last_name', 'first_name'],
+                'permissions': (('view_address', 'Can view address'), ('view_contact_details', 'Can view contact details'), ('view_photo', 'Can view photo'), ('view_person_groups', 'Can view persons groups'), ('view_personal_details', 'Can view personal details')),
+            },
+            managers=[
+                ('objects', django.contrib.sites.managers.CurrentSiteManager()),
+            ],
+        ),
+        migrations.CreateModel(
+            name='DummyPerson',
+            fields=[
+            ],
+            options={
+                'managed': False,
+                'proxy': True,
+            },
+            bases=('core.person',),
+            managers=[
+                ('objects', django.contrib.sites.managers.CurrentSiteManager()),
+            ],
+        ),
+        migrations.CreateModel(
+            name='SitePreferenceModel',
+            fields=[
+                ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+                ('section', models.CharField(blank=True, db_index=True, default=None, max_length=150, null=True, verbose_name='Section Name')),
+                ('name', models.CharField(db_index=True, max_length=150, verbose_name='Name')),
+                ('raw_value', models.TextField(blank=True, null=True, verbose_name='Raw Value')),
+                ('instance', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='sites.Site')),
+            ],
+            bases=(models.Model, aleksis.core.mixins.PureDjangoModel),
+        ),
+        migrations.CreateModel(
+            name='PersonPreferenceModel',
+            fields=[
+                ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+                ('section', models.CharField(blank=True, db_index=True, default=None, max_length=150, null=True, verbose_name='Section Name')),
+                ('name', models.CharField(db_index=True, max_length=150, verbose_name='Name')),
+                ('raw_value', models.TextField(blank=True, null=True, verbose_name='Raw Value')),
+                ('instance', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='core.Person')),
+            ],
+            bases=(models.Model, aleksis.core.mixins.PureDjangoModel),
+        ),
+        migrations.CreateModel(
+            name='PersonGroupThrough',
+            fields=[
+                ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+                ('extended_data', django.contrib.postgres.fields.jsonb.JSONField(default=dict, editable=False)),
+                ('group', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='core.Group')),
+                ('person', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='core.Person')),
+                ('site', models.ForeignKey(default=1, editable=False, on_delete=django.db.models.deletion.CASCADE, to='sites.Site')),
+            ],
+            options={
+                'abstract': False,
             },
-            bases=(aleksis.core.mixins.ExtensibleModel,),
+            managers=[
+                ('objects', django.contrib.sites.managers.CurrentSiteManager()),
+            ],
+        ),
+        migrations.CreateModel(
+            name='Notification',
+            fields=[
+                ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+                ('extended_data', django.contrib.postgres.fields.jsonb.JSONField(default=dict, editable=False)),
+                ('sender', models.CharField(max_length=100, verbose_name='Sender')),
+                ('title', models.CharField(max_length=150, verbose_name='Title')),
+                ('description', models.TextField(max_length=500, verbose_name='Description')),
+                ('link', models.URLField(blank=True, verbose_name='Link')),
+                ('read', models.BooleanField(default=False, verbose_name='Read')),
+                ('sent', models.BooleanField(default=False, verbose_name='Sent')),
+                ('recipient', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='notifications', to='core.Person', verbose_name='Recipient')),
+                ('site', models.ForeignKey(default=1, editable=False, on_delete=django.db.models.deletion.CASCADE, to='sites.Site')),
+            ],
+            options={
+                'verbose_name': 'Notification',
+                'verbose_name_plural': 'Notifications',
+            },
+            managers=[
+                ('objects', django.contrib.sites.managers.CurrentSiteManager()),
+            ],
+        ),
+        migrations.CreateModel(
+            name='GroupType',
+            fields=[
+                ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+                ('extended_data', django.contrib.postgres.fields.jsonb.JSONField(default=dict, editable=False)),
+                ('name', models.CharField(max_length=50, verbose_name='Title of type')),
+                ('description', models.CharField(max_length=500, verbose_name='Description')),
+                ('site', models.ForeignKey(default=1, editable=False, on_delete=django.db.models.deletion.CASCADE, to='sites.Site')),
+            ],
+            options={
+                'verbose_name': 'Group type',
+                'verbose_name_plural': 'Group types',
+            },
+            managers=[
+                ('objects', django.contrib.sites.managers.CurrentSiteManager()),
+            ],
+        ),
+        migrations.CreateModel(
+            name='GroupPreferenceModel',
+            fields=[
+                ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+                ('section', models.CharField(blank=True, db_index=True, default=None, max_length=150, null=True, verbose_name='Section Name')),
+                ('name', models.CharField(db_index=True, max_length=150, verbose_name='Name')),
+                ('raw_value', models.TextField(blank=True, null=True, verbose_name='Raw Value')),
+                ('instance', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='core.Group')),
+            ],
+            bases=(models.Model, aleksis.core.mixins.PureDjangoModel),
+        ),
+        migrations.AddField(
+            model_name='group',
+            name='group_type',
+            field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='type', to='core.GroupType', verbose_name='Type of group'),
         ),
         migrations.AddField(
             model_name='group',
             name='members',
-            field=models.ManyToManyField(blank=True, related_name='member_of', to='core.Person'),
+            field=models.ManyToManyField(blank=True, related_name='member_of', through='core.PersonGroupThrough', to='core.Person', verbose_name='Members'),
         ),
         migrations.AddField(
             model_name='group',
             name='owners',
-            field=models.ManyToManyField(blank=True, related_name='owner_of', to='core.Person'),
+            field=models.ManyToManyField(blank=True, related_name='owner_of', to='core.Person', verbose_name='Owners'),
         ),
         migrations.AddField(
             model_name='group',
             name='parent_groups',
-            field=models.ManyToManyField(blank=True, related_name='child_groups', to='core.Group',
-                                         verbose_name='Parent groups'),
+            field=models.ManyToManyField(blank=True, related_name='child_groups', to='core.Group', verbose_name='Parent groups'),
+        ),
+        migrations.AddField(
+            model_name='group',
+            name='site',
+            field=models.ForeignKey(default=1, editable=False, on_delete=django.db.models.deletion.CASCADE, to='sites.Site'),
+        ),
+        migrations.CreateModel(
+            name='DashboardWidget',
+            fields=[
+                ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+                ('title', models.CharField(max_length=150, verbose_name='Widget Title')),
+                ('active', models.BooleanField(verbose_name='Activate Widget')),
+                ('polymorphic_ctype', models.ForeignKey(editable=False, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='polymorphic_core.dashboardwidget_set+', to='contenttypes.ContentType')),
+            ],
+            options={
+                'verbose_name': 'Dashboard Widget',
+                'verbose_name_plural': 'Dashboard Widgets',
+            },
+            bases=(models.Model, aleksis.core.mixins.PureDjangoModel),
+        ),
+        migrations.CreateModel(
+            name='CustomMenuItem',
+            fields=[
+                ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+                ('extended_data', django.contrib.postgres.fields.jsonb.JSONField(default=dict, editable=False)),
+                ('name', models.CharField(max_length=150, verbose_name='Name')),
+                ('url', models.URLField(verbose_name='Link')),
+                ('icon', models.CharField(blank=True, choices=[('3d_rotation', '3d_rotation'), ('ac_unit', 'ac_unit'), ('access_alarm', 'access_alarm'), ('access_alarms', 'access_alarms'), ('access_time', 'access_time'), ('accessibility', 'accessibility'), ('accessible', 'accessible'), ('account_balance', 'account_balance'), ('account_balance_wallet', 'account_balance_wallet'), ('account_box', 'account_box'), ('account_circle', 'account_circle'), ('adb', 'adb'), ('add', 'add'), ('add_a_photo', 'add_a_photo'), ('add_alarm', 'add_alarm'), ('add_alert', 'add_alert'), ('add_box', 'add_box'), ('add_circle', 'add_circle'), ('add_circle_outline', 'add_circle_outline'), ('add_location', 'add_location'), ('add_shopping_cart', 'add_shopping_cart'), ('add_to_photos', 'add_to_photos'), ('add_to_queue', 'add_to_queue'), ('adjust', 'adjust'), ('airline_seat_flat', 'airline_seat_flat'), ('airline_seat_flat_angled', 'airline_seat_flat_angled'), ('airline_seat_individual_suite', 'airline_seat_individual_suite'), ('airline_seat_legroom_extra', 'airline_seat_legroom_extra'), ('airline_seat_legroom_normal', 'airline_seat_legroom_normal'), ('airline_seat_legroom_reduced', 'airline_seat_legroom_reduced'), ('airline_seat_recline_extra', 'airline_seat_recline_extra'), ('airline_seat_recline_normal', 'airline_seat_recline_normal'), ('airplanemode_active', 'airplanemode_active'), ('airplanemode_inactive', 'airplanemode_inactive'), ('airplay', 'airplay'), ('airport_shuttle', 'airport_shuttle'), ('alarm', 'alarm'), ('alarm_add', 'alarm_add'), ('alarm_off', 'alarm_off'), ('alarm_on', 'alarm_on'), ('album', 'album'), ('all_inclusive', 'all_inclusive'), ('all_out', 'all_out'), ('android', 'android'), ('announcement', 'announcement'), ('apps', 'apps'), ('archive', 'archive'), ('arrow_back', 'arrow_back'), ('arrow_downward', 'arrow_downward'), ('arrow_drop_down', 'arrow_drop_down'), ('arrow_drop_down_circle', 'arrow_drop_down_circle'), ('arrow_drop_up', 'arrow_drop_up'), ('arrow_forward', 'arrow_forward'), ('arrow_upward', 'arrow_upward'), ('art_track', 'art_track'), ('aspect_ratio', 'aspect_ratio'), ('assessment', 'assessment'), ('assignment', 'assignment'), ('assignment_ind', 'assignment_ind'), ('assignment_late', 'assignment_late'), ('assignment_return', 'assignment_return'), ('assignment_returned', 'assignment_returned'), ('assignment_turned_in', 'assignment_turned_in'), ('assistant', 'assistant'), ('assistant_photo', 'assistant_photo'), ('attach_file', 'attach_file'), ('attach_money', 'attach_money'), ('attachment', 'attachment'), ('audiotrack', 'audiotrack'), ('autorenew', 'autorenew'), ('av_timer', 'av_timer'), ('backspace', 'backspace'), ('backup', 'backup'), ('battery_alert', 'battery_alert'), ('battery_charging_full', 'battery_charging_full'), ('battery_full', 'battery_full'), ('battery_std', 'battery_std'), ('battery_unknown', 'battery_unknown'), ('beach_access', 'beach_access'), ('beenhere', 'beenhere'), ('block', 'block'), ('bluetooth', 'bluetooth'), ('bluetooth_audio', 'bluetooth_audio'), ('bluetooth_connected', 'bluetooth_connected'), ('bluetooth_disabled', 'bluetooth_disabled'), ('bluetooth_searching', 'bluetooth_searching'), ('blur_circular', 'blur_circular'), ('blur_linear', 'blur_linear'), ('blur_off', 'blur_off'), ('blur_on', 'blur_on'), ('book', 'book'), ('bookmark', 'bookmark'), ('bookmark_border', 'bookmark_border'), ('border_all', 'border_all'), ('border_bottom', 'border_bottom'), ('border_clear', 'border_clear'), ('border_color', 'border_color'), ('border_horizontal', 'border_horizontal'), ('border_inner', 'border_inner'), ('border_left', 'border_left'), ('border_outer', 'border_outer'), ('border_right', 'border_right'), ('border_style', 'border_style'), ('border_top', 'border_top'), ('border_vertical', 'border_vertical'), ('branding_watermark', 'branding_watermark'), ('brightness_1', 'brightness_1'), ('brightness_2', 'brightness_2'), ('brightness_3', 'brightness_3'), ('brightness_4', 'brightness_4'), ('brightness_5', 'brightness_5'), ('brightness_6', 'brightness_6'), ('brightness_7', 'brightness_7'), ('brightness_auto', 'brightness_auto'), ('brightness_high', 'brightness_high'), ('brightness_low', 'brightness_low'), ('brightness_medium', 'brightness_medium'), ('broken_image', 'broken_image'), ('brush', 'brush'), ('bubble_chart', 'bubble_chart'), ('bug_report', 'bug_report'), ('build', 'build'), ('burst_mode', 'burst_mode'), ('business', 'business'), ('business_center', 'business_center'), ('cached', 'cached'), ('cake', 'cake'), ('call', 'call'), ('call_end', 'call_end'), ('call_made', 'call_made'), ('call_merge', 'call_merge'), ('call_missed', 'call_missed'), ('call_missed_outgoing', 'call_missed_outgoing'), ('call_received', 'call_received'), ('call_split', 'call_split'), ('call_to_action', 'call_to_action'), ('camera', 'camera'), ('camera_alt', 'camera_alt'), ('camera_enhance', 'camera_enhance'), ('camera_front', 'camera_front'), ('camera_rear', 'camera_rear'), ('camera_roll', 'camera_roll'), ('cancel', 'cancel'), ('card_giftcard', 'card_giftcard'), ('card_membership', 'card_membership'), ('card_travel', 'card_travel'), ('casino', 'casino'), ('cast', 'cast'), ('cast_connected', 'cast_connected'), ('center_focus_strong', 'center_focus_strong'), ('center_focus_weak', 'center_focus_weak'), ('change_history', 'change_history'), ('chat', 'chat'), ('chat_bubble', 'chat_bubble'), ('chat_bubble_outline', 'chat_bubble_outline'), ('check', 'check'), ('check_box', 'check_box'), ('check_box_outline_blank', 'check_box_outline_blank'), ('check_circle', 'check_circle'), ('chevron_left', 'chevron_left'), ('chevron_right', 'chevron_right'), ('child_care', 'child_care'), ('child_friendly', 'child_friendly'), ('chrome_reader_mode', 'chrome_reader_mode'), ('class', 'class'), ('clear', 'clear'), ('clear_all', 'clear_all'), ('close', 'close'), ('closed_caption', 'closed_caption'), ('cloud', 'cloud'), ('cloud_circle', 'cloud_circle'), ('cloud_done', 'cloud_done'), ('cloud_download', 'cloud_download'), ('cloud_off', 'cloud_off'), ('cloud_queue', 'cloud_queue'), ('cloud_upload', 'cloud_upload'), ('code', 'code'), ('collections', 'collections'), ('collections_bookmark', 'collections_bookmark'), ('color_lens', 'color_lens'), ('colorize', 'colorize'), ('comment', 'comment'), ('compare', 'compare'), ('compare_arrows', 'compare_arrows'), ('computer', 'computer'), ('confirmation_number', 'confirmation_number'), ('contact_mail', 'contact_mail'), ('contact_phone', 'contact_phone'), ('contacts', 'contacts'), ('content_copy', 'content_copy'), ('content_cut', 'content_cut'), ('content_paste', 'content_paste'), ('control_point', 'control_point'), ('control_point_duplicate', 'control_point_duplicate'), ('copyright', 'copyright'), ('create', 'create'), ('create_new_folder', 'create_new_folder'), ('credit_card', 'credit_card'), ('crop', 'crop'), ('crop_16_9', 'crop_16_9'), ('crop_3_2', 'crop_3_2'), ('crop_5_4', 'crop_5_4'), ('crop_7_5', 'crop_7_5'), ('crop_din', 'crop_din'), ('crop_free', 'crop_free'), ('crop_landscape', 'crop_landscape'), ('crop_original', 'crop_original'), ('crop_portrait', 'crop_portrait'), ('crop_rotate', 'crop_rotate'), ('crop_square', 'crop_square'), ('dashboard', 'dashboard'), ('data_usage', 'data_usage'), ('date_range', 'date_range'), ('dehaze', 'dehaze'), ('delete', 'delete'), ('delete_forever', 'delete_forever'), ('delete_sweep', 'delete_sweep'), ('description', 'description'), ('desktop_mac', 'desktop_mac'), ('desktop_windows', 'desktop_windows'), ('details', 'details'), ('developer_board', 'developer_board'), ('developer_mode', 'developer_mode'), ('device_hub', 'device_hub'), ('devices', 'devices'), ('devices_other', 'devices_other'), ('dialer_sip', 'dialer_sip'), ('dialpad', 'dialpad'), ('directions', 'directions'), ('directions_bike', 'directions_bike'), ('directions_boat', 'directions_boat'), ('directions_bus', 'directions_bus'), ('directions_car', 'directions_car'), ('directions_railway', 'directions_railway'), ('directions_run', 'directions_run'), ('directions_subway', 'directions_subway'), ('directions_transit', 'directions_transit'), ('directions_walk', 'directions_walk'), ('disc_full', 'disc_full'), ('dns', 'dns'), ('do_not_disturb', 'do_not_disturb'), ('do_not_disturb_alt', 'do_not_disturb_alt'), ('do_not_disturb_off', 'do_not_disturb_off'), ('do_not_disturb_on', 'do_not_disturb_on'), ('dock', 'dock'), ('domain', 'domain'), ('done', 'done'), ('done_all', 'done_all'), ('donut_large', 'donut_large'), ('donut_small', 'donut_small'), ('drafts', 'drafts'), ('drag_handle', 'drag_handle'), ('drive_eta', 'drive_eta'), ('dvr', 'dvr'), ('edit', 'edit'), ('edit_location', 'edit_location'), ('eject', 'eject'), ('email', 'email'), ('enhanced_encryption', 'enhanced_encryption'), ('equalizer', 'equalizer'), ('error', 'error'), ('error_outline', 'error_outline'), ('euro_symbol', 'euro_symbol'), ('ev_station', 'ev_station'), ('event', 'event'), ('event_available', 'event_available'), ('event_busy', 'event_busy'), ('event_note', 'event_note'), ('event_seat', 'event_seat'), ('exit_to_app', 'exit_to_app'), ('expand_less', 'expand_less'), ('expand_more', 'expand_more'), ('explicit', 'explicit'), ('explore', 'explore'), ('exposure', 'exposure'), ('exposure_neg_1', 'exposure_neg_1'), ('exposure_neg_2', 'exposure_neg_2'), ('exposure_plus_1', 'exposure_plus_1'), ('exposure_plus_2', 'exposure_plus_2'), ('exposure_zero', 'exposure_zero'), ('extension', 'extension'), ('face', 'face'), ('fast_forward', 'fast_forward'), ('fast_rewind', 'fast_rewind'), ('favorite', 'favorite'), ('favorite_border', 'favorite_border'), ('featured_play_list', 'featured_play_list'), ('featured_video', 'featured_video'), ('feedback', 'feedback'), ('fiber_dvr', 'fiber_dvr'), ('fiber_manual_record', 'fiber_manual_record'), ('fiber_new', 'fiber_new'), ('fiber_pin', 'fiber_pin'), ('fiber_smart_record', 'fiber_smart_record'), ('file_download', 'file_download'), ('file_upload', 'file_upload'), ('filter', 'filter'), ('filter_1', 'filter_1'), ('filter_2', 'filter_2'), ('filter_3', 'filter_3'), ('filter_4', 'filter_4'), ('filter_5', 'filter_5'), ('filter_6', 'filter_6'), ('filter_7', 'filter_7'), ('filter_8', 'filter_8'), ('filter_9', 'filter_9'), ('filter_9_plus', 'filter_9_plus'), ('filter_b_and_w', 'filter_b_and_w'), ('filter_center_focus', 'filter_center_focus'), ('filter_drama', 'filter_drama'), ('filter_frames', 'filter_frames'), ('filter_hdr', 'filter_hdr'), ('filter_list', 'filter_list'), ('filter_none', 'filter_none'), ('filter_tilt_shift', 'filter_tilt_shift'), ('filter_vintage', 'filter_vintage'), ('find_in_page', 'find_in_page'), ('find_replace', 'find_replace'), ('fingerprint', 'fingerprint'), ('first_page', 'first_page'), ('fitness_center', 'fitness_center'), ('flag', 'flag'), ('flare', 'flare'), ('flash_auto', 'flash_auto'), ('flash_off', 'flash_off'), ('flash_on', 'flash_on'), ('flight', 'flight'), ('flight_land', 'flight_land'), ('flight_takeoff', 'flight_takeoff'), ('flip', 'flip'), ('flip_to_back', 'flip_to_back'), ('flip_to_front', 'flip_to_front'), ('folder', 'folder'), ('folder_open', 'folder_open'), ('folder_shared', 'folder_shared'), ('folder_special', 'folder_special'), ('font_download', 'font_download'), ('format_align_center', 'format_align_center'), ('format_align_justify', 'format_align_justify'), ('format_align_left', 'format_align_left'), ('format_align_right', 'format_align_right'), ('format_bold', 'format_bold'), ('format_clear', 'format_clear'), ('format_color_fill', 'format_color_fill'), ('format_color_reset', 'format_color_reset'), ('format_color_text', 'format_color_text'), ('format_indent_decrease', 'format_indent_decrease'), ('format_indent_increase', 'format_indent_increase'), ('format_italic', 'format_italic'), ('format_line_spacing', 'format_line_spacing'), ('format_list_bulleted', 'format_list_bulleted'), ('format_list_numbered', 'format_list_numbered'), ('format_paint', 'format_paint'), ('format_quote', 'format_quote'), ('format_shapes', 'format_shapes'), ('format_size', 'format_size'), ('format_strikethrough', 'format_strikethrough'), ('format_textdirection_l_to_r', 'format_textdirection_l_to_r'), ('format_textdirection_r_to_l', 'format_textdirection_r_to_l'), ('format_underlined', 'format_underlined'), ('forum', 'forum'), ('forward', 'forward'), ('forward_10', 'forward_10'), ('forward_30', 'forward_30'), ('forward_5', 'forward_5'), ('free_breakfast', 'free_breakfast'), ('fullscreen', 'fullscreen'), ('fullscreen_exit', 'fullscreen_exit'), ('functions', 'functions'), ('g_translate', 'g_translate'), ('gamepad', 'gamepad'), ('games', 'games'), ('gavel', 'gavel'), ('gesture', 'gesture'), ('get_app', 'get_app'), ('gif', 'gif'), ('golf_course', 'golf_course'), ('gps_fixed', 'gps_fixed'), ('gps_not_fixed', 'gps_not_fixed'), ('gps_off', 'gps_off'), ('grade', 'grade'), ('gradient', 'gradient'), ('grain', 'grain'), ('graphic_eq', 'graphic_eq'), ('grid_off', 'grid_off'), ('grid_on', 'grid_on'), ('group', 'group'), ('group_add', 'group_add'), ('group_work', 'group_work'), ('hd', 'hd'), ('hdr_off', 'hdr_off'), ('hdr_on', 'hdr_on'), ('hdr_strong', 'hdr_strong'), ('hdr_weak', 'hdr_weak'), ('headset', 'headset'), ('headset_mic', 'headset_mic'), ('healing', 'healing'), ('hearing', 'hearing'), ('help', 'help'), ('help_outline', 'help_outline'), ('high_quality', 'high_quality'), ('highlight', 'highlight'), ('highlight_off', 'highlight_off'), ('history', 'history'), ('home', 'home'), ('hot_tub', 'hot_tub'), ('hotel', 'hotel'), ('hourglass_empty', 'hourglass_empty'), ('hourglass_full', 'hourglass_full'), ('http', 'http'), ('https', 'https'), ('image', 'image'), ('image_aspect_ratio', 'image_aspect_ratio'), ('import_contacts', 'import_contacts'), ('import_export', 'import_export'), ('important_devices', 'important_devices'), ('inbox', 'inbox'), ('indeterminate_check_box', 'indeterminate_check_box'), ('info', 'info'), ('info_outline', 'info_outline'), ('input', 'input'), ('insert_chart', 'insert_chart'), ('insert_comment', 'insert_comment'), ('insert_drive_file', 'insert_drive_file'), ('insert_emoticon', 'insert_emoticon'), ('insert_invitation', 'insert_invitation'), ('insert_link', 'insert_link'), ('insert_photo', 'insert_photo'), ('invert_colors', 'invert_colors'), ('invert_colors_off', 'invert_colors_off'), ('iso', 'iso'), ('keyboard', 'keyboard'), ('keyboard_arrow_down', 'keyboard_arrow_down'), ('keyboard_arrow_left', 'keyboard_arrow_left'), ('keyboard_arrow_right', 'keyboard_arrow_right'), ('keyboard_arrow_up', 'keyboard_arrow_up'), ('keyboard_backspace', 'keyboard_backspace'), ('keyboard_capslock', 'keyboard_capslock'), ('keyboard_hide', 'keyboard_hide'), ('keyboard_return', 'keyboard_return'), ('keyboard_tab', 'keyboard_tab'), ('keyboard_voice', 'keyboard_voice'), ('kitchen', 'kitchen'), ('label', 'label'), ('label_outline', 'label_outline'), ('landscape', 'landscape'), ('language', 'language'), ('laptop', 'laptop'), ('laptop_chromebook', 'laptop_chromebook'), ('laptop_mac', 'laptop_mac'), ('laptop_windows', 'laptop_windows'), ('last_page', 'last_page'), ('launch', 'launch'), ('layers', 'layers'), ('layers_clear', 'layers_clear'), ('leak_add', 'leak_add'), ('leak_remove', 'leak_remove'), ('lens', 'lens'), ('library_add', 'library_add'), ('library_books', 'library_books'), ('library_music', 'library_music'), ('lightbulb_outline', 'lightbulb_outline'), ('line_style', 'line_style'), ('line_weight', 'line_weight'), ('linear_scale', 'linear_scale'), ('link', 'link'), ('linked_camera', 'linked_camera'), ('list', 'list'), ('live_help', 'live_help'), ('live_tv', 'live_tv'), ('local_activity', 'local_activity'), ('local_airport', 'local_airport'), ('local_atm', 'local_atm'), ('local_bar', 'local_bar'), ('local_cafe', 'local_cafe'), ('local_car_wash', 'local_car_wash'), ('local_convenience_store', 'local_convenience_store'), ('local_dining', 'local_dining'), ('local_drink', 'local_drink'), ('local_florist', 'local_florist'), ('local_gas_station', 'local_gas_station'), ('local_grocery_store', 'local_grocery_store'), ('local_hospital', 'local_hospital'), ('local_hotel', 'local_hotel'), ('local_laundry_service', 'local_laundry_service'), ('local_library', 'local_library'), ('local_mall', 'local_mall'), ('local_movies', 'local_movies'), ('local_offer', 'local_offer'), ('local_parking', 'local_parking'), ('local_pharmacy', 'local_pharmacy'), ('local_phone', 'local_phone'), ('local_pizza', 'local_pizza'), ('local_play', 'local_play'), ('local_post_office', 'local_post_office'), ('local_printshop', 'local_printshop'), ('local_see', 'local_see'), ('local_shipping', 'local_shipping'), ('local_taxi', 'local_taxi'), ('location_city', 'location_city'), ('location_disabled', 'location_disabled'), ('location_off', 'location_off'), ('location_on', 'location_on'), ('location_searching', 'location_searching'), ('lock', 'lock'), ('lock_open', 'lock_open'), ('lock_outline', 'lock_outline'), ('looks', 'looks'), ('looks_3', 'looks_3'), ('looks_4', 'looks_4'), ('looks_5', 'looks_5'), ('looks_6', 'looks_6'), ('looks_one', 'looks_one'), ('looks_two', 'looks_two'), ('loop', 'loop'), ('loupe', 'loupe'), ('low_priority', 'low_priority'), ('loyalty', 'loyalty'), ('mail', 'mail'), ('mail_outline', 'mail_outline'), ('map', 'map'), ('markunread', 'markunread'), ('markunread_mailbox', 'markunread_mailbox'), ('memory', 'memory'), ('menu', 'menu'), ('merge_type', 'merge_type'), ('message', 'message'), ('mic', 'mic'), ('mic_none', 'mic_none'), ('mic_off', 'mic_off'), ('mms', 'mms'), ('mode_comment', 'mode_comment'), ('mode_edit', 'mode_edit'), ('monetization_on', 'monetization_on'), ('money_off', 'money_off'), ('monochrome_photos', 'monochrome_photos'), ('mood', 'mood'), ('mood_bad', 'mood_bad'), ('more', 'more'), ('more_horiz', 'more_horiz'), ('more_vert', 'more_vert'), ('motorcycle', 'motorcycle'), ('mouse', 'mouse'), ('move_to_inbox', 'move_to_inbox'), ('movie', 'movie'), ('movie_creation', 'movie_creation'), ('movie_filter', 'movie_filter'), ('multiline_chart', 'multiline_chart'), ('music_note', 'music_note'), ('music_video', 'music_video'), ('my_location', 'my_location'), ('nature', 'nature'), ('nature_people', 'nature_people'), ('navigate_before', 'navigate_before'), ('navigate_next', 'navigate_next'), ('navigation', 'navigation'), ('near_me', 'near_me'), ('network_cell', 'network_cell'), ('network_check', 'network_check'), ('network_locked', 'network_locked'), ('network_wifi', 'network_wifi'), ('new_releases', 'new_releases'), ('next_week', 'next_week'), ('nfc', 'nfc'), ('no_encryption', 'no_encryption'), ('no_sim', 'no_sim'), ('not_interested', 'not_interested'), ('note', 'note'), ('note_add', 'note_add'), ('notifications', 'notifications'), ('notifications_active', 'notifications_active'), ('notifications_none', 'notifications_none'), ('notifications_off', 'notifications_off'), ('notifications_paused', 'notifications_paused'), ('offline_pin', 'offline_pin'), ('ondemand_video', 'ondemand_video'), ('opacity', 'opacity'), ('open_in_browser', 'open_in_browser'), ('open_in_new', 'open_in_new'), ('open_with', 'open_with'), ('pages', 'pages'), ('pageview', 'pageview'), ('palette', 'palette'), ('pan_tool', 'pan_tool'), ('panorama', 'panorama'), ('panorama_fish_eye', 'panorama_fish_eye'), ('panorama_horizontal', 'panorama_horizontal'), ('panorama_vertical', 'panorama_vertical'), ('panorama_wide_angle', 'panorama_wide_angle'), ('party_mode', 'party_mode'), ('pause', 'pause'), ('pause_circle_filled', 'pause_circle_filled'), ('pause_circle_outline', 'pause_circle_outline'), ('payment', 'payment'), ('people', 'people'), ('people_outline', 'people_outline'), ('perm_camera_mic', 'perm_camera_mic'), ('perm_contact_calendar', 'perm_contact_calendar'), ('perm_data_setting', 'perm_data_setting'), ('perm_device_information', 'perm_device_information'), ('perm_identity', 'perm_identity'), ('perm_media', 'perm_media'), ('perm_phone_msg', 'perm_phone_msg'), ('perm_scan_wifi', 'perm_scan_wifi'), ('person', 'person'), ('person_add', 'person_add'), ('person_outline', 'person_outline'), ('person_pin', 'person_pin'), ('person_pin_circle', 'person_pin_circle'), ('personal_video', 'personal_video'), ('pets', 'pets'), ('phone', 'phone'), ('phone_android', 'phone_android'), ('phone_bluetooth_speaker', 'phone_bluetooth_speaker'), ('phone_forwarded', 'phone_forwarded'), ('phone_in_talk', 'phone_in_talk'), ('phone_iphone', 'phone_iphone'), ('phone_locked', 'phone_locked'), ('phone_missed', 'phone_missed'), ('phone_paused', 'phone_paused'), ('phonelink', 'phonelink'), ('phonelink_erase', 'phonelink_erase'), ('phonelink_lock', 'phonelink_lock'), ('phonelink_off', 'phonelink_off'), ('phonelink_ring', 'phonelink_ring'), ('phonelink_setup', 'phonelink_setup'), ('photo', 'photo'), ('photo_album', 'photo_album'), ('photo_camera', 'photo_camera'), ('photo_filter', 'photo_filter'), ('photo_library', 'photo_library'), ('photo_size_select_actual', 'photo_size_select_actual'), ('photo_size_select_large', 'photo_size_select_large'), ('photo_size_select_small', 'photo_size_select_small'), ('picture_as_pdf', 'picture_as_pdf'), ('picture_in_picture', 'picture_in_picture'), ('picture_in_picture_alt', 'picture_in_picture_alt'), ('pie_chart', 'pie_chart'), ('pie_chart_outlined', 'pie_chart_outlined'), ('pin_drop', 'pin_drop'), ('place', 'place'), ('play_arrow', 'play_arrow'), ('play_circle_filled', 'play_circle_filled'), ('play_circle_outline', 'play_circle_outline'), ('play_for_work', 'play_for_work'), ('playlist_add', 'playlist_add'), ('playlist_add_check', 'playlist_add_check'), ('playlist_play', 'playlist_play'), ('plus_one', 'plus_one'), ('poll', 'poll'), ('polymer', 'polymer'), ('pool', 'pool'), ('portable_wifi_off', 'portable_wifi_off'), ('portrait', 'portrait'), ('power', 'power'), ('power_input', 'power_input'), ('power_settings_new', 'power_settings_new'), ('pregnant_woman', 'pregnant_woman'), ('present_to_all', 'present_to_all'), ('print', 'print'), ('priority_high', 'priority_high'), ('public', 'public'), ('publish', 'publish'), ('query_builder', 'query_builder'), ('question_answer', 'question_answer'), ('queue', 'queue'), ('queue_music', 'queue_music'), ('queue_play_next', 'queue_play_next'), ('radio', 'radio'), ('radio_button_checked', 'radio_button_checked'), ('radio_button_unchecked', 'radio_button_unchecked'), ('rate_review', 'rate_review'), ('receipt', 'receipt'), ('recent_actors', 'recent_actors'), ('record_voice_over', 'record_voice_over'), ('redeem', 'redeem'), ('redo', 'redo'), ('refresh', 'refresh'), ('remove', 'remove'), ('remove_circle', 'remove_circle'), ('remove_circle_outline', 'remove_circle_outline'), ('remove_from_queue', 'remove_from_queue'), ('remove_red_eye', 'remove_red_eye'), ('remove_shopping_cart', 'remove_shopping_cart'), ('reorder', 'reorder'), ('repeat', 'repeat'), ('repeat_one', 'repeat_one'), ('replay', 'replay'), ('replay_10', 'replay_10'), ('replay_30', 'replay_30'), ('replay_5', 'replay_5'), ('reply', 'reply'), ('reply_all', 'reply_all'), ('report', 'report'), ('report_problem', 'report_problem'), ('restaurant', 'restaurant'), ('restaurant_menu', 'restaurant_menu'), ('restore', 'restore'), ('restore_page', 'restore_page'), ('ring_volume', 'ring_volume'), ('room', 'room'), ('room_service', 'room_service'), ('rotate_90_degrees_ccw', 'rotate_90_degrees_ccw'), ('rotate_left', 'rotate_left'), ('rotate_right', 'rotate_right'), ('rounded_corner', 'rounded_corner'), ('router', 'router'), ('rowing', 'rowing'), ('rss_feed', 'rss_feed'), ('rv_hookup', 'rv_hookup'), ('satellite', 'satellite'), ('save', 'save'), ('scanner', 'scanner'), ('schedule', 'schedule'), ('school', 'school'), ('screen_lock_landscape', 'screen_lock_landscape'), ('screen_lock_portrait', 'screen_lock_portrait'), ('screen_lock_rotation', 'screen_lock_rotation'), ('screen_rotation', 'screen_rotation'), ('screen_share', 'screen_share'), ('sd_card', 'sd_card'), ('sd_storage', 'sd_storage'), ('search', 'search'), ('security', 'security'), ('select_all', 'select_all'), ('send', 'send'), ('sentiment_dissatisfied', 'sentiment_dissatisfied'), ('sentiment_neutral', 'sentiment_neutral'), ('sentiment_satisfied', 'sentiment_satisfied'), ('sentiment_very_dissatisfied', 'sentiment_very_dissatisfied'), ('sentiment_very_satisfied', 'sentiment_very_satisfied'), ('settings', 'settings'), ('settings_applications', 'settings_applications'), ('settings_backup_restore', 'settings_backup_restore'), ('settings_bluetooth', 'settings_bluetooth'), ('settings_brightness', 'settings_brightness'), ('settings_cell', 'settings_cell'), ('settings_ethernet', 'settings_ethernet'), ('settings_input_antenna', 'settings_input_antenna'), ('settings_input_component', 'settings_input_component'), ('settings_input_composite', 'settings_input_composite'), ('settings_input_hdmi', 'settings_input_hdmi'), ('settings_input_svideo', 'settings_input_svideo'), ('settings_overscan', 'settings_overscan'), ('settings_phone', 'settings_phone'), ('settings_power', 'settings_power'), ('settings_remote', 'settings_remote'), ('settings_system_daydream', 'settings_system_daydream'), ('settings_voice', 'settings_voice'), ('share', 'share'), ('shop', 'shop'), ('shop_two', 'shop_two'), ('shopping_basket', 'shopping_basket'), ('shopping_cart', 'shopping_cart'), ('short_text', 'short_text'), ('show_chart', 'show_chart'), ('shuffle', 'shuffle'), ('signal_cellular_4_bar', 'signal_cellular_4_bar'), ('signal_cellular_connected_no_internet_4_bar', 'signal_cellular_connected_no_internet_4_bar'), ('signal_cellular_no_sim', 'signal_cellular_no_sim'), ('signal_cellular_null', 'signal_cellular_null'), ('signal_cellular_off', 'signal_cellular_off'), ('signal_wifi_4_bar', 'signal_wifi_4_bar'), ('signal_wifi_4_bar_lock', 'signal_wifi_4_bar_lock'), ('signal_wifi_off', 'signal_wifi_off'), ('sim_card', 'sim_card'), ('sim_card_alert', 'sim_card_alert'), ('skip_next', 'skip_next'), ('skip_previous', 'skip_previous'), ('slideshow', 'slideshow'), ('slow_motion_video', 'slow_motion_video'), ('smartphone', 'smartphone'), ('smoke_free', 'smoke_free'), ('smoking_rooms', 'smoking_rooms'), ('sms', 'sms'), ('sms_failed', 'sms_failed'), ('snooze', 'snooze'), ('sort', 'sort'), ('sort_by_alpha', 'sort_by_alpha'), ('spa', 'spa'), ('space_bar', 'space_bar'), ('speaker', 'speaker'), ('speaker_group', 'speaker_group'), ('speaker_notes', 'speaker_notes'), ('speaker_notes_off', 'speaker_notes_off'), ('speaker_phone', 'speaker_phone'), ('spellcheck', 'spellcheck'), ('star', 'star'), ('star_border', 'star_border'), ('star_half', 'star_half'), ('stars', 'stars'), ('stay_current_landscape', 'stay_current_landscape'), ('stay_current_portrait', 'stay_current_portrait'), ('stay_primary_landscape', 'stay_primary_landscape'), ('stay_primary_portrait', 'stay_primary_portrait'), ('stop', 'stop'), ('stop_screen_share', 'stop_screen_share'), ('storage', 'storage'), ('store', 'store'), ('store_mall_directory', 'store_mall_directory'), ('straighten', 'straighten'), ('streetview', 'streetview'), ('strikethrough_s', 'strikethrough_s'), ('style', 'style'), ('subdirectory_arrow_left', 'subdirectory_arrow_left'), ('subdirectory_arrow_right', 'subdirectory_arrow_right'), ('subject', 'subject'), ('subscriptions', 'subscriptions'), ('subtitles', 'subtitles'), ('subway', 'subway'), ('supervisor_account', 'supervisor_account'), ('surround_sound', 'surround_sound'), ('swap_calls', 'swap_calls'), ('swap_horiz', 'swap_horiz'), ('swap_vert', 'swap_vert'), ('swap_vertical_circle', 'swap_vertical_circle'), ('switch_camera', 'switch_camera'), ('switch_video', 'switch_video'), ('sync', 'sync'), ('sync_disabled', 'sync_disabled'), ('sync_problem', 'sync_problem'), ('system_update', 'system_update'), ('system_update_alt', 'system_update_alt'), ('tab', 'tab'), ('tab_unselected', 'tab_unselected'), ('tablet', 'tablet'), ('tablet_android', 'tablet_android'), ('tablet_mac', 'tablet_mac'), ('tag_faces', 'tag_faces'), ('tap_and_play', 'tap_and_play'), ('terrain', 'terrain'), ('text_fields', 'text_fields'), ('text_format', 'text_format'), ('textsms', 'textsms'), ('texture', 'texture'), ('theaters', 'theaters'), ('thumb_down', 'thumb_down'), ('thumb_up', 'thumb_up'), ('thumbs_up_down', 'thumbs_up_down'), ('time_to_leave', 'time_to_leave'), ('timelapse', 'timelapse'), ('timeline', 'timeline'), ('timer', 'timer'), ('timer_10', 'timer_10'), ('timer_3', 'timer_3'), ('timer_off', 'timer_off'), ('title', 'title'), ('toc', 'toc'), ('today', 'today'), ('toll', 'toll'), ('tonality', 'tonality'), ('touch_app', 'touch_app'), ('toys', 'toys'), ('track_changes', 'track_changes'), ('traffic', 'traffic'), ('train', 'train'), ('tram', 'tram'), ('transfer_within_a_station', 'transfer_within_a_station'), ('transform', 'transform'), ('translate', 'translate'), ('trending_down', 'trending_down'), ('trending_flat', 'trending_flat'), ('trending_up', 'trending_up'), ('tune', 'tune'), ('turned_in', 'turned_in'), ('turned_in_not', 'turned_in_not'), ('tv', 'tv'), ('unarchive', 'unarchive'), ('undo', 'undo'), ('unfold_less', 'unfold_less'), ('unfold_more', 'unfold_more'), ('update', 'update'), ('usb', 'usb'), ('verified_user', 'verified_user'), ('vertical_align_bottom', 'vertical_align_bottom'), ('vertical_align_center', 'vertical_align_center'), ('vertical_align_top', 'vertical_align_top'), ('vibration', 'vibration'), ('video_call', 'video_call'), ('video_label', 'video_label'), ('video_library', 'video_library'), ('videocam', 'videocam'), ('videocam_off', 'videocam_off'), ('videogame_asset', 'videogame_asset'), ('view_agenda', 'view_agenda'), ('view_array', 'view_array'), ('view_carousel', 'view_carousel'), ('view_column', 'view_column'), ('view_comfy', 'view_comfy'), ('view_compact', 'view_compact'), ('view_day', 'view_day'), ('view_headline', 'view_headline'), ('view_list', 'view_list'), ('view_module', 'view_module'), ('view_quilt', 'view_quilt'), ('view_stream', 'view_stream'), ('view_week', 'view_week'), ('vignette', 'vignette'), ('visibility', 'visibility'), ('visibility_off', 'visibility_off'), ('voice_chat', 'voice_chat'), ('voicemail', 'voicemail'), ('volume_down', 'volume_down'), ('volume_mute', 'volume_mute'), ('volume_off', 'volume_off'), ('volume_up', 'volume_up'), ('vpn_key', 'vpn_key'), ('vpn_lock', 'vpn_lock'), ('wallpaper', 'wallpaper'), ('warning', 'warning'), ('watch', 'watch'), ('watch_later', 'watch_later'), ('wb_auto', 'wb_auto'), ('wb_cloudy', 'wb_cloudy'), ('wb_incandescent', 'wb_incandescent'), ('wb_iridescent', 'wb_iridescent'), ('wb_sunny', 'wb_sunny'), ('wc', 'wc'), ('web', 'web'), ('web_asset', 'web_asset'), ('weekend', 'weekend'), ('whatshot', 'whatshot'), ('widgets', 'widgets'), ('wifi', 'wifi'), ('wifi_lock', 'wifi_lock'), ('wifi_tethering', 'wifi_tethering'), ('work', 'work'), ('wrap_text', 'wrap_text'), ('youtube_searched_for', 'youtube_searched_for'), ('zoom_in', 'zoom_in'), ('zoom_out', 'zoom_out'), ('zoom_out_map', 'zoom_out_map')], max_length=50, verbose_name='Icon')),
+                ('menu', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='items', to='core.CustomMenu', verbose_name='Menu')),
+                ('site', models.ForeignKey(default=1, editable=False, on_delete=django.db.models.deletion.CASCADE, to='sites.Site')),
+            ],
+            options={
+                'verbose_name': 'Custom menu item',
+                'verbose_name_plural': 'Custom menu items',
+            },
+            managers=[
+                ('objects', django.contrib.sites.managers.CurrentSiteManager()),
+            ],
+        ),
+        migrations.CreateModel(
+            name='AnnouncementRecipient',
+            fields=[
+                ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+                ('extended_data', django.contrib.postgres.fields.jsonb.JSONField(default=dict, editable=False)),
+                ('recipient_id', models.PositiveIntegerField()),
+                ('announcement', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='recipients', to='core.Announcement')),
+                ('content_type', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='contenttypes.ContentType')),
+                ('site', models.ForeignKey(default=1, editable=False, on_delete=django.db.models.deletion.CASCADE, to='sites.Site')),
+            ],
+            options={
+                'verbose_name': 'Announcement recipient',
+                'verbose_name_plural': 'Announcement recipients',
+            },
+            managers=[
+                ('objects', django.contrib.sites.managers.CurrentSiteManager()),
+            ],
+        ),
+        migrations.CreateModel(
+            name='Activity',
+            fields=[
+                ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+                ('extended_data', django.contrib.postgres.fields.jsonb.JSONField(default=dict, editable=False)),
+                ('title', models.CharField(max_length=150, verbose_name='Title')),
+                ('description', models.TextField(max_length=500, verbose_name='Description')),
+                ('app', models.CharField(max_length=100, verbose_name='Application')),
+                ('site', models.ForeignKey(default=1, editable=False, on_delete=django.db.models.deletion.CASCADE, to='sites.Site')),
+                ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='activities', to='core.Person', verbose_name='User')),
+            ],
+            options={
+                'verbose_name': 'Activity',
+                'verbose_name_plural': 'Activities',
+            },
+            managers=[
+                ('objects', django.contrib.sites.managers.CurrentSiteManager()),
+            ],
         ),
     ]
diff --git a/aleksis/core/migrations/0002_activity_notification.py b/aleksis/core/migrations/0002_activity_notification.py
deleted file mode 100644
index 0b05696e31ab754c009dd64743b06cd2d73deaa1..0000000000000000000000000000000000000000
--- a/aleksis/core/migrations/0002_activity_notification.py
+++ /dev/null
@@ -1,42 +0,0 @@
-# Generated by Django 3.0.2 on 2020-01-03 19:19
-
-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', '0001_initial'),
-    ]
-
-    operations = [
-        migrations.CreateModel(
-            name='Notification',
-            fields=[
-                ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
-                ('title', models.CharField(max_length=150)),
-                ('description', models.TextField(max_length=500)),
-                ('link', models.URLField(blank=True)),
-                ('app', models.CharField(max_length=100, verbose_name='Sender')),
-                ('read', models.BooleanField(default=False)),
-                ('mailed', models.BooleanField(default=False, verbose_name='Sent')),
-                ('created_at', models.DateTimeField(default=django.utils.timezone.now)),
-                ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='notifications',
-                                           to=settings.AUTH_USER_MODEL)),
-            ],
-        ),
-        migrations.CreateModel(
-            name='Activity',
-            fields=[
-                ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
-                ('title', models.CharField(max_length=150)),
-                ('description', models.TextField(max_length=500)),
-                ('app', models.CharField(max_length=100)),
-                ('created_at', models.DateTimeField(default=django.utils.timezone.now)),
-                ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
-            ],
-        ),
-    ]
diff --git a/aleksis/core/migrations/0002_school_term.py b/aleksis/core/migrations/0002_school_term.py
new file mode 100644
index 0000000000000000000000000000000000000000..456f78ebf7e16f32b92b954d36ad39f5d323e866
--- /dev/null
+++ b/aleksis/core/migrations/0002_school_term.py
@@ -0,0 +1,64 @@
+# Generated by Django 3.0.7 on 2020-06-13 14:34
+
+import django.contrib.postgres.fields.jsonb
+from django.db import migrations, models
+import django.db.models.deletion
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('sites', '0002_alter_domain_unique'),
+        ('core', '0001_initial'),
+    ]
+
+    operations = [
+        migrations.CreateModel(
+            name='SchoolTerm',
+            fields=[
+                ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+                ('extended_data', django.contrib.postgres.fields.jsonb.JSONField(default=dict, editable=False)),
+                ('name', models.CharField(max_length=255, unique=True, verbose_name='Name')),
+                ('date_start', models.DateField(verbose_name='Start date')),
+                ('date_end', models.DateField(verbose_name='End date')),
+            ],
+            options={
+                'verbose_name': 'School term',
+                'verbose_name_plural': 'School terms',
+            },
+        ),
+        migrations.AlterModelManagers(
+            name='group',
+            managers=[
+            ],
+        ),
+        migrations.AlterField(
+            model_name='group',
+            name='name',
+            field=models.CharField(max_length=255, verbose_name='Long name'),
+        ),
+        migrations.AlterField(
+            model_name='group',
+            name='short_name',
+            field=models.CharField(blank=True, max_length=255, null=True, verbose_name='Short name'),
+        ),
+        migrations.AddField(
+            model_name='group',
+            name='school_term',
+            field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE,
+                                    related_name='+', to='core.SchoolTerm', verbose_name='Linked school term'),
+        ),
+        migrations.AddConstraint(
+            model_name='group',
+            constraint=models.UniqueConstraint(fields=('school_term', 'name'), name='unique_school_term_name'),
+        ),
+        migrations.AddConstraint(
+            model_name='group',
+            constraint=models.UniqueConstraint(fields=('school_term', 'short_name'), name='unique_school_term_short_name'),
+        ),
+        migrations.AddField(
+            model_name='schoolterm',
+            name='site',
+            field=models.ForeignKey(default=1, editable=False, on_delete=django.db.models.deletion.CASCADE, to='sites.Site'),
+        ),
+    ]
diff --git a/aleksis/core/migrations/0003_add_permissions_for_group_stats.py b/aleksis/core/migrations/0003_add_permissions_for_group_stats.py
new file mode 100644
index 0000000000000000000000000000000000000000..7f031fa5eb3eb3cff216b40bc251300561f03091
--- /dev/null
+++ b/aleksis/core/migrations/0003_add_permissions_for_group_stats.py
@@ -0,0 +1,22 @@
+# Generated by Django 3.0.7 on 2020-06-28 14:02
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('core', '0002_school_term'),
+    ]
+
+    operations = [
+        migrations.AlterModelOptions(
+            name='group',
+            options={'ordering': ['short_name', 'name'], 'permissions': (('assign_child_groups_to_groups', 'Can assign child groups to groups'), ('view_group_stats', 'Can view statistics about group.')), 'verbose_name': 'Group', 'verbose_name_plural': 'Groups'},
+        ),
+        migrations.AlterField(
+            model_name='group',
+            name='additional_fields',
+            field=models.ManyToManyField(blank=True, to='core.AdditionalField', verbose_name='Additional fields'),
+        ),
+    ]
diff --git a/aleksis/core/migrations/0003_add_verbose_names.py b/aleksis/core/migrations/0003_add_verbose_names.py
deleted file mode 100644
index 68e2e00302fdfc5d8e4be9c891e76ca2aea28225..0000000000000000000000000000000000000000
--- a/aleksis/core/migrations/0003_add_verbose_names.py
+++ /dev/null
@@ -1,69 +0,0 @@
-# Generated by Django 3.0.2 on 2020-01-05 16:50
-
-from django.db import migrations, models
-import django.utils.timezone
-
-
-class Migration(migrations.Migration):
-
-    dependencies = [
-        ('core', '0002_activity_notification'),
-    ]
-
-    operations = [
-        migrations.AlterField(
-            model_name='activity',
-            name='app',
-            field=models.CharField(max_length=100, verbose_name='Application'),
-        ),
-        migrations.AlterField(
-            model_name='activity',
-            name='created_at',
-            field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='Created at'),
-        ),
-        migrations.AlterField(
-            model_name='activity',
-            name='description',
-            field=models.TextField(max_length=500, verbose_name='Description'),
-        ),
-        migrations.AlterField(
-            model_name='activity',
-            name='title',
-            field=models.CharField(max_length=150, verbose_name='Title'),
-        ),
-        migrations.AlterField(
-            model_name='notification',
-            name='app',
-            field=models.CharField(max_length=100, verbose_name='Application'),
-        ),
-        migrations.AlterField(
-            model_name='notification',
-            name='created_at',
-            field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='Created at'),
-        ),
-        migrations.AlterField(
-            model_name='notification',
-            name='description',
-            field=models.TextField(max_length=500, verbose_name='Description'),
-        ),
-        migrations.AlterField(
-            model_name='notification',
-            name='link',
-            field=models.URLField(blank=True, verbose_name='Link'),
-        ),
-        migrations.AlterField(
-            model_name='notification',
-            name='mailed',
-            field=models.BooleanField(default=False, verbose_name='Mailed'),
-        ),
-        migrations.AlterField(
-            model_name='notification',
-            name='read',
-            field=models.BooleanField(default=False, verbose_name='Read'),
-        ),
-        migrations.AlterField(
-            model_name='notification',
-            name='title',
-            field=models.CharField(max_length=150, verbose_name='Title'),
-        ),
-    ]
diff --git a/aleksis/core/migrations/0004_replace_user_by_person.py b/aleksis/core/migrations/0004_replace_user_by_person.py
deleted file mode 100644
index 32222f8782866903eaa138880c2477291993cb75..0000000000000000000000000000000000000000
--- a/aleksis/core/migrations/0004_replace_user_by_person.py
+++ /dev/null
@@ -1,24 +0,0 @@
-# Generated by Django 3.0.2 on 2020-01-05 18:32
-
-from django.db import migrations, models
-import django.db.models.deletion
-
-
-class Migration(migrations.Migration):
-
-    dependencies = [
-        ('core', '0003_add_verbose_names'),
-    ]
-
-    operations = [
-        migrations.AlterField(
-            model_name='activity',
-            name='user',
-            field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='activities', to='core.Person'),
-        ),
-        migrations.AlterField(
-            model_name='notification',
-            name='user',
-            field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='notifications', to='core.Person'),
-        ),
-    ]
diff --git a/aleksis/core/migrations/0005_add_verbose_names_meta.py b/aleksis/core/migrations/0005_add_verbose_names_meta.py
deleted file mode 100644
index 9c2b69399b586507e2aca265283f71cde90afab7..0000000000000000000000000000000000000000
--- a/aleksis/core/migrations/0005_add_verbose_names_meta.py
+++ /dev/null
@@ -1,37 +0,0 @@
-# Generated by Django 3.0.2 on 2020-01-05 22:40
-
-from django.db import migrations
-
-
-class Migration(migrations.Migration):
-
-    dependencies = [
-        ('core', '0004_replace_user_by_person'),
-    ]
-
-    operations = [
-        migrations.AlterModelOptions(
-            name='activity',
-            options={'verbose_name': 'Activity', 'verbose_name_plural': 'Activities'},
-        ),
-        migrations.AlterModelOptions(
-            name='group',
-            options={'ordering': ['short_name', 'name'], 'verbose_name': 'Group', 'verbose_name_plural': 'Groups'},
-        ),
-        migrations.AlterModelOptions(
-            name='notification',
-            options={'verbose_name': 'Notification', 'verbose_name_plural': 'Notifications'},
-        ),
-        migrations.AlterModelOptions(
-            name='person',
-            options={'ordering': ['last_name', 'first_name'], 'verbose_name': 'Person', 'verbose_name_plural': 'Persons'},
-        ),
-        migrations.AlterModelOptions(
-            name='school',
-            options={'ordering': ['name', 'name_official'], 'verbose_name': 'School', 'verbose_name_plural': 'Schools'},
-        ),
-        migrations.AlterModelOptions(
-            name='schoolterm',
-            options={'verbose_name': 'School term', 'verbose_name_plural': 'School terms'},
-        ),
-    ]
diff --git a/aleksis/core/migrations/0008_rename_fields_notification_activity.py b/aleksis/core/migrations/0008_rename_fields_notification_activity.py
deleted file mode 100644
index fdec9870b58cfd0f5769319e22f447b8555ba35a..0000000000000000000000000000000000000000
--- a/aleksis/core/migrations/0008_rename_fields_notification_activity.py
+++ /dev/null
@@ -1,39 +0,0 @@
-# Generated by Django 3.0.2 on 2020-01-22 16:49
-
-import django.contrib.postgres.fields.jsonb
-from django.db import migrations, models
-
-
-class Migration(migrations.Migration):
-
-    dependencies = [
-        ('core', '0005_add_verbose_names_meta'),
-    ]
-
-    operations = [
-        migrations.RenameField(
-            model_name='notification',
-            old_name='user',
-            new_name='recipient',
-        ),
-        migrations.RenameField(
-            model_name='notification',
-            old_name='app',
-            new_name='sender',
-        ),
-        migrations.RenameField(
-            model_name='notification',
-            old_name='mailed',
-            new_name='sent',
-        ),
-        migrations.AlterField(
-            model_name='activity',
-            name='created_at',
-            field=models.DateTimeField(auto_now_add=True, verbose_name='Created at'),
-        ),
-        migrations.AlterField(
-            model_name='notification',
-            name='created_at',
-            field=models.DateTimeField(auto_now_add=True, verbose_name='Created at'),
-        ),
-    ]
diff --git a/aleksis/core/migrations/0009_dashboard_widget.py b/aleksis/core/migrations/0009_dashboard_widget.py
deleted file mode 100644
index b57c272b4cc8501e41b5ffaf9f2ec3d796795021..0000000000000000000000000000000000000000
--- a/aleksis/core/migrations/0009_dashboard_widget.py
+++ /dev/null
@@ -1,28 +0,0 @@
-# Generated by Django 3.0.2 on 2020-01-29 16:45
-
-from django.db import migrations, models
-import django.db.models.deletion
-
-
-class Migration(migrations.Migration):
-
-    dependencies = [
-        ('contenttypes', '0002_remove_content_type_name'),
-        ('core', '0008_rename_fields_notification_activity'),
-    ]
-
-    operations = [
-        migrations.CreateModel(
-            name='DashboardWidget',
-            fields=[
-                ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
-                ('title', models.CharField(max_length=150, verbose_name='Widget Title')),
-                ('active', models.BooleanField(blank=True, verbose_name='Activate Widget')),
-                ('polymorphic_ctype', models.ForeignKey(editable=False, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='polymorphic_core.dashboardwidget_set+', to='contenttypes.ContentType')),
-            ],
-            options={
-                'verbose_name': 'Dashboard Widget',
-                'verbose_name_plural': 'Dashboard Widgets',
-            },
-        ),
-    ]
diff --git a/aleksis/core/migrations/0011_make_primary_group_optional.py b/aleksis/core/migrations/0011_make_primary_group_optional.py
deleted file mode 100644
index 0d28af2f4c01013a139328851aba9f5881e1d883..0000000000000000000000000000000000000000
--- a/aleksis/core/migrations/0011_make_primary_group_optional.py
+++ /dev/null
@@ -1,19 +0,0 @@
-# Generated by Django 3.0.2 on 2020-02-03 22:41
-
-from django.db import migrations, models
-import django.db.models.deletion
-
-
-class Migration(migrations.Migration):
-
-    dependencies = [
-        ('core', '0009_dashboard_widget'),
-    ]
-
-    operations = [
-        migrations.AlterField(
-            model_name='person',
-            name='primary_group',
-            field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='core.Group'),
-        ),
-    ]
diff --git a/aleksis/core/migrations/0012_announcement.py b/aleksis/core/migrations/0012_announcement.py
deleted file mode 100644
index 7cfd66158d4565fed228194e753c56f516fd4b6b..0000000000000000000000000000000000000000
--- a/aleksis/core/migrations/0012_announcement.py
+++ /dev/null
@@ -1,29 +0,0 @@
-# Generated by Django 3.0.3 on 2020-02-10 14:22
-
-import datetime
-from django.db import migrations, models
-import django.db.models.deletion
-
-
-class Migration(migrations.Migration):
-
-    dependencies = [
-        ('contenttypes', '0002_remove_content_type_name'),
-        ('core', '0011_make_primary_group_optional'),
-    ]
-
-    operations = [
-        migrations.CreateModel(
-            name='Announcement',
-            fields=[
-                ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
-                ('title', models.CharField(max_length=150, verbose_name='Title')),
-                ('description', models.TextField(max_length=500, verbose_name='Description')),
-                ('link', models.URLField(blank=True, verbose_name='Link')),
-                ('valid_from', models.DateTimeField(default=datetime.datetime.now, verbose_name='Date and time from when to show')),
-                ('valid_until', models.DateTimeField(blank=True, null=True, verbose_name='Date and time until when to show')),
-                ('recipient_id', models.PositiveIntegerField()),
-                ('content_type', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='contenttypes.ContentType')),
-            ],
-        ),
-    ]
diff --git a/aleksis/core/migrations/0013_extensible_model_as_default.py b/aleksis/core/migrations/0013_extensible_model_as_default.py
deleted file mode 100644
index 4e23d86f49c0fcc53d92813b794c4083e475a6e3..0000000000000000000000000000000000000000
--- a/aleksis/core/migrations/0013_extensible_model_as_default.py
+++ /dev/null
@@ -1,38 +0,0 @@
-# Generated by Django 3.0.3 on 2020-02-20 12:24
-
-import aleksis.core.util.core_helpers
-import django.contrib.postgres.fields.jsonb
-from django.db import migrations, models
-
-
-class Migration(migrations.Migration):
-
-    dependencies = [
-        ('core', '0012_announcement'),
-    ]
-
-    operations = [
-        migrations.RemoveField(
-            model_name='activity',
-            name='created_at',
-        ),
-        migrations.RemoveField(
-            model_name='notification',
-            name='created_at',
-        ),
-        migrations.AddField(
-            model_name='activity',
-            name='extended_data',
-            field=django.contrib.postgres.fields.jsonb.JSONField(default=dict, editable=False),
-        ),
-        migrations.AddField(
-            model_name='announcement',
-            name='extended_data',
-            field=django.contrib.postgres.fields.jsonb.JSONField(default=dict, editable=False),
-        ),
-        migrations.AddField(
-            model_name='notification',
-            name='extended_data',
-            field=django.contrib.postgres.fields.jsonb.JSONField(default=dict, editable=False),
-        ),
-    ]
diff --git a/aleksis/core/migrations/0014_accouncement_recipients.py b/aleksis/core/migrations/0014_accouncement_recipients.py
deleted file mode 100644
index 50e78da8fbb67c81fade2cd3e8923ada4653cc08..0000000000000000000000000000000000000000
--- a/aleksis/core/migrations/0014_accouncement_recipients.py
+++ /dev/null
@@ -1,57 +0,0 @@
-# Generated by Django 3.0.3 on 2020-03-11 18:43
-
-import aleksis.core.models
-import django.contrib.postgres.fields.jsonb
-from django.db import migrations, models
-import django.db.models.deletion
-
-
-class Migration(migrations.Migration):
-
-     dependencies = [
-         ('contenttypes', '0002_remove_content_type_name'),
-         ('core', '0013_extensible_model_as_default'),
-     ]
-
-     operations = [
-         migrations.AlterModelOptions(
-            name='announcement',
-            options={'verbose_name': 'Announcement', 'verbose_name_plural': 'Announcements'},
-         ),
-         migrations.RemoveField(
-             model_name='announcement',
-             name='content_type',
-         ),
-         migrations.RemoveField(
-             model_name='announcement',
-             name='recipient_id',
-         ),
-         migrations.AlterField(
-             model_name='announcement',
-             name='description',
-             field=models.TextField(blank=True, max_length=500, verbose_name='Description'),
-         ),
-         migrations.AlterField(
-             model_name='announcement',
-             name='valid_until',
-             field=models.DateTimeField(default=aleksis.core.util.core_helpers.now_tomorrow, verbose_name='Date and time until when to show'),
-         ),
-         migrations.CreateModel(
-             name='AnnouncementRecipient',
-             fields=[
-                 ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
-                 ('recipient_id', models.PositiveIntegerField()),
-                 ('announcement', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='recipients', to='core.Announcement')),
-                 ('content_type', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='contenttypes.ContentType')),
-             ],
-         ),
-         migrations.AlterModelOptions(
-             name='announcementrecipient',
-             options={'verbose_name': 'Announcement recipient', 'verbose_name_plural': 'Announcement recipients'},
-         ),
-         migrations.AddField(
-             model_name='announcementrecipient',
-             name='extended_data',
-             field=django.contrib.postgres.fields.jsonb.JSONField(default=dict, editable=False),
-         ),
-     ]
diff --git a/aleksis/core/migrations/0015_add_import_ref_to_group.py b/aleksis/core/migrations/0015_add_import_ref_to_group.py
deleted file mode 100644
index 280e7a5565b622e1ea164dda848b4c70f5a95e02..0000000000000000000000000000000000000000
--- a/aleksis/core/migrations/0015_add_import_ref_to_group.py
+++ /dev/null
@@ -1,18 +0,0 @@
-# Generated by Django 3.0.4 on 2020-03-26 23:51
-
-from django.db import migrations, models
-
-
-class Migration(migrations.Migration):
-
-    dependencies = [
-        ('core', '0014_accouncement_recipients'),
-    ]
-
-    operations = [
-        migrations.AddField(
-            model_name='group',
-            name='import_ref',
-            field=models.CharField(blank=True, editable=False, max_length=200, null=True, unique=True, verbose_name='Reference ID of import source'),
-        ),
-    ]
diff --git a/aleksis/core/migrations/0016_custom_menus.py b/aleksis/core/migrations/0016_custom_menus.py
deleted file mode 100644
index 4961d51c6b2e3e34da2245c288e06864bbef3090..0000000000000000000000000000000000000000
--- a/aleksis/core/migrations/0016_custom_menus.py
+++ /dev/null
@@ -1,42 +0,0 @@
-# Generated by Django 3.0.4 on 2020-03-21 16:39
-
-import django.contrib.postgres.fields.jsonb
-from django.db import migrations, models
-import django.db.models.deletion
-
-
-class Migration(migrations.Migration):
-
-    dependencies = [
-        ('core', '0015_add_import_ref_to_group'),
-    ]
-
-    operations = [
-        migrations.CreateModel(
-            name='CustomMenu',
-            fields=[
-                ('extended_data', django.contrib.postgres.fields.jsonb.JSONField(default=dict, editable=False)),
-                ('id', models.CharField(max_length=100, primary_key=True, serialize=False, verbose_name='Menu ID')),
-                ('name', models.CharField(max_length=150, verbose_name='Menu name')),
-            ],
-            options={
-                'verbose_name': 'Custom menu',
-                'verbose_name_plural': 'Custom menus',
-            },
-        ),
-        migrations.CreateModel(
-            name='CustomMenuItem',
-            fields=[
-                ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
-                ('extended_data', django.contrib.postgres.fields.jsonb.JSONField(default=dict, editable=False)),
-                ('name', models.CharField(max_length=150, verbose_name='Name')),
-                ('url', models.URLField(verbose_name='Link')),
-                ('icon', models.CharField(blank=True, choices=[('3d_rotation', '3d_rotation'), ('ac_unit', 'ac_unit'), ('access_alarm', 'access_alarm'), ('access_alarms', 'access_alarms'), ('access_time', 'access_time'), ('accessibility', 'accessibility'), ('accessible', 'accessible'), ('account_balance', 'account_balance'), ('account_balance_wallet', 'account_balance_wallet'), ('account_box', 'account_box'), ('account_circle', 'account_circle'), ('adb', 'adb'), ('add', 'add'), ('add_a_photo', 'add_a_photo'), ('add_alarm', 'add_alarm'), ('add_alert', 'add_alert'), ('add_box', 'add_box'), ('add_circle', 'add_circle'), ('add_circle_outline', 'add_circle_outline'), ('add_location', 'add_location'), ('add_shopping_cart', 'add_shopping_cart'), ('add_to_photos', 'add_to_photos'), ('add_to_queue', 'add_to_queue'), ('adjust', 'adjust'), ('airline_seat_flat', 'airline_seat_flat'), ('airline_seat_flat_angled', 'airline_seat_flat_angled'), ('airline_seat_individual_suite', 'airline_seat_individual_suite'), ('airline_seat_legroom_extra', 'airline_seat_legroom_extra'), ('airline_seat_legroom_normal', 'airline_seat_legroom_normal'), ('airline_seat_legroom_reduced', 'airline_seat_legroom_reduced'), ('airline_seat_recline_extra', 'airline_seat_recline_extra'), ('airline_seat_recline_normal', 'airline_seat_recline_normal'), ('airplanemode_active', 'airplanemode_active'), ('airplanemode_inactive', 'airplanemode_inactive'), ('airplay', 'airplay'), ('airport_shuttle', 'airport_shuttle'), ('alarm', 'alarm'), ('alarm_add', 'alarm_add'), ('alarm_off', 'alarm_off'), ('alarm_on', 'alarm_on'), ('album', 'album'), ('all_inclusive', 'all_inclusive'), ('all_out', 'all_out'), ('android', 'android'), ('announcement', 'announcement'), ('apps', 'apps'), ('archive', 'archive'), ('arrow_back', 'arrow_back'), ('arrow_downward', 'arrow_downward'), ('arrow_drop_down', 'arrow_drop_down'), ('arrow_drop_down_circle', 'arrow_drop_down_circle'), ('arrow_drop_up', 'arrow_drop_up'), ('arrow_forward', 'arrow_forward'), ('arrow_upward', 'arrow_upward'), ('art_track', 'art_track'), ('aspect_ratio', 'aspect_ratio'), ('assessment', 'assessment'), ('assignment', 'assignment'), ('assignment_ind', 'assignment_ind'), ('assignment_late', 'assignment_late'), ('assignment_return', 'assignment_return'), ('assignment_returned', 'assignment_returned'), ('assignment_turned_in', 'assignment_turned_in'), ('assistant', 'assistant'), ('assistant_photo', 'assistant_photo'), ('attach_file', 'attach_file'), ('attach_money', 'attach_money'), ('attachment', 'attachment'), ('audiotrack', 'audiotrack'), ('autorenew', 'autorenew'), ('av_timer', 'av_timer'), ('backspace', 'backspace'), ('backup', 'backup'), ('battery_alert', 'battery_alert'), ('battery_charging_full', 'battery_charging_full'), ('battery_full', 'battery_full'), ('battery_std', 'battery_std'), ('battery_unknown', 'battery_unknown'), ('beach_access', 'beach_access'), ('beenhere', 'beenhere'), ('block', 'block'), ('bluetooth', 'bluetooth'), ('bluetooth_audio', 'bluetooth_audio'), ('bluetooth_connected', 'bluetooth_connected'), ('bluetooth_disabled', 'bluetooth_disabled'), ('bluetooth_searching', 'bluetooth_searching'), ('blur_circular', 'blur_circular'), ('blur_linear', 'blur_linear'), ('blur_off', 'blur_off'), ('blur_on', 'blur_on'), ('book', 'book'), ('bookmark', 'bookmark'), ('bookmark_border', 'bookmark_border'), ('border_all', 'border_all'), ('border_bottom', 'border_bottom'), ('border_clear', 'border_clear'), ('border_color', 'border_color'), ('border_horizontal', 'border_horizontal'), ('border_inner', 'border_inner'), ('border_left', 'border_left'), ('border_outer', 'border_outer'), ('border_right', 'border_right'), ('border_style', 'border_style'), ('border_top', 'border_top'), ('border_vertical', 'border_vertical'), ('branding_watermark', 'branding_watermark'), ('brightness_1', 'brightness_1'), ('brightness_2', 'brightness_2'), ('brightness_3', 'brightness_3'), ('brightness_4', 'brightness_4'), ('brightness_5', 'brightness_5'), ('brightness_6', 'brightness_6'), ('brightness_7', 'brightness_7'), ('brightness_auto', 'brightness_auto'), ('brightness_high', 'brightness_high'), ('brightness_low', 'brightness_low'), ('brightness_medium', 'brightness_medium'), ('broken_image', 'broken_image'), ('brush', 'brush'), ('bubble_chart', 'bubble_chart'), ('bug_report', 'bug_report'), ('build', 'build'), ('burst_mode', 'burst_mode'), ('business', 'business'), ('business_center', 'business_center'), ('cached', 'cached'), ('cake', 'cake'), ('call', 'call'), ('call_end', 'call_end'), ('call_made', 'call_made'), ('call_merge', 'call_merge'), ('call_missed', 'call_missed'), ('call_missed_outgoing', 'call_missed_outgoing'), ('call_received', 'call_received'), ('call_split', 'call_split'), ('call_to_action', 'call_to_action'), ('camera', 'camera'), ('camera_alt', 'camera_alt'), ('camera_enhance', 'camera_enhance'), ('camera_front', 'camera_front'), ('camera_rear', 'camera_rear'), ('camera_roll', 'camera_roll'), ('cancel', 'cancel'), ('card_giftcard', 'card_giftcard'), ('card_membership', 'card_membership'), ('card_travel', 'card_travel'), ('casino', 'casino'), ('cast', 'cast'), ('cast_connected', 'cast_connected'), ('center_focus_strong', 'center_focus_strong'), ('center_focus_weak', 'center_focus_weak'), ('change_history', 'change_history'), ('chat', 'chat'), ('chat_bubble', 'chat_bubble'), ('chat_bubble_outline', 'chat_bubble_outline'), ('check', 'check'), ('check_box', 'check_box'), ('check_box_outline_blank', 'check_box_outline_blank'), ('check_circle', 'check_circle'), ('chevron_left', 'chevron_left'), ('chevron_right', 'chevron_right'), ('child_care', 'child_care'), ('child_friendly', 'child_friendly'), ('chrome_reader_mode', 'chrome_reader_mode'), ('class', 'class'), ('clear', 'clear'), ('clear_all', 'clear_all'), ('close', 'close'), ('closed_caption', 'closed_caption'), ('cloud', 'cloud'), ('cloud_circle', 'cloud_circle'), ('cloud_done', 'cloud_done'), ('cloud_download', 'cloud_download'), ('cloud_off', 'cloud_off'), ('cloud_queue', 'cloud_queue'), ('cloud_upload', 'cloud_upload'), ('code', 'code'), ('collections', 'collections'), ('collections_bookmark', 'collections_bookmark'), ('color_lens', 'color_lens'), ('colorize', 'colorize'), ('comment', 'comment'), ('compare', 'compare'), ('compare_arrows', 'compare_arrows'), ('computer', 'computer'), ('confirmation_number', 'confirmation_number'), ('contact_mail', 'contact_mail'), ('contact_phone', 'contact_phone'), ('contacts', 'contacts'), ('content_copy', 'content_copy'), ('content_cut', 'content_cut'), ('content_paste', 'content_paste'), ('control_point', 'control_point'), ('control_point_duplicate', 'control_point_duplicate'), ('copyright', 'copyright'), ('create', 'create'), ('create_new_folder', 'create_new_folder'), ('credit_card', 'credit_card'), ('crop', 'crop'), ('crop_16_9', 'crop_16_9'), ('crop_3_2', 'crop_3_2'), ('crop_5_4', 'crop_5_4'), ('crop_7_5', 'crop_7_5'), ('crop_din', 'crop_din'), ('crop_free', 'crop_free'), ('crop_landscape', 'crop_landscape'), ('crop_original', 'crop_original'), ('crop_portrait', 'crop_portrait'), ('crop_rotate', 'crop_rotate'), ('crop_square', 'crop_square'), ('dashboard', 'dashboard'), ('data_usage', 'data_usage'), ('date_range', 'date_range'), ('dehaze', 'dehaze'), ('delete', 'delete'), ('delete_forever', 'delete_forever'), ('delete_sweep', 'delete_sweep'), ('description', 'description'), ('desktop_mac', 'desktop_mac'), ('desktop_windows', 'desktop_windows'), ('details', 'details'), ('developer_board', 'developer_board'), ('developer_mode', 'developer_mode'), ('device_hub', 'device_hub'), ('devices', 'devices'), ('devices_other', 'devices_other'), ('dialer_sip', 'dialer_sip'), ('dialpad', 'dialpad'), ('directions', 'directions'), ('directions_bike', 'directions_bike'), ('directions_boat', 'directions_boat'), ('directions_bus', 'directions_bus'), ('directions_car', 'directions_car'), ('directions_railway', 'directions_railway'), ('directions_run', 'directions_run'), ('directions_subway', 'directions_subway'), ('directions_transit', 'directions_transit'), ('directions_walk', 'directions_walk'), ('disc_full', 'disc_full'), ('dns', 'dns'), ('do_not_disturb', 'do_not_disturb'), ('do_not_disturb_alt', 'do_not_disturb_alt'), ('do_not_disturb_off', 'do_not_disturb_off'), ('do_not_disturb_on', 'do_not_disturb_on'), ('dock', 'dock'), ('domain', 'domain'), ('done', 'done'), ('done_all', 'done_all'), ('donut_large', 'donut_large'), ('donut_small', 'donut_small'), ('drafts', 'drafts'), ('drag_handle', 'drag_handle'), ('drive_eta', 'drive_eta'), ('dvr', 'dvr'), ('edit', 'edit'), ('edit_location', 'edit_location'), ('eject', 'eject'), ('email', 'email'), ('enhanced_encryption', 'enhanced_encryption'), ('equalizer', 'equalizer'), ('error', 'error'), ('error_outline', 'error_outline'), ('euro_symbol', 'euro_symbol'), ('ev_station', 'ev_station'), ('event', 'event'), ('event_available', 'event_available'), ('event_busy', 'event_busy'), ('event_note', 'event_note'), ('event_seat', 'event_seat'), ('exit_to_app', 'exit_to_app'), ('expand_less', 'expand_less'), ('expand_more', 'expand_more'), ('explicit', 'explicit'), ('explore', 'explore'), ('exposure', 'exposure'), ('exposure_neg_1', 'exposure_neg_1'), ('exposure_neg_2', 'exposure_neg_2'), ('exposure_plus_1', 'exposure_plus_1'), ('exposure_plus_2', 'exposure_plus_2'), ('exposure_zero', 'exposure_zero'), ('extension', 'extension'), ('face', 'face'), ('fast_forward', 'fast_forward'), ('fast_rewind', 'fast_rewind'), ('favorite', 'favorite'), ('favorite_border', 'favorite_border'), ('featured_play_list', 'featured_play_list'), ('featured_video', 'featured_video'), ('feedback', 'feedback'), ('fiber_dvr', 'fiber_dvr'), ('fiber_manual_record', 'fiber_manual_record'), ('fiber_new', 'fiber_new'), ('fiber_pin', 'fiber_pin'), ('fiber_smart_record', 'fiber_smart_record'), ('file_download', 'file_download'), ('file_upload', 'file_upload'), ('filter', 'filter'), ('filter_1', 'filter_1'), ('filter_2', 'filter_2'), ('filter_3', 'filter_3'), ('filter_4', 'filter_4'), ('filter_5', 'filter_5'), ('filter_6', 'filter_6'), ('filter_7', 'filter_7'), ('filter_8', 'filter_8'), ('filter_9', 'filter_9'), ('filter_9_plus', 'filter_9_plus'), ('filter_b_and_w', 'filter_b_and_w'), ('filter_center_focus', 'filter_center_focus'), ('filter_drama', 'filter_drama'), ('filter_frames', 'filter_frames'), ('filter_hdr', 'filter_hdr'), ('filter_list', 'filter_list'), ('filter_none', 'filter_none'), ('filter_tilt_shift', 'filter_tilt_shift'), ('filter_vintage', 'filter_vintage'), ('find_in_page', 'find_in_page'), ('find_replace', 'find_replace'), ('fingerprint', 'fingerprint'), ('first_page', 'first_page'), ('fitness_center', 'fitness_center'), ('flag', 'flag'), ('flare', 'flare'), ('flash_auto', 'flash_auto'), ('flash_off', 'flash_off'), ('flash_on', 'flash_on'), ('flight', 'flight'), ('flight_land', 'flight_land'), ('flight_takeoff', 'flight_takeoff'), ('flip', 'flip'), ('flip_to_back', 'flip_to_back'), ('flip_to_front', 'flip_to_front'), ('folder', 'folder'), ('folder_open', 'folder_open'), ('folder_shared', 'folder_shared'), ('folder_special', 'folder_special'), ('font_download', 'font_download'), ('format_align_center', 'format_align_center'), ('format_align_justify', 'format_align_justify'), ('format_align_left', 'format_align_left'), ('format_align_right', 'format_align_right'), ('format_bold', 'format_bold'), ('format_clear', 'format_clear'), ('format_color_fill', 'format_color_fill'), ('format_color_reset', 'format_color_reset'), ('format_color_text', 'format_color_text'), ('format_indent_decrease', 'format_indent_decrease'), ('format_indent_increase', 'format_indent_increase'), ('format_italic', 'format_italic'), ('format_line_spacing', 'format_line_spacing'), ('format_list_bulleted', 'format_list_bulleted'), ('format_list_numbered', 'format_list_numbered'), ('format_paint', 'format_paint'), ('format_quote', 'format_quote'), ('format_shapes', 'format_shapes'), ('format_size', 'format_size'), ('format_strikethrough', 'format_strikethrough'), ('format_textdirection_l_to_r', 'format_textdirection_l_to_r'), ('format_textdirection_r_to_l', 'format_textdirection_r_to_l'), ('format_underlined', 'format_underlined'), ('forum', 'forum'), ('forward', 'forward'), ('forward_10', 'forward_10'), ('forward_30', 'forward_30'), ('forward_5', 'forward_5'), ('free_breakfast', 'free_breakfast'), ('fullscreen', 'fullscreen'), ('fullscreen_exit', 'fullscreen_exit'), ('functions', 'functions'), ('g_translate', 'g_translate'), ('gamepad', 'gamepad'), ('games', 'games'), ('gavel', 'gavel'), ('gesture', 'gesture'), ('get_app', 'get_app'), ('gif', 'gif'), ('golf_course', 'golf_course'), ('gps_fixed', 'gps_fixed'), ('gps_not_fixed', 'gps_not_fixed'), ('gps_off', 'gps_off'), ('grade', 'grade'), ('gradient', 'gradient'), ('grain', 'grain'), ('graphic_eq', 'graphic_eq'), ('grid_off', 'grid_off'), ('grid_on', 'grid_on'), ('group', 'group'), ('group_add', 'group_add'), ('group_work', 'group_work'), ('hd', 'hd'), ('hdr_off', 'hdr_off'), ('hdr_on', 'hdr_on'), ('hdr_strong', 'hdr_strong'), ('hdr_weak', 'hdr_weak'), ('headset', 'headset'), ('headset_mic', 'headset_mic'), ('healing', 'healing'), ('hearing', 'hearing'), ('help', 'help'), ('help_outline', 'help_outline'), ('high_quality', 'high_quality'), ('highlight', 'highlight'), ('highlight_off', 'highlight_off'), ('history', 'history'), ('home', 'home'), ('hot_tub', 'hot_tub'), ('hotel', 'hotel'), ('hourglass_empty', 'hourglass_empty'), ('hourglass_full', 'hourglass_full'), ('http', 'http'), ('https', 'https'), ('image', 'image'), ('image_aspect_ratio', 'image_aspect_ratio'), ('import_contacts', 'import_contacts'), ('import_export', 'import_export'), ('important_devices', 'important_devices'), ('inbox', 'inbox'), ('indeterminate_check_box', 'indeterminate_check_box'), ('info', 'info'), ('info_outline', 'info_outline'), ('input', 'input'), ('insert_chart', 'insert_chart'), ('insert_comment', 'insert_comment'), ('insert_drive_file', 'insert_drive_file'), ('insert_emoticon', 'insert_emoticon'), ('insert_invitation', 'insert_invitation'), ('insert_link', 'insert_link'), ('insert_photo', 'insert_photo'), ('invert_colors', 'invert_colors'), ('invert_colors_off', 'invert_colors_off'), ('iso', 'iso'), ('keyboard', 'keyboard'), ('keyboard_arrow_down', 'keyboard_arrow_down'), ('keyboard_arrow_left', 'keyboard_arrow_left'), ('keyboard_arrow_right', 'keyboard_arrow_right'), ('keyboard_arrow_up', 'keyboard_arrow_up'), ('keyboard_backspace', 'keyboard_backspace'), ('keyboard_capslock', 'keyboard_capslock'), ('keyboard_hide', 'keyboard_hide'), ('keyboard_return', 'keyboard_return'), ('keyboard_tab', 'keyboard_tab'), ('keyboard_voice', 'keyboard_voice'), ('kitchen', 'kitchen'), ('label', 'label'), ('label_outline', 'label_outline'), ('landscape', 'landscape'), ('language', 'language'), ('laptop', 'laptop'), ('laptop_chromebook', 'laptop_chromebook'), ('laptop_mac', 'laptop_mac'), ('laptop_windows', 'laptop_windows'), ('last_page', 'last_page'), ('launch', 'launch'), ('layers', 'layers'), ('layers_clear', 'layers_clear'), ('leak_add', 'leak_add'), ('leak_remove', 'leak_remove'), ('lens', 'lens'), ('library_add', 'library_add'), ('library_books', 'library_books'), ('library_music', 'library_music'), ('lightbulb_outline', 'lightbulb_outline'), ('line_style', 'line_style'), ('line_weight', 'line_weight'), ('linear_scale', 'linear_scale'), ('link', 'link'), ('linked_camera', 'linked_camera'), ('list', 'list'), ('live_help', 'live_help'), ('live_tv', 'live_tv'), ('local_activity', 'local_activity'), ('local_airport', 'local_airport'), ('local_atm', 'local_atm'), ('local_bar', 'local_bar'), ('local_cafe', 'local_cafe'), ('local_car_wash', 'local_car_wash'), ('local_convenience_store', 'local_convenience_store'), ('local_dining', 'local_dining'), ('local_drink', 'local_drink'), ('local_florist', 'local_florist'), ('local_gas_station', 'local_gas_station'), ('local_grocery_store', 'local_grocery_store'), ('local_hospital', 'local_hospital'), ('local_hotel', 'local_hotel'), ('local_laundry_service', 'local_laundry_service'), ('local_library', 'local_library'), ('local_mall', 'local_mall'), ('local_movies', 'local_movies'), ('local_offer', 'local_offer'), ('local_parking', 'local_parking'), ('local_pharmacy', 'local_pharmacy'), ('local_phone', 'local_phone'), ('local_pizza', 'local_pizza'), ('local_play', 'local_play'), ('local_post_office', 'local_post_office'), ('local_printshop', 'local_printshop'), ('local_see', 'local_see'), ('local_shipping', 'local_shipping'), ('local_taxi', 'local_taxi'), ('location_city', 'location_city'), ('location_disabled', 'location_disabled'), ('location_off', 'location_off'), ('location_on', 'location_on'), ('location_searching', 'location_searching'), ('lock', 'lock'), ('lock_open', 'lock_open'), ('lock_outline', 'lock_outline'), ('looks', 'looks'), ('looks_3', 'looks_3'), ('looks_4', 'looks_4'), ('looks_5', 'looks_5'), ('looks_6', 'looks_6'), ('looks_one', 'looks_one'), ('looks_two', 'looks_two'), ('loop', 'loop'), ('loupe', 'loupe'), ('low_priority', 'low_priority'), ('loyalty', 'loyalty'), ('mail', 'mail'), ('mail_outline', 'mail_outline'), ('map', 'map'), ('markunread', 'markunread'), ('markunread_mailbox', 'markunread_mailbox'), ('memory', 'memory'), ('menu', 'menu'), ('merge_type', 'merge_type'), ('message', 'message'), ('mic', 'mic'), ('mic_none', 'mic_none'), ('mic_off', 'mic_off'), ('mms', 'mms'), ('mode_comment', 'mode_comment'), ('mode_edit', 'mode_edit'), ('monetization_on', 'monetization_on'), ('money_off', 'money_off'), ('monochrome_photos', 'monochrome_photos'), ('mood', 'mood'), ('mood_bad', 'mood_bad'), ('more', 'more'), ('more_horiz', 'more_horiz'), ('more_vert', 'more_vert'), ('motorcycle', 'motorcycle'), ('mouse', 'mouse'), ('move_to_inbox', 'move_to_inbox'), ('movie', 'movie'), ('movie_creation', 'movie_creation'), ('movie_filter', 'movie_filter'), ('multiline_chart', 'multiline_chart'), ('music_note', 'music_note'), ('music_video', 'music_video'), ('my_location', 'my_location'), ('nature', 'nature'), ('nature_people', 'nature_people'), ('navigate_before', 'navigate_before'), ('navigate_next', 'navigate_next'), ('navigation', 'navigation'), ('near_me', 'near_me'), ('network_cell', 'network_cell'), ('network_check', 'network_check'), ('network_locked', 'network_locked'), ('network_wifi', 'network_wifi'), ('new_releases', 'new_releases'), ('next_week', 'next_week'), ('nfc', 'nfc'), ('no_encryption', 'no_encryption'), ('no_sim', 'no_sim'), ('not_interested', 'not_interested'), ('note', 'note'), ('note_add', 'note_add'), ('notifications', 'notifications'), ('notifications_active', 'notifications_active'), ('notifications_none', 'notifications_none'), ('notifications_off', 'notifications_off'), ('notifications_paused', 'notifications_paused'), ('offline_pin', 'offline_pin'), ('ondemand_video', 'ondemand_video'), ('opacity', 'opacity'), ('open_in_browser', 'open_in_browser'), ('open_in_new', 'open_in_new'), ('open_with', 'open_with'), ('pages', 'pages'), ('pageview', 'pageview'), ('palette', 'palette'), ('pan_tool', 'pan_tool'), ('panorama', 'panorama'), ('panorama_fish_eye', 'panorama_fish_eye'), ('panorama_horizontal', 'panorama_horizontal'), ('panorama_vertical', 'panorama_vertical'), ('panorama_wide_angle', 'panorama_wide_angle'), ('party_mode', 'party_mode'), ('pause', 'pause'), ('pause_circle_filled', 'pause_circle_filled'), ('pause_circle_outline', 'pause_circle_outline'), ('payment', 'payment'), ('people', 'people'), ('people_outline', 'people_outline'), ('perm_camera_mic', 'perm_camera_mic'), ('perm_contact_calendar', 'perm_contact_calendar'), ('perm_data_setting', 'perm_data_setting'), ('perm_device_information', 'perm_device_information'), ('perm_identity', 'perm_identity'), ('perm_media', 'perm_media'), ('perm_phone_msg', 'perm_phone_msg'), ('perm_scan_wifi', 'perm_scan_wifi'), ('person', 'person'), ('person_add', 'person_add'), ('person_outline', 'person_outline'), ('person_pin', 'person_pin'), ('person_pin_circle', 'person_pin_circle'), ('personal_video', 'personal_video'), ('pets', 'pets'), ('phone', 'phone'), ('phone_android', 'phone_android'), ('phone_bluetooth_speaker', 'phone_bluetooth_speaker'), ('phone_forwarded', 'phone_forwarded'), ('phone_in_talk', 'phone_in_talk'), ('phone_iphone', 'phone_iphone'), ('phone_locked', 'phone_locked'), ('phone_missed', 'phone_missed'), ('phone_paused', 'phone_paused'), ('phonelink', 'phonelink'), ('phonelink_erase', 'phonelink_erase'), ('phonelink_lock', 'phonelink_lock'), ('phonelink_off', 'phonelink_off'), ('phonelink_ring', 'phonelink_ring'), ('phonelink_setup', 'phonelink_setup'), ('photo', 'photo'), ('photo_album', 'photo_album'), ('photo_camera', 'photo_camera'), ('photo_filter', 'photo_filter'), ('photo_library', 'photo_library'), ('photo_size_select_actual', 'photo_size_select_actual'), ('photo_size_select_large', 'photo_size_select_large'), ('photo_size_select_small', 'photo_size_select_small'), ('picture_as_pdf', 'picture_as_pdf'), ('picture_in_picture', 'picture_in_picture'), ('picture_in_picture_alt', 'picture_in_picture_alt'), ('pie_chart', 'pie_chart'), ('pie_chart_outlined', 'pie_chart_outlined'), ('pin_drop', 'pin_drop'), ('place', 'place'), ('play_arrow', 'play_arrow'), ('play_circle_filled', 'play_circle_filled'), ('play_circle_outline', 'play_circle_outline'), ('play_for_work', 'play_for_work'), ('playlist_add', 'playlist_add'), ('playlist_add_check', 'playlist_add_check'), ('playlist_play', 'playlist_play'), ('plus_one', 'plus_one'), ('poll', 'poll'), ('polymer', 'polymer'), ('pool', 'pool'), ('portable_wifi_off', 'portable_wifi_off'), ('portrait', 'portrait'), ('power', 'power'), ('power_input', 'power_input'), ('power_settings_new', 'power_settings_new'), ('pregnant_woman', 'pregnant_woman'), ('present_to_all', 'present_to_all'), ('print', 'print'), ('priority_high', 'priority_high'), ('public', 'public'), ('publish', 'publish'), ('query_builder', 'query_builder'), ('question_answer', 'question_answer'), ('queue', 'queue'), ('queue_music', 'queue_music'), ('queue_play_next', 'queue_play_next'), ('radio', 'radio'), ('radio_button_checked', 'radio_button_checked'), ('radio_button_unchecked', 'radio_button_unchecked'), ('rate_review', 'rate_review'), ('receipt', 'receipt'), ('recent_actors', 'recent_actors'), ('record_voice_over', 'record_voice_over'), ('redeem', 'redeem'), ('redo', 'redo'), ('refresh', 'refresh'), ('remove', 'remove'), ('remove_circle', 'remove_circle'), ('remove_circle_outline', 'remove_circle_outline'), ('remove_from_queue', 'remove_from_queue'), ('remove_red_eye', 'remove_red_eye'), ('remove_shopping_cart', 'remove_shopping_cart'), ('reorder', 'reorder'), ('repeat', 'repeat'), ('repeat_one', 'repeat_one'), ('replay', 'replay'), ('replay_10', 'replay_10'), ('replay_30', 'replay_30'), ('replay_5', 'replay_5'), ('reply', 'reply'), ('reply_all', 'reply_all'), ('report', 'report'), ('report_problem', 'report_problem'), ('restaurant', 'restaurant'), ('restaurant_menu', 'restaurant_menu'), ('restore', 'restore'), ('restore_page', 'restore_page'), ('ring_volume', 'ring_volume'), ('room', 'room'), ('room_service', 'room_service'), ('rotate_90_degrees_ccw', 'rotate_90_degrees_ccw'), ('rotate_left', 'rotate_left'), ('rotate_right', 'rotate_right'), ('rounded_corner', 'rounded_corner'), ('router', 'router'), ('rowing', 'rowing'), ('rss_feed', 'rss_feed'), ('rv_hookup', 'rv_hookup'), ('satellite', 'satellite'), ('save', 'save'), ('scanner', 'scanner'), ('schedule', 'schedule'), ('school', 'school'), ('screen_lock_landscape', 'screen_lock_landscape'), ('screen_lock_portrait', 'screen_lock_portrait'), ('screen_lock_rotation', 'screen_lock_rotation'), ('screen_rotation', 'screen_rotation'), ('screen_share', 'screen_share'), ('sd_card', 'sd_card'), ('sd_storage', 'sd_storage'), ('search', 'search'), ('security', 'security'), ('select_all', 'select_all'), ('send', 'send'), ('sentiment_dissatisfied', 'sentiment_dissatisfied'), ('sentiment_neutral', 'sentiment_neutral'), ('sentiment_satisfied', 'sentiment_satisfied'), ('sentiment_very_dissatisfied', 'sentiment_very_dissatisfied'), ('sentiment_very_satisfied', 'sentiment_very_satisfied'), ('settings', 'settings'), ('settings_applications', 'settings_applications'), ('settings_backup_restore', 'settings_backup_restore'), ('settings_bluetooth', 'settings_bluetooth'), ('settings_brightness', 'settings_brightness'), ('settings_cell', 'settings_cell'), ('settings_ethernet', 'settings_ethernet'), ('settings_input_antenna', 'settings_input_antenna'), ('settings_input_component', 'settings_input_component'), ('settings_input_composite', 'settings_input_composite'), ('settings_input_hdmi', 'settings_input_hdmi'), ('settings_input_svideo', 'settings_input_svideo'), ('settings_overscan', 'settings_overscan'), ('settings_phone', 'settings_phone'), ('settings_power', 'settings_power'), ('settings_remote', 'settings_remote'), ('settings_system_daydream', 'settings_system_daydream'), ('settings_voice', 'settings_voice'), ('share', 'share'), ('shop', 'shop'), ('shop_two', 'shop_two'), ('shopping_basket', 'shopping_basket'), ('shopping_cart', 'shopping_cart'), ('short_text', 'short_text'), ('show_chart', 'show_chart'), ('shuffle', 'shuffle'), ('signal_cellular_4_bar', 'signal_cellular_4_bar'), ('signal_cellular_connected_no_internet_4_bar', 'signal_cellular_connected_no_internet_4_bar'), ('signal_cellular_no_sim', 'signal_cellular_no_sim'), ('signal_cellular_null', 'signal_cellular_null'), ('signal_cellular_off', 'signal_cellular_off'), ('signal_wifi_4_bar', 'signal_wifi_4_bar'), ('signal_wifi_4_bar_lock', 'signal_wifi_4_bar_lock'), ('signal_wifi_off', 'signal_wifi_off'), ('sim_card', 'sim_card'), ('sim_card_alert', 'sim_card_alert'), ('skip_next', 'skip_next'), ('skip_previous', 'skip_previous'), ('slideshow', 'slideshow'), ('slow_motion_video', 'slow_motion_video'), ('smartphone', 'smartphone'), ('smoke_free', 'smoke_free'), ('smoking_rooms', 'smoking_rooms'), ('sms', 'sms'), ('sms_failed', 'sms_failed'), ('snooze', 'snooze'), ('sort', 'sort'), ('sort_by_alpha', 'sort_by_alpha'), ('spa', 'spa'), ('space_bar', 'space_bar'), ('speaker', 'speaker'), ('speaker_group', 'speaker_group'), ('speaker_notes', 'speaker_notes'), ('speaker_notes_off', 'speaker_notes_off'), ('speaker_phone', 'speaker_phone'), ('spellcheck', 'spellcheck'), ('star', 'star'), ('star_border', 'star_border'), ('star_half', 'star_half'), ('stars', 'stars'), ('stay_current_landscape', 'stay_current_landscape'), ('stay_current_portrait', 'stay_current_portrait'), ('stay_primary_landscape', 'stay_primary_landscape'), ('stay_primary_portrait', 'stay_primary_portrait'), ('stop', 'stop'), ('stop_screen_share', 'stop_screen_share'), ('storage', 'storage'), ('store', 'store'), ('store_mall_directory', 'store_mall_directory'), ('straighten', 'straighten'), ('streetview', 'streetview'), ('strikethrough_s', 'strikethrough_s'), ('style', 'style'), ('subdirectory_arrow_left', 'subdirectory_arrow_left'), ('subdirectory_arrow_right', 'subdirectory_arrow_right'), ('subject', 'subject'), ('subscriptions', 'subscriptions'), ('subtitles', 'subtitles'), ('subway', 'subway'), ('supervisor_account', 'supervisor_account'), ('surround_sound', 'surround_sound'), ('swap_calls', 'swap_calls'), ('swap_horiz', 'swap_horiz'), ('swap_vert', 'swap_vert'), ('swap_vertical_circle', 'swap_vertical_circle'), ('switch_camera', 'switch_camera'), ('switch_video', 'switch_video'), ('sync', 'sync'), ('sync_disabled', 'sync_disabled'), ('sync_problem', 'sync_problem'), ('system_update', 'system_update'), ('system_update_alt', 'system_update_alt'), ('tab', 'tab'), ('tab_unselected', 'tab_unselected'), ('tablet', 'tablet'), ('tablet_android', 'tablet_android'), ('tablet_mac', 'tablet_mac'), ('tag_faces', 'tag_faces'), ('tap_and_play', 'tap_and_play'), ('terrain', 'terrain'), ('text_fields', 'text_fields'), ('text_format', 'text_format'), ('textsms', 'textsms'), ('texture', 'texture'), ('theaters', 'theaters'), ('thumb_down', 'thumb_down'), ('thumb_up', 'thumb_up'), ('thumbs_up_down', 'thumbs_up_down'), ('time_to_leave', 'time_to_leave'), ('timelapse', 'timelapse'), ('timeline', 'timeline'), ('timer', 'timer'), ('timer_10', 'timer_10'), ('timer_3', 'timer_3'), ('timer_off', 'timer_off'), ('title', 'title'), ('toc', 'toc'), ('today', 'today'), ('toll', 'toll'), ('tonality', 'tonality'), ('touch_app', 'touch_app'), ('toys', 'toys'), ('track_changes', 'track_changes'), ('traffic', 'traffic'), ('train', 'train'), ('tram', 'tram'), ('transfer_within_a_station', 'transfer_within_a_station'), ('transform', 'transform'), ('translate', 'translate'), ('trending_down', 'trending_down'), ('trending_flat', 'trending_flat'), ('trending_up', 'trending_up'), ('tune', 'tune'), ('turned_in', 'turned_in'), ('turned_in_not', 'turned_in_not'), ('tv', 'tv'), ('unarchive', 'unarchive'), ('undo', 'undo'), ('unfold_less', 'unfold_less'), ('unfold_more', 'unfold_more'), ('update', 'update'), ('usb', 'usb'), ('verified_user', 'verified_user'), ('vertical_align_bottom', 'vertical_align_bottom'), ('vertical_align_center', 'vertical_align_center'), ('vertical_align_top', 'vertical_align_top'), ('vibration', 'vibration'), ('video_call', 'video_call'), ('video_label', 'video_label'), ('video_library', 'video_library'), ('videocam', 'videocam'), ('videocam_off', 'videocam_off'), ('videogame_asset', 'videogame_asset'), ('view_agenda', 'view_agenda'), ('view_array', 'view_array'), ('view_carousel', 'view_carousel'), ('view_column', 'view_column'), ('view_comfy', 'view_comfy'), ('view_compact', 'view_compact'), ('view_day', 'view_day'), ('view_headline', 'view_headline'), ('view_list', 'view_list'), ('view_module', 'view_module'), ('view_quilt', 'view_quilt'), ('view_stream', 'view_stream'), ('view_week', 'view_week'), ('vignette', 'vignette'), ('visibility', 'visibility'), ('visibility_off', 'visibility_off'), ('voice_chat', 'voice_chat'), ('voicemail', 'voicemail'), ('volume_down', 'volume_down'), ('volume_mute', 'volume_mute'), ('volume_off', 'volume_off'), ('volume_up', 'volume_up'), ('vpn_key', 'vpn_key'), ('vpn_lock', 'vpn_lock'), ('wallpaper', 'wallpaper'), ('warning', 'warning'), ('watch', 'watch'), ('watch_later', 'watch_later'), ('wb_auto', 'wb_auto'), ('wb_cloudy', 'wb_cloudy'), ('wb_incandescent', 'wb_incandescent'), ('wb_iridescent', 'wb_iridescent'), ('wb_sunny', 'wb_sunny'), ('wc', 'wc'), ('web', 'web'), ('web_asset', 'web_asset'), ('weekend', 'weekend'), ('whatshot', 'whatshot'), ('widgets', 'widgets'), ('wifi', 'wifi'), ('wifi_lock', 'wifi_lock'), ('wifi_tethering', 'wifi_tethering'), ('work', 'work'), ('wrap_text', 'wrap_text'), ('youtube_searched_for', 'youtube_searched_for'), ('zoom_in', 'zoom_in'), ('zoom_out', 'zoom_out'), ('zoom_out_map', 'zoom_out_map')], max_length=50, null=True, verbose_name='Icon')),
-                ('menu', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='items', to='core.CustomMenu', verbose_name='Menu')),
-            ],
-            options={
-                'verbose_name': 'Custom menu item',
-                'verbose_name_plural': 'Custom menu items'
-            },
-        ),
-    ]
diff --git a/aleksis/core/migrations/0017_make_group_short_name_optional.py b/aleksis/core/migrations/0017_make_group_short_name_optional.py
deleted file mode 100644
index 80c45b922b35706e3e33a2c432150d2ec87e9b14..0000000000000000000000000000000000000000
--- a/aleksis/core/migrations/0017_make_group_short_name_optional.py
+++ /dev/null
@@ -1,18 +0,0 @@
-# Generated by Django 3.0.4 on 2020-03-28 16:07
-
-from django.db import migrations, models
-
-
-class Migration(migrations.Migration):
-
-    dependencies = [
-        ('core', '0016_custom_menus'),
-    ]
-
-    operations = [
-        migrations.AlterField(
-            model_name='group',
-            name='short_name',
-            field=models.CharField(blank=True, max_length=16, null=True, unique=True, verbose_name='Short name of group'),
-        ),
-    ]
diff --git a/aleksis/core/migrations/0018_increase_length_of_group_shortname.py b/aleksis/core/migrations/0018_increase_length_of_group_shortname.py
deleted file mode 100644
index f8a0e15106f90f4e3616202207456907adb26a2a..0000000000000000000000000000000000000000
--- a/aleksis/core/migrations/0018_increase_length_of_group_shortname.py
+++ /dev/null
@@ -1,18 +0,0 @@
-# Generated by Django 3.0.4 on 2020-03-29 13:38
-
-from django.db import migrations, models
-
-
-class Migration(migrations.Migration):
-
-    dependencies = [
-        ('core', '0017_make_group_short_name_optional'),
-    ]
-
-    operations = [
-        migrations.AlterField(
-            model_name='group',
-            name='short_name',
-            field=models.CharField(blank=True, max_length=30, null=True, unique=True, verbose_name='Short name of group'),
-        ),
-    ]
diff --git a/aleksis/core/migrations/0019_drop_import_refs.py b/aleksis/core/migrations/0019_drop_import_refs.py
deleted file mode 100644
index 493eb1217964b5ca99b80981e998d05be637c0b6..0000000000000000000000000000000000000000
--- a/aleksis/core/migrations/0019_drop_import_refs.py
+++ /dev/null
@@ -1,21 +0,0 @@
-# Generated by Django 3.0.4 on 2020-03-30 12:31
-
-from django.db import migrations, models
-
-
-class Migration(migrations.Migration):
-
-    dependencies = [
-        ('core', '0018_increase_length_of_group_shortname'),
-    ]
-
-    operations = [
-        migrations.RemoveField(
-            model_name='group',
-            name='import_ref',
-        ),
-        migrations.RemoveField(
-            model_name='person',
-            name='import_ref',
-        ),
-    ]
diff --git a/aleksis/core/migrations/0020_incease_length_of_fields.py b/aleksis/core/migrations/0020_incease_length_of_fields.py
deleted file mode 100644
index d6346f123e62df628166de205d033935c04142e4..0000000000000000000000000000000000000000
--- a/aleksis/core/migrations/0020_incease_length_of_fields.py
+++ /dev/null
@@ -1,88 +0,0 @@
-# Generated by Django 3.0.4 on 2020-03-31 12:23
-
-from django.db import migrations, models
-
-
-class Migration(migrations.Migration):
-
-    dependencies = [
-        ('core', '0019_drop_import_refs'),
-    ]
-
-    operations = [
-        migrations.AlterField(
-            model_name='group',
-            name='name',
-            field=models.CharField(max_length=255, unique=True, verbose_name='Long name of group'),
-        ),
-        migrations.AlterField(
-            model_name='group',
-            name='short_name',
-            field=models.CharField(blank=True, max_length=255, null=True, unique=True, verbose_name='Short name of group'),
-        ),
-        migrations.AlterField(
-            model_name='notification',
-            name='sender',
-            field=models.CharField(max_length=100, verbose_name='Sender'),
-        ),
-        migrations.AlterField(
-            model_name='notification',
-            name='sent',
-            field=models.BooleanField(default=False, verbose_name='Sent'),
-        ),
-        migrations.AlterField(
-            model_name='person',
-            name='additional_name',
-            field=models.CharField(blank=True, max_length=255, verbose_name='Additional name(s)'),
-        ),
-        migrations.AlterField(
-            model_name='person',
-            name='first_name',
-            field=models.CharField(max_length=255, verbose_name='First name'),
-        ),
-        migrations.AlterField(
-            model_name='person',
-            name='housenumber',
-            field=models.CharField(blank=True, max_length=255, verbose_name='Street number'),
-        ),
-        migrations.AlterField(
-            model_name='person',
-            name='last_name',
-            field=models.CharField(max_length=255, verbose_name='Last name'),
-        ),
-        migrations.AlterField(
-            model_name='person',
-            name='place',
-            field=models.CharField(blank=True, max_length=255, verbose_name='Place'),
-        ),
-        migrations.AlterField(
-            model_name='person',
-            name='postal_code',
-            field=models.CharField(blank=True, max_length=255, verbose_name='Postal code'),
-        ),
-        migrations.AlterField(
-            model_name='person',
-            name='short_name',
-            field=models.CharField(blank=True, max_length=255, null=True, unique=True, verbose_name='Short name'),
-        ),
-        migrations.AlterField(
-            model_name='person',
-            name='street',
-            field=models.CharField(blank=True, max_length=255, verbose_name='Street'),
-        ),
-        migrations.AlterField(
-            model_name='school',
-            name='name',
-            field=models.CharField(max_length=255, verbose_name='Name'),
-        ),
-        migrations.AlterField(
-            model_name='school',
-            name='name_official',
-            field=models.CharField(help_text='Official name of the school, e.g. as given by supervisory authority', max_length=255, verbose_name='Official name'),
-        ),
-        migrations.AlterField(
-            model_name='schoolterm',
-            name='caption',
-            field=models.CharField(max_length=255, verbose_name='Visible caption of the term'),
-        ),
-    ]
diff --git a/aleksis/core/migrations/0021_person_description_field.py b/aleksis/core/migrations/0021_person_description_field.py
deleted file mode 100644
index 6e8aa927c1b04a2b5812dd54b86d0a0a67397cb1..0000000000000000000000000000000000000000
--- a/aleksis/core/migrations/0021_person_description_field.py
+++ /dev/null
@@ -1,18 +0,0 @@
-# Generated by Django 3.0.5 on 2020-04-13 13:24
-
-from django.db import migrations, models
-
-
-class Migration(migrations.Migration):
-
-    dependencies = [
-        ('core', '0020_incease_length_of_fields'),
-    ]
-
-    operations = [
-        migrations.AddField(
-            model_name='person',
-            name='description',
-            field=models.TextField(blank=True, null=True, verbose_name='Description'),
-        ),
-    ]
diff --git a/aleksis/core/migrations/0022_group_types.py b/aleksis/core/migrations/0022_group_types.py
deleted file mode 100644
index c521da4a6f5a5d6f6c314ee0b34e57cb374c9e7c..0000000000000000000000000000000000000000
--- a/aleksis/core/migrations/0022_group_types.py
+++ /dev/null
@@ -1,33 +0,0 @@
-# Generated by Django 3.0.5 on 2020-04-13 13:44
-
-import django.contrib.postgres.fields.jsonb
-from django.db import migrations, models
-import django.db.models.deletion
-
-
-class Migration(migrations.Migration):
-
-    dependencies = [
-        ('core', '0021_person_description_field'),
-    ]
-
-    operations = [
-        migrations.CreateModel(
-            name='GroupType',
-            fields=[
-                ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
-                ('extended_data', django.contrib.postgres.fields.jsonb.JSONField(default=dict, editable=False)),
-                ('name', models.CharField(max_length=50, verbose_name='Title of type')),
-                ('description', models.CharField(max_length=500, verbose_name='Description')),
-            ],
-            options={
-                'verbose_name': 'Group type',
-                'verbose_name_plural': 'Group types',
-            },
-        ),
-        migrations.AddField(
-            model_name='group',
-            name='type',
-            field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='type', to='core.GroupType', verbose_name='Type of group'),
-        ),
-    ]
diff --git a/aleksis/core/mixins.py b/aleksis/core/mixins.py
index 574cf185cc2350e3e7b45f7723c8e9313a631790..d196b044c610ae9ad94627e2f65d34c8d57da1a7 100644
--- a/aleksis/core/mixins.py
+++ b/aleksis/core/mixins.py
@@ -1,33 +1,55 @@
+# flake8: noqa: DJ12
+
 from datetime import datetime
-from typing import Any, Callable, Optional, Union
+from typing import Any, Callable, List, Optional, Tuple, Union
 
+from django.conf import settings
+from django.contrib import messages
 from django.contrib.contenttypes.models import ContentType
+from django.contrib.sites.managers import CurrentSiteManager
+from django.contrib.sites.models import Site
 from django.db import models
 from django.db.models import QuerySet
-from django.forms.models import ModelFormMetaclass, ModelForm
-
+from django.forms.forms import BaseForm
+from django.forms.models import ModelForm, ModelFormMetaclass
+from django.http import HttpResponse
+from django.utils.functional import lazy
+from django.utils.translation import gettext as _
+from django.views.generic import CreateView, UpdateView
+from django.views.generic.edit import ModelFormMixin
+
+import reversion
 from easyaudit.models import CRUDEvent
-from jsonstore.fields import JSONField, JSONFieldMixin
-from material.base import LayoutNode, Layout
+from guardian.admin import GuardedModelAdmin
+from jsonstore.fields import IntegerField, JSONField, JSONFieldMixin
+from material.base import Layout, LayoutNode
+from rules.contrib.admin import ObjectPermissionsModelAdmin
 
+from aleksis.core.managers import CurrentSiteManagerWithoutMigrations, SchoolTermRelatedQuerySet
 
-class CRUDMixin(models.Model):
-    class Meta:
-        abstract = True
 
-    @property
-    def crud_events(self) -> QuerySet:
-        """Get all CRUD events connected to this object from easyaudit."""
+class _ExtensibleModelBase(models.base.ModelBase):
+    """Ensure predefined behaviour on model creation.
 
-        content_type = ContentType.objects.get_for_model(self)
+    This metaclass serves the following purposes:
 
-        return CRUDEvent.objects.filter(
-            object_id=self.pk, content_type=content_type
-        ).select_related("user")
+     - Register all AlekSIS models with django-reverseion
+    """
+
+    def __new__(mcls, name, bases, attrs):
+        mcls = super().__new__(mcls, name, bases, attrs)
+
+        if "Meta" not in attrs or not attrs["Meta"].abstract:
+            # Register all non-abstract models with django-reversion
+            mcls = reversion.register(mcls)
 
+            mcls.extra_permissions = []
 
-class ExtensibleModel(CRUDMixin):
-    """ Base model for all objects in AlekSIS apps
+        return mcls
+
+
+class ExtensibleModel(models.Model, metaclass=_ExtensibleModelBase):
+    """Base model for all objects in AlekSIS apps.
 
     This base model ensures all objects in AlekSIS apps fulfill the
     following properties:
@@ -74,31 +96,46 @@ class ExtensibleModel(CRUDMixin):
     # Defines a material design icon associated with this type of model
     icon_ = "radio_button_unchecked"
 
+    site = models.ForeignKey(
+        Site, on_delete=models.CASCADE, default=settings.SITE_ID, editable=False
+    )
+    objects = CurrentSiteManager()
+    objects_all_sites = models.Manager()
+
+    extra_permissions = []
+
     def get_absolute_url(self) -> str:
-        """ Get the URL o a view representing this model instance """
+        """Get the URL o a view representing this model instance."""
         pass
 
+    @property
+    def crud_events(self) -> QuerySet:
+        """Get all CRUD events connected to this object from easyaudit."""
+        content_type = ContentType.objects.get_for_model(self)
+
+        return CRUDEvent.objects.filter(
+            object_id=self.pk, content_type=content_type
+        ).select_related("user")
+
     @property
     def crud_event_create(self) -> Optional[CRUDEvent]:
-        """ Return create event of this object """
+        """Return create event of this object."""
         return self.crud_events.filter(event_type=CRUDEvent.CREATE).latest("datetime")
 
     @property
     def crud_event_update(self) -> Optional[CRUDEvent]:
-        """ Return last event of this object """
+        """Return last event of this object."""
         return self.crud_events.latest("datetime")
 
     @property
     def created_at(self) -> Optional[datetime]:
-        """ Determine creation timestamp from CRUD log """
-
+        """Determine creation timestamp from CRUD log."""
         if self.crud_event_create:
             return self.crud_event_create.datetime
 
     @property
     def updated_at(self) -> Optional[datetime]:
-        """ Determine last timestamp from CRUD log """
-
+        """Determine last timestamp from CRUD log."""
         if self.crud_event_update:
             return self.crud_event_update.datetime
 
@@ -106,15 +143,13 @@ class ExtensibleModel(CRUDMixin):
 
     @property
     def created_by(self) -> Optional[models.Model]:
-        """ Determine user who created this object from CRUD log """
-
+        """Determine user who created this object from CRUD log."""
         if self.crud_event_create:
             return self.crud_event_create.user
 
     @property
     def updated_by(self) -> Optional[models.Model]:
-        """ Determine user who last updated this object from CRUD log """
-
+        """Determine user who last updated this object from CRUD log."""
         if self.crud_event_update:
             return self.crud_event_update.user
 
@@ -129,45 +164,40 @@ class ExtensibleModel(CRUDMixin):
             if name.isidentifier():
                 prop_name = name
             else:
-                raise ValueError("%s is not a valid name." % name)
+                raise ValueError(f"{name} is not a valid name.")
 
         # Verify that attribute name does not clash with other names in the class
         if hasattr(cls, prop_name):
-            raise ValueError("%s already used." % prop_name)
+            raise ValueError(f"{prop_name} already used.")
 
         # Let Django's model magic add the attribute if we got here
         cls.add_to_class(name, obj)
 
     @classmethod
-    def property(cls, func: Callable[[], Any], name: Optional[str] = None) -> None:
-        """ Adds the passed callable as a property. """
-
-        cls._safe_add(property(func), func.__name__)
+    def property_(cls, func: Callable[[], Any], name: Optional[str] = None) -> None:
+        """Add the passed callable as a property."""
+        cls._safe_add(property(func), name or func.__name__)
 
     @classmethod
     def method(cls, func: Callable[[], Any], name: Optional[str] = None) -> None:
-        """ Adds the passed callable as a method. """
-
-        cls._safe_add(func, func.__name__)
+        """Add the passed callable as a method."""
+        cls._safe_add(func, name or func.__name__)
 
     @classmethod
     def class_method(cls, func: Callable[[], Any], name: Optional[str] = None) -> None:
-        """ Adds the passed callable as a classmethod. """
-
-        cls._safe_add(classmethod(func), func.__name__)
+        """Add the passed callable as a classmethod."""
+        cls._safe_add(classmethod(func), name or func.__name__)
 
     @classmethod
     def field(cls, **kwargs) -> None:
-        """ Adds the passed jsonstore field. Must be one of the fields in
-        django-jsonstore.
+        """Add the passed jsonstore field. Must be one of the fields in django-jsonstore.
 
         Accepts exactly one keyword argument, with the name being the desired
         model field name and the value the field instance.
         """
-
         # Force kwargs to be exactly one argument
         if len(kwargs) != 1:
-            raise TypeError("field() takes 1 keyword argument but %d were given" % len(kwargs))
+            raise TypeError(f"field() takes 1 keyword argument but {len(kwargs)} were given")
         name, field = kwargs.popitem()
 
         # Force the field to be one of the jsonstore fields
@@ -179,18 +209,107 @@ class ExtensibleModel(CRUDMixin):
 
         cls._safe_add(field, name)
 
+    @classmethod
+    def foreign_key(
+        cls,
+        field_name: str,
+        to: models.Model,
+        to_field: str = "pk",
+        to_field_type: JSONFieldMixin = IntegerField,
+        related_name: Optional[str] = None,
+    ) -> None:
+        """Add a virtual ForeignKey.
+
+        This works by storing the primary key (or any field passed in the to_field argument)
+        and adding a property that queries the desired model.
+
+        If the foreign model also is an ExtensibleModel, a reverse mapping is also added under
+        the related_name passed as argument, or this model's default related name.
+        """
+
+        id_field_name = f"{field_name}_id"
+        if related_name is None:
+            related_name = cls.Meta.default_related_name
+
+        # Add field to hold key to foreign model
+        id_field = to_field_type()
+        cls.field(**{id_field_name: id_field})
+
+        @property
+        def _virtual_fk(self) -> Optional[models.Model]:
+            id_field_val = getattr(self, id_field_name)
+            if id_field_val:
+                try:
+                    return to.objects.get(**{to_field: id_field_val})
+                except to.DoesNotExist:
+                    # We found a stale foreign key
+                    setattr(self, id_field_name, None)
+                    self.save()
+                    return None
+            else:
+                return None
+
+        @_virtual_fk.setter
+        def _virtual_fk(self, value: Optional[models.Model] = None) -> None:
+            if value is None:
+                id_field_val = None
+            else:
+                id_field_val = getattr(value, to_field)
+            setattr(self, id_field_name, id_field_val)
+
+        # Add property to wrap get/set on foreign model instance
+        cls._safe_add(_virtual_fk, field_name)
+
+        # Add related property on foreign model instance if it provides such an interface
+        if hasattr(to, "_safe_add"):
+
+            def _virtual_related(self) -> models.QuerySet:
+                id_field_val = getattr(self, to_field)
+                return cls.objects.filter(**{id_field_name: id_field_val})
+
+            to.property_(_virtual_related, related_name)
+
+    @classmethod
+    def syncable_fields(cls) -> List[models.Field]:
+        """Collect all fields that can be synced on a model."""
+        return [
+            field
+            for field in cls._meta.fields
+            if (field.editable and not field.auto_created and not field.is_relation)
+        ]
+
+    @classmethod
+    def syncable_fields_choices(cls) -> Tuple[Tuple[str, str]]:
+        """Collect all fields that can be synced on a model."""
+        return tuple(
+            [(field.name, field.verbose_name or field.name) for field in cls.syncable_fields()]
+        )
+
+    @classmethod
+    def syncable_fields_choices_lazy(cls) -> Callable[[], Tuple[Tuple[str, str]]]:
+        """Collect all fields that can be synced on a model."""
+        return lazy(cls.syncable_fields_choices, tuple)
+
+    @classmethod
+    def add_permission(cls, name: str, verbose_name: str):
+        """Dynamically add a new permission to a model."""
+        cls.extra_permissions.append((name, verbose_name))
+
     class Meta:
         abstract = True
 
+
 class PureDjangoModel(object):
-    """ No-op mixin to mark a model as deliberately not using ExtensibleModel """
+    """No-op mixin to mark a model as deliberately not using ExtensibleModel."""
+
     pass
 
 
 class _ExtensibleFormMetaclass(ModelFormMetaclass):
-    def __new__(mcs, name, bases, dct):
-        x = super().__new__(mcs, name, bases, dct)
+    def __new__(cls, name, bases, dct):
+        x = super().__new__(cls, name, bases, dct)
 
+        # Enforce a default for the base layout for forms that o not specify one
         if hasattr(x, "layout"):
             base_layout = x.layout.elements
         else:
@@ -203,7 +322,7 @@ class _ExtensibleFormMetaclass(ModelFormMetaclass):
 
 
 class ExtensibleForm(ModelForm, metaclass=_ExtensibleFormMetaclass):
-    """ Base model for extensible forms
+    """Base model for extensible forms.
 
     This mixin adds functionality which allows
     - apps to add layout nodes to the layout used by django-material
@@ -224,12 +343,68 @@ class ExtensibleForm(ModelForm, metaclass=_ExtensibleFormMetaclass):
 
     @classmethod
     def add_node_to_layout(cls, node: Union[LayoutNode, str]):
-        """
-        Add a node to `layout` attribute
+        """Add a node to `layout` attribute.
 
         :param node: django-material layout node (Fieldset, Row etc.)
         :type node: LayoutNode
         """
-
         cls.base_layout.append(node)
         cls.layout = Layout(*cls.base_layout)
+
+
+class BaseModelAdmin(GuardedModelAdmin, ObjectPermissionsModelAdmin):
+    """A base class for ModelAdmin combining django-guardian and rules."""
+
+    pass
+
+
+class SuccessMessageMixin(ModelFormMixin):
+    success_message: Optional[str] = None
+
+    def form_valid(self, form: BaseForm) -> HttpResponse:
+        if self.success_message:
+            messages.success(self.request, self.success_message)
+        return super().form_valid(form)
+
+
+class AdvancedCreateView(CreateView, SuccessMessageMixin):
+    pass
+
+
+class AdvancedEditView(UpdateView, SuccessMessageMixin):
+    pass
+
+
+class SchoolTermRelatedExtensibleModel(ExtensibleModel):
+    """Add relation to school term."""
+
+    objects = CurrentSiteManagerWithoutMigrations.from_queryset(SchoolTermRelatedQuerySet)()
+
+    school_term = models.ForeignKey(
+        "core.SchoolTerm",
+        on_delete=models.CASCADE,
+        related_name="+",
+        verbose_name=_("Linked school term"),
+        blank=True,
+        null=True,
+    )
+
+    class Meta:
+        abstract = True
+
+
+class SchoolTermRelatedExtensibleForm(ExtensibleForm):
+    """Extensible form for school term related data.
+
+    .. warning::
+        This doesn't automatically include the field `school_term` in `fields` or `layout`,
+        it just sets an initial value.
+    """
+
+    def __init__(self, *args, **kwargs):
+        from aleksis.core.models import SchoolTerm  # noqa
+
+        if "instance" not in kwargs:
+            kwargs["initial"] = {"school_term": SchoolTerm.current}
+
+        super().__init__(*args, **kwargs)
diff --git a/aleksis/core/models.py b/aleksis/core/models.py
index 7e8b7c64bb367854977f1d2bca2eec484580248a..7a2c5bcb8c397b1eeb46315aed1a115029472541 100644
--- a/aleksis/core/models.py
+++ b/aleksis/core/models.py
@@ -1,85 +1,92 @@
+# flake8: noqa: DJ01
+
 from datetime import date, datetime
-from typing import Optional, Iterable, Union, Sequence, List
+from typing import Iterable, List, Optional, Sequence, Union
 
 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.sites.models import Site
+from django.core.exceptions import ValidationError
 from django.db import models
 from django.db.models import QuerySet
 from django.forms.widgets import Media
 from django.urls import reverse
 from django.utils import timezone
+from django.utils.decorators import classproperty
+from django.utils.text import slugify
 from django.utils.translation import gettext_lazy as _
+
+import jsonstore
+from dynamic_preferences.models import PerInstancePreferenceModel
 from image_cropping import ImageCropField, ImageRatioField
 from phonenumber_field.modelfields import PhoneNumberField
 from polymorphic.models import PolymorphicModel
 
-from .mixins import ExtensibleModel, PureDjangoModel
+from .managers import CurrentSiteManagerWithoutMigrations, SchoolTermQuerySet
+from .mixins import ExtensibleModel, PureDjangoModel, SchoolTermRelatedExtensibleModel
 from .tasks import send_notification
-from .util.core_helpers import now_tomorrow
+from .util.core_helpers import get_site_preferences, now_tomorrow
 from .util.model_helpers import ICONS
 
-from constance import config
-
-
-class School(ExtensibleModel):
-    """A school that will have many other objects linked to it.
-    AlekSIS has multi-tenant support by linking all objects to a school,
-    and limiting all features to objects related to the same school as the
-    currently logged-in user.
-    """
-
-    name = models.CharField(verbose_name=_("Name"), max_length=255)
-    name_official = models.CharField(
-        verbose_name=_("Official name"),
-        max_length=255,
-        help_text=_("Official name of the school, e.g. as given by supervisory authority"),
-    )
-
-    logo = ImageCropField(verbose_name=_("School logo"), blank=True, null=True)
-    logo_cropping = ImageRatioField("logo", "600x600", size_warning=True)
-
-    @classmethod
-    def get_default(cls):
-        return cls.objects.first()
-
-    @property
-    def current_term(self):
-        return SchoolTerm.objects.get(current=True)
-
-    class Meta:
-        ordering = ["name", "name_official"]
-        verbose_name = _("School")
-        verbose_name_plural = _("Schools")
+FIELD_CHOICES = (
+    ("BooleanField", _("Boolean (Yes/No)")),
+    ("CharField", _("Text (one line)")),
+    ("DateField", _("Date")),
+    ("DateTimeField", _("Date and time")),
+    ("DecimalField", _("Decimal number")),
+    ("EmailField", _("E-mail address")),
+    ("IntegerField", _("Integer")),
+    ("GenericIPAddressField", _("IP address")),
+    ("NullBooleanField", _("Boolean or empty (Yes/No/Neither)")),
+    ("TextField", _("Text (multi-line)")),
+    ("TimeField", _("Time")),
+    ("URLField", _("URL / Link")),
+)
 
 
 class SchoolTerm(ExtensibleModel):
-    """ Information about a term (limited time frame) that data can
-    be linked to.
-    """
+    """School term model.
 
-    caption = models.CharField(verbose_name=_("Visible caption of the term"), max_length=255)
+    This is used to manage start and end times of a school term and link data to it.
+    """
 
-    date_start = models.DateField(verbose_name=_("Effective start date of term"), null=True)
-    date_end = models.DateField(verbose_name=_("Effective end date of term"), null=True)
+    objects = CurrentSiteManagerWithoutMigrations.from_queryset(SchoolTermQuerySet)()
 
-    current = models.NullBooleanField(default=None, unique=True)
+    name = models.CharField(verbose_name=_("Name"), max_length=255, unique=True)
 
-    def save(self, *args, **kwargs):
-        if self.current is False:
-            self.current = None
-        super().save(*args, **kwargs)
+    date_start = models.DateField(verbose_name=_("Start date"))
+    date_end = models.DateField(verbose_name=_("End date"))
 
     @classmethod
-    def maintain_default_data(cls):
-        if not cls.objects.filter(current=True).exists():
-            if cls.objects.exists():
-                term = cls.objects.latest('date_start')
-                term.current=True
-                term.save()
-            else:
-                cls.objects.create(date_start=date.today(), current=True)
+    def get_current(cls, day: Optional[date] = None):
+        if not day:
+            day = timezone.now().date()
+        try:
+            return cls.objects.on_day(day).first()
+        except SchoolTerm.DoesNotExist:
+            return None
+
+    @classproperty
+    def current(cls):
+        return cls.get_current()
+
+    def clean(self):
+        """Ensure there is only one school term at each point of time."""
+        if self.date_end < self.date_start:
+            raise ValidationError(_("The start date must be earlier than the end date."))
+
+        qs = SchoolTerm.objects.within_dates(self.date_start, self.date_end)
+        if self.pk:
+            qs.exclude(pk=self.pk)
+        if qs.exists():
+            raise ValidationError(
+                _("There is already a school term for this time or a part of this time.")
+            )
+
+    def __str__(self):
+        return self.name
 
     class Meta:
         verbose_name = _("School term")
@@ -87,7 +94,9 @@ class SchoolTerm(ExtensibleModel):
 
 
 class Person(ExtensibleModel):
-    """ A model describing any person related to a school, including, but not
+    """Person model.
+
+    A model describing any person related to a school, including, but not
     limited to, students, teachers and guardians (parents).
     """
 
@@ -95,13 +104,25 @@ class Person(ExtensibleModel):
         ordering = ["last_name", "first_name"]
         verbose_name = _("Person")
         verbose_name_plural = _("Persons")
+        permissions = (
+            ("view_address", _("Can view address")),
+            ("view_contact_details", _("Can view contact details")),
+            ("view_photo", _("Can view photo")),
+            ("view_person_groups", _("Can view persons groups")),
+            ("view_personal_details", _("Can view personal details")),
+        )
 
     icon_ = "person"
 
     SEX_CHOICES = [("f", _("female")), ("m", _("male"))]
 
     user = models.OneToOneField(
-        get_user_model(), on_delete=models.SET_NULL, blank=True, null=True, related_name="person"
+        get_user_model(),
+        on_delete=models.SET_NULL,
+        blank=True,
+        null=True,
+        related_name="person",
+        verbose_name=_("Linked user"),
     )
     is_active = models.BooleanField(verbose_name=_("Is person active?"), default=True)
 
@@ -112,7 +133,7 @@ class Person(ExtensibleModel):
     )
 
     short_name = models.CharField(
-        verbose_name=_("Short name"), max_length=255, blank=True, null=True, unique=True
+        verbose_name=_("Short name"), max_length=255, blank=True, null=True, unique=True  # noqa
     )
 
     street = models.CharField(verbose_name=_("Street"), max_length=255, blank=True)
@@ -132,52 +153,69 @@ class Person(ExtensibleModel):
     photo_cropping = ImageRatioField("photo", "600x800", size_warning=True)
 
     guardians = models.ManyToManyField(
-        "self", verbose_name=_("Guardians / Parents"), symmetrical=False, related_name="children", blank=True
+        "self",
+        verbose_name=_("Guardians / Parents"),
+        symmetrical=False,
+        related_name="children",
+        blank=True,
     )
 
-    primary_group = models.ForeignKey("Group", models.SET_NULL, null=True, blank=True)
-
-    description = models.TextField(verbose_name=_("Description"), blank=True, null=True)
+    primary_group = models.ForeignKey(
+        "Group", models.SET_NULL, null=True, blank=True, verbose_name=_("Primary group")
+    )
 
+    description = models.TextField(verbose_name=_("Description"), blank=True)
 
     def get_absolute_url(self) -> str:
         return reverse("person_by_id", args=[self.id])
 
     @property
     def primary_group_short_name(self) -> Optional[str]:
-        """ Returns the short_name field of the primary
-        group related object.
-        """
-
+        """Return the short_name field of the primary group related object."""
         if self.primary_group:
             return self.primary_group.short_name
 
     @primary_group_short_name.setter
     def primary_group_short_name(self, value: str) -> None:
-        """ Sets the primary group related object by
-        a short name. It uses the first existing group
+        """
+        Set the primary group related object by a short name.
+
+        It uses the first existing group
         with this short name it can find, creating one
         if it can't find one.
         """
-
         group, created = Group.objects.get_or_create(short_name=value, defaults={"name": value})
         self.primary_group = group
 
     @property
     def full_name(self) -> str:
+        """Full name of person in last name, first name order."""
         return f"{self.last_name}, {self.first_name}"
 
     @property
-    def adressing_name(self) -> str:
-        if config.ADRESSING_NAME_FORMAT == "dutch":
-            return f"{self.last_name} {self.first_name}"
-        elif config.ADRESSING_NAME_FORMAT == "english":
+    def addressing_name(self) -> str:
+        """Full name of person in format configured for addressing."""
+        if self.preferences["notification__addressing_name_format"] == "last_first":
             return f"{self.last_name}, {self.first_name}"
-        else:
+        elif self.preferences["notification__addressing_name_format"] == "first_last":
             return f"{self.first_name} {self.last_name}"
 
+    @property
+    def mail_sender(self) -> str:
+        """E-mail sender in "Name <email>" format."""
+        return f'"{self.addressing_name}" <{self.email}>'
+
+    @property
+    def mail_sender_via(self) -> str:
+        """E-mail sender for via addresses, in "Name via Site <email>" format."""
+        site_mail = get_site_preferences()["mail__address"]
+        site_name = get_site_preferences()["general__title"]
+
+        return f'"{self.addressing_name} via {site_name}" <{site_mail}>'
+
     @property
     def age(self):
+        """Age of the person at current time."""
         return self.age_at(timezone.datetime.now().date())
 
     def age_at(self, today):
@@ -203,6 +241,7 @@ class Person(ExtensibleModel):
         for group in self.member_of.union(self.owner_of.all()).all():
             group.save()
 
+        # Select a primary group if none is set
         self.auto_select_primary_group()
 
     def __str__(self) -> str:
@@ -210,38 +249,69 @@ class Person(ExtensibleModel):
 
     @classmethod
     def maintain_default_data(cls):
-        # First, ensure we have an admin user
-        User = get_user_model()
-        if not User.objects.filter(is_superuser=True).exists():
-            admin = User.objects.create_superuser(
-                username='admin',
-                email='root@example.com',
-                password='admin'
+        # Ensure we have an admin user
+        user = get_user_model()
+        if not user.objects.filter(is_superuser=True).exists():
+            admin = user.objects.create_superuser(
+                username="admin", email="root@example.com", password="admin"
             )
             admin.save()
 
-            # Ensure this admin user has a person linked to it
-            person = Person(user=admin)
-            person.save()
-
-    def auto_select_primary_group(self, pattern: Optional[str] = None, force: bool = False) -> None:
-        """ Auto-select the primary group among the groups the person is member of
+    def auto_select_primary_group(
+        self, pattern: Optional[str] = None, field: Optional[str] = None, force: bool = False
+    ) -> None:
+        """Auto-select the primary group among the groups the person is member of.
 
         Uses either the pattern passed as argument, or the pattern configured system-wide.
 
         Does not do anything if either no pattern is defined or the user already has
         a primary group, unless force is True.
         """
-
-        pattern = pattern or config.PRIMARY_GROUP_PATTERN
+        pattern = pattern or get_site_preferences()["account__primary_group_pattern"]
+        field = field or get_site_preferences()["account__primary_group_field"]
 
         if pattern:
             if force or not self.primary_group:
-                self.primary_group = self.member_of.filter(name__regex=pattern).first()
+                self.primary_group = self.member_of.filter(**{f"{field}__regex": pattern}).first()
+
+
+class DummyPerson(Person):
+    """A dummy person that is not stored into the database.
+
+    Used to temporarily inject a Person object into a User.
+    """
+
+    class Meta:
+        managed = False
+        proxy = True
+
+    is_dummy = True
+
+    def save(self, *args, **kwargs):
+        # Do nothing, not even call Model's save(), so this is never persisted
+        pass
+
 
+class AdditionalField(ExtensibleModel):
+    """An additional field that can be linked to a group."""
 
-class Group(ExtensibleModel):
-    """Any kind of group of persons in a school, including, but not limited
+    title = models.CharField(verbose_name=_("Title of field"), max_length=255)
+    field_type = models.CharField(
+        verbose_name=_("Type of field"), choices=FIELD_CHOICES, max_length=50
+    )
+
+    def __str__(self) -> str:
+        return self.title
+
+    class Meta:
+        verbose_name = _("Addtitional field for groups")
+        verbose_name_plural = _("Addtitional fields for groups")
+
+
+class Group(SchoolTermRelatedExtensibleModel):
+    """Group model.
+
+    Any kind of group of persons in a school, including, but not limited
     classes, clubs, and the like.
     """
 
@@ -249,14 +319,34 @@ class Group(ExtensibleModel):
         ordering = ["short_name", "name"]
         verbose_name = _("Group")
         verbose_name_plural = _("Groups")
+        permissions = (
+            ("assign_child_groups_to_groups", _("Can assign child groups to groups")),
+            ("view_group_stats", _("Can view statistics about group.")),
+        )
+        constraints = [
+            models.UniqueConstraint(fields=["school_term", "name"], name="unique_school_term_name"),
+            models.UniqueConstraint(
+                fields=["school_term", "short_name"], name="unique_school_term_short_name"
+            ),
+        ]
 
     icon_ = "group"
 
-    name = models.CharField(verbose_name=_("Long name of group"), max_length=255, unique=True)
-    short_name = models.CharField(verbose_name=_("Short name of group"), max_length=255, unique=True, blank=True, null=True)
+    name = models.CharField(verbose_name=_("Long name"), max_length=255)
+    short_name = models.CharField(
+        verbose_name=_("Short name"), max_length=255, blank=True, null=True  # noqa
+    )
 
-    members = models.ManyToManyField("Person", related_name="member_of", blank=True)
-    owners = models.ManyToManyField("Person", related_name="owner_of", blank=True)
+    members = models.ManyToManyField(
+        "Person",
+        related_name="member_of",
+        blank=True,
+        through="PersonGroupThrough",
+        verbose_name=_("Members"),
+    )
+    owners = models.ManyToManyField(
+        "Person", related_name="owner_of", blank=True, verbose_name=_("Owners")
+    )
 
     parent_groups = models.ManyToManyField(
         "self",
@@ -266,14 +356,24 @@ class Group(ExtensibleModel):
         blank=True,
     )
 
-    type = models.ForeignKey("GroupType", on_delete=models.CASCADE, related_name="type", verbose_name=_("Type of group"), null=True, blank=True)
-
+    group_type = models.ForeignKey(
+        "GroupType",
+        on_delete=models.SET_NULL,
+        related_name="type",
+        verbose_name=_("Type of group"),
+        null=True,
+        blank=True,
+    )
+    additional_fields = models.ManyToManyField(
+        AdditionalField, verbose_name=_("Additional fields"), blank=True
+    )
 
     def get_absolute_url(self) -> str:
         return reverse("group_by_id", args=[self.id])
 
     @property
     def announcement_recipients(self):
+        """Flat list of all members and owners to fulfill announcement API contract."""
         return list(self.members.all()) + list(self.owners.all())
 
     @property
@@ -293,7 +393,10 @@ class Group(ExtensibleModel):
         return stats
 
     def __str__(self) -> str:
-        return "%s (%s)" % (self.name, self.short_name)
+        if self.school_term:
+            return f"{self.name} ({self.short_name}) ({self.school_term})"
+        else:
+            return f"{self.name} ({self.short_name})"
 
     def save(self, *args, **kwargs):
         super().save(*args, **kwargs)
@@ -302,16 +405,40 @@ class Group(ExtensibleModel):
         dj_group, _ = DjangoGroup.objects.get_or_create(name=self.name)
         dj_group.user_set.set(
             list(
-                self.members.filter(user__isnull=False).values_list("user", flat=True).union(
-                    self.owners.filter(user__isnull=False).values_list("user", flat=True)
-                )
+                self.members.filter(user__isnull=False)
+                .values_list("user", flat=True)
+                .union(self.owners.filter(user__isnull=False).values_list("user", flat=True))
             )
         )
         dj_group.save()
 
 
+class PersonGroupThrough(ExtensibleModel):
+    """Through table for many-to-many relationship of group members.
+
+    It does not have any fields on its own; these are generated upon instantiation
+    by inspecting the additional fields selected for the linked group.
+    """
+
+    group = models.ForeignKey(Group, on_delete=models.CASCADE)
+    person = models.ForeignKey(Person, on_delete=models.CASCADE)
+
+    def __init__(self, *args, **kwargs):
+        super().__init__(*args, **kwargs)
+
+        for field in self.group.additional_fields.all():
+            field_class = getattr(jsonstore, field.field_type)
+            field_name = slugify(field.title).replace("-", "_")
+            field_instance = field_class(verbose_name=field.title)
+            setattr(self, field_name, field_instance)
+
+
 class Activity(ExtensibleModel):
-    user = models.ForeignKey("Person", on_delete=models.CASCADE, related_name="activities")
+    """Activity of a user to trace some actions done in AlekSIS in displayable form."""
+
+    user = models.ForeignKey(
+        "Person", on_delete=models.CASCADE, related_name="activities", verbose_name=_("User")
+    )
 
     title = models.CharField(max_length=150, verbose_name=_("Title"))
     description = models.TextField(max_length=500, verbose_name=_("Description"))
@@ -327,8 +454,15 @@ class Activity(ExtensibleModel):
 
 
 class Notification(ExtensibleModel):
+    """Notification to submit to a user."""
+
     sender = models.CharField(max_length=100, verbose_name=_("Sender"))
-    recipient = models.ForeignKey("Person", on_delete=models.CASCADE, related_name="notifications")
+    recipient = models.ForeignKey(
+        "Person",
+        on_delete=models.CASCADE,
+        related_name="notifications",
+        verbose_name=_("Recipient"),
+    )
 
     title = models.CharField(max_length=150, verbose_name=_("Title"))
     description = models.TextField(max_length=500, verbose_name=_("Description"))
@@ -341,6 +475,7 @@ class Notification(ExtensibleModel):
         return str(self.title)
 
     def save(self, **kwargs):
+        super().save(**kwargs)
         if not self.sent:
             send_notification(self.pk, resend=True)
         self.sent = True
@@ -352,23 +487,25 @@ class Notification(ExtensibleModel):
 
 
 class AnnouncementQuerySet(models.QuerySet):
+    """Queryset for announcements providing time-based utility functions."""
+
     def relevant_for(self, obj: Union[models.Model, models.QuerySet]) -> models.QuerySet:
-        """ Get a QuerySet with all announcements relevant for a certain Model (e.g. a Group)
+        """Get all relevant announcements.
+
+        Get a QuerySet with all announcements relevant for a certain Model (e.g. a Group)
         or a set of models in a QuerySet.
         """
-
         if isinstance(obj, models.QuerySet):
             ct = ContentType.objects.get_for_model(obj.model)
-            pks = list(obj.values_list('pk', flat=True))
+            pks = list(obj.values_list("pk", flat=True))
         else:
             ct = ContentType.objects.get_for_model(obj)
             pks = [obj.pk]
 
         return self.filter(recipients__content_type=ct, recipients__recipient_id__in=pks)
 
-    def at_time(self,when: Optional[datetime] = None ) -> models.QuerySet:
-        """ Get all announcements at a certain time """
-
+    def at_time(self, when: Optional[datetime] = None) -> models.QuerySet:
+        """Get all announcements at a certain time."""
         when = when or timezone.datetime.now()
 
         # Get announcements by time
@@ -377,8 +514,7 @@ class AnnouncementQuerySet(models.QuerySet):
         return announcements
 
     def on_date(self, when: Optional[date] = None) -> models.QuerySet:
-        """ Get all announcements at a certain date """
-
+        """Get all announcements at a certain date."""
         when = when or timezone.datetime.now().date()
 
         # Get announcements by time
@@ -387,16 +523,14 @@ class AnnouncementQuerySet(models.QuerySet):
         return announcements
 
     def within_days(self, start: date, stop: date) -> models.QuerySet:
-        """ Get all announcements valid for a set of days """
-
+        """Get all announcements valid for a set of days."""
         # Get announcements
         announcements = self.filter(valid_from__date__lte=stop, valid_until__date__gte=start)
 
         return announcements
 
     def for_person(self, person: Person) -> List:
-        """ Get all announcements for one person """
-
+        """Get all announcements for one person."""
         # Filter by person
         announcements_for_person = []
         for announcement in self:
@@ -407,32 +541,39 @@ class AnnouncementQuerySet(models.QuerySet):
 
 
 class Announcement(ExtensibleModel):
+    """Announcement model.
+
+    Persistent announcement to display to groups or persons in various places during a
+    specific time range.
+    """
+
     objects = models.Manager.from_queryset(AnnouncementQuerySet)()
 
     title = models.CharField(max_length=150, verbose_name=_("Title"))
     description = models.TextField(max_length=500, verbose_name=_("Description"), blank=True)
-    link = models.URLField(blank=True, verbose_name=_("Link"))
+    link = models.URLField(blank=True, verbose_name=_("Link to detailed view"))
 
     valid_from = models.DateTimeField(
         verbose_name=_("Date and time from when to show"), default=timezone.datetime.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
     def recipient_persons(self) -> Sequence[Person]:
-        """ Return a list of Persons this announcement is relevant for """
-
+        """Return a list of Persons this announcement is relevant for."""
         persons = []
         for recipient in self.recipients.all():
             persons += recipient.persons
         return persons
 
     def get_recipients_for_model(self, obj: Union[models.Model]) -> Sequence[models.Model]:
-        """ Get all recipients for this announcement with a special content type (provided through model) """
+        """Get all recipients.
 
+        Get all recipients for this announcement
+        with a special content type (provided through model)
+        """
         ct = ContentType.objects.get_for_model(obj)
         return [r.recipient for r in self.recipients.filter(content_type=ct)]
 
@@ -445,7 +586,18 @@ class Announcement(ExtensibleModel):
 
 
 class AnnouncementRecipient(ExtensibleModel):
-    announcement = models.ForeignKey(Announcement, on_delete=models.CASCADE, related_name="recipients")
+    """Announcement recipient model.
+
+    Generalisation of a recipient for an announcement, used to wrap arbitrary
+    objects that can receive announcements.
+
+    Contract: Objects to serve as recipient have a property announcement_recipients
+    returning a flat list of Person objects.
+    """
+
+    announcement = models.ForeignKey(
+        Announcement, on_delete=models.CASCADE, related_name="recipients"
+    )
 
     content_type = models.ForeignKey(ContentType, on_delete=models.CASCADE)
     recipient_id = models.PositiveIntegerField()
@@ -453,12 +605,11 @@ class AnnouncementRecipient(ExtensibleModel):
 
     @property
     def persons(self) -> Sequence[Person]:
-        """ Return a list of Persons selected by this recipient object
+        """Return a list of Persons selected by this recipient object.
 
         If the recipient is a Person, return that object. If not, it returns the list
         from the announcement_recipients field on the target model.
         """
-
         if isinstance(self.recipient, Person):
             return [self.recipient]
         else:
@@ -473,7 +624,7 @@ class AnnouncementRecipient(ExtensibleModel):
 
 
 class DashboardWidget(PolymorphicModel, PureDjangoModel):
-    """ Base class for dashboard widgets on the index page
+    """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
@@ -481,8 +632,9 @@ class DashboardWidget(PolymorphicModel, PureDjangoModel):
 
     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/
+    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::
 
@@ -509,8 +661,7 @@ class DashboardWidget(PolymorphicModel, PureDjangoModel):
 
     @staticmethod
     def get_media(widgets: Union[QuerySet, Iterable]):
-        """ Return all media required to render the selected widgets. """
-
+        """Return all media required to render the selected widgets."""
         media = Media()
         for widget in widgets:
             media = media + widget.media
@@ -520,12 +671,18 @@ class DashboardWidget(PolymorphicModel, PureDjangoModel):
     media = Media()
 
     title = models.CharField(max_length=150, verbose_name=_("Widget Title"))
-    active = models.BooleanField(blank=True, verbose_name=_("Activate Widget"))
+    active = models.BooleanField(verbose_name=_("Activate Widget"))
 
     def get_context(self):
+        """Get the context dictionary to pass to the widget template."""
         raise NotImplementedError("A widget subclass needs to implement the get_context method.")
 
     def get_template(self):
+        """Get template.
+
+        Get the template to render the widget with. Defaults to the template attribute,
+        but can be overridden to allow more complex template generation scenarios.
+        """
         return self.template
 
     def __str__(self):
@@ -537,21 +694,17 @@ class DashboardWidget(PolymorphicModel, PureDjangoModel):
 
 
 class CustomMenu(ExtensibleModel):
-    id = models.CharField(max_length=100, verbose_name=_("Menu ID"), primary_key=True)
-    name = models.CharField(max_length=150, verbose_name=_("Menu name"))
+    """A custom menu to display in the footer."""
+
+    name = models.CharField(max_length=100, verbose_name=_("Menu ID"), unique=True)
 
     def __str__(self):
         return self.name if self.name != "" else self.id
 
-    @classmethod
-    def maintain_default_data(cls):
-        menus = ["footer"]
-        for menu in menus:
-            cls.get_default(menu)
-
     @classmethod
     def get_default(cls, name):
-        menu, _ = cls.objects.get_or_create(id=name, defaults={"name": name})
+        """Get a menu by name or create if it does not exist."""
+        menu, _ = cls.objects.get_or_create(name=name)
         return menu
 
     class Meta:
@@ -560,26 +713,80 @@ class CustomMenu(ExtensibleModel):
 
 
 class CustomMenuItem(ExtensibleModel):
+    """Single item in a custom menu."""
+
     menu = models.ForeignKey(
         CustomMenu, models.CASCADE, verbose_name=_("Menu"), related_name="items"
     )
     name = models.CharField(max_length=150, verbose_name=_("Name"))
     url = models.URLField(verbose_name=_("Link"))
-    icon = models.CharField(
-        max_length=50, blank=True, null=True, choices=ICONS, verbose_name=_("Icon")
-    )
+    icon = models.CharField(max_length=50, blank=True, choices=ICONS, verbose_name=_("Icon"))
 
     def __str__(self):
-        return "[{}] {}".format(self.menu, self.name)
+        return f"[{self.menu}] {self.name}"
 
     class Meta:
         verbose_name = _("Custom menu item")
         verbose_name_plural = _("Custom menu items")
 
+
 class GroupType(ExtensibleModel):
+    """Group type model.
+
+    Descriptive type of a group; used to tag groups and for apps to distinguish
+    how to display or handle a certain group.
+    """
+
     name = models.CharField(verbose_name=_("Title of type"), max_length=50)
     description = models.CharField(verbose_name=_("Description"), max_length=500)
 
+    def __str__(self) -> str:
+        return self.name
+
     class Meta:
         verbose_name = _("Group type")
         verbose_name_plural = _("Group types")
+
+
+class GlobalPermissions(ExtensibleModel):
+    """Container for global permissions."""
+
+    class Meta:
+        managed = False
+        permissions = (
+            ("view_system_status", _("Can view system status")),
+            ("link_persons_accounts", _("Can link persons to accounts")),
+            ("manage_data", _("Can manage data")),
+            ("impersonate", _("Can impersonate")),
+            ("search", _("Can use search")),
+            ("change_site_preferences", _("Can change site preferences")),
+            ("change_person_preferences", _("Can change person preferences")),
+            ("change_group_preferences", _("Can change group preferences")),
+        )
+
+
+class SitePreferenceModel(PerInstancePreferenceModel, PureDjangoModel):
+    """Preference model to hold pereferences valid for a site."""
+
+    instance = models.ForeignKey(Site, on_delete=models.CASCADE)
+
+    class Meta:
+        app_label = "core"
+
+
+class PersonPreferenceModel(PerInstancePreferenceModel, PureDjangoModel):
+    """Preference model to hold pereferences valid for a person."""
+
+    instance = models.ForeignKey(Person, on_delete=models.CASCADE)
+
+    class Meta:
+        app_label = "core"
+
+
+class GroupPreferenceModel(PerInstancePreferenceModel, PureDjangoModel):
+    """Preference model to hold pereferences valid for members of a group."""
+
+    instance = models.ForeignKey(Group, on_delete=models.CASCADE)
+
+    class Meta:
+        app_label = "core"
diff --git a/aleksis/core/preferences.py b/aleksis/core/preferences.py
new file mode 100644
index 0000000000000000000000000000000000000000..6693aff1fba605180b6f83e7741fe1cb005267fe
--- /dev/null
+++ b/aleksis/core/preferences.py
@@ -0,0 +1,180 @@
+from django.conf import settings
+from django.forms import EmailField, ImageField, URLField
+from django.utils.translation import gettext_lazy as _
+
+from dynamic_preferences.preferences import Section
+from dynamic_preferences.types import ChoicePreference, FilePreference, StringPreference
+
+from .models import Person
+from .registries import person_preferences_registry, site_preferences_registry
+from .util.notifications import get_notification_choices_lazy
+
+general = Section("general")
+school = Section("school")
+theme = Section("theme")
+mail = Section("mail")
+notification = Section("notification")
+footer = Section("footer")
+account = Section("account")
+
+
+@site_preferences_registry.register
+class SiteTitle(StringPreference):
+    section = general
+    name = "title"
+    default = "AlekSIS"
+    required = False
+    verbose_name = _("Site title")
+
+
+@site_preferences_registry.register
+class SiteDescription(StringPreference):
+    section = general
+    name = "description"
+    default = "The Free School Information System"
+    required = False
+    verbose_name = _("Site description")
+
+
+@site_preferences_registry.register
+class ColourPrimary(StringPreference):
+    section = theme
+    name = "primary"
+    default = "#0d5eaf"
+    required = False
+    verbose_name = _("Primary colour")
+
+
+@site_preferences_registry.register
+class ColourSecondary(StringPreference):
+    section = theme
+    name = "secondary"
+    default = "#0d5eaf"
+    required = False
+    verbose_name = _("Secondary colour")
+
+
+@site_preferences_registry.register
+class Logo(FilePreference):
+    section = theme
+    field_class = ImageField
+    name = "logo"
+    verbose_name = _("Logo")
+
+
+@site_preferences_registry.register
+class Favicon(FilePreference):
+    section = theme
+    field_class = ImageField
+    name = "favicon"
+    verbose_name = _("Favicon")
+
+
+@site_preferences_registry.register
+class PWAIcon(FilePreference):
+    section = theme
+    field_class = ImageField
+    name = "pwa_icon"
+    verbose_name = _("PWA-Icon")
+
+
+@site_preferences_registry.register
+class MailOutName(StringPreference):
+    section = mail
+    name = "name"
+    default = "AlekSIS"
+    required = False
+    verbose_name = _("Mail out name")
+
+
+@site_preferences_registry.register
+class MailOut(StringPreference):
+    section = mail
+    name = "address"
+    default = settings.DEFAULT_FROM_EMAIL
+    required = False
+    verbose_name = _("Mail out address")
+    field_class = EmailField
+
+
+@site_preferences_registry.register
+class PrivacyURL(StringPreference):
+    section = footer
+    name = "privacy_url"
+    default = ""
+    required = False
+    verbose_name = _("Link to privacy policy")
+    field_class = URLField
+
+
+@site_preferences_registry.register
+class ImprintURL(StringPreference):
+    section = footer
+    name = "imprint_url"
+    default = ""
+    required = False
+    verbose_name = _("Link to imprint")
+    field_class = URLField
+
+
+@person_preferences_registry.register
+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"),
+    )
+
+
+@person_preferences_registry.register
+class NotificationChannels(ChoicePreference):
+    # FIXME should be a MultipleChoicePreference
+    section = notification
+    name = "channels"
+    default = "email"
+    required = False
+    verbose_name = _("Channels to use for notifications")
+    choices = get_notification_choices_lazy()
+
+
+@site_preferences_registry.register
+class PrimaryGroupPattern(StringPreference):
+    section = account
+    name = "primary_group_pattern"
+    default = ""
+    required = False
+    verbose_name = _("Regular expression to match primary group, e.g. '^Class .*'")
+
+
+@site_preferences_registry.register
+class PrimaryGroupField(ChoicePreference):
+    section = account
+    name = "primary_group_field"
+    default = "name"
+    required = False
+    verbose_name = _("Field on person to match primary group against")
+
+    def get_choices(self):
+        return Person.syncable_fields_choices()
+
+
+@site_preferences_registry.register
+class SchoolName(StringPreference):
+    section = school
+    name = "name"
+    default = ""
+    required = False
+    verbose_name = _("Display name of the school")
+
+
+@site_preferences_registry.register
+class SchoolNameOfficial(StringPreference):
+    section = school
+    name = "name_official"
+    default = ""
+    required = False
+    verbose_name = _("Official name of the school, e.g. as given by supervisory authority")
diff --git a/aleksis/core/registries.py b/aleksis/core/registries.py
new file mode 100644
index 0000000000000000000000000000000000000000..bccc9e57590d113641ac14534a1bfaabbcc5e1e0
--- /dev/null
+++ b/aleksis/core/registries.py
@@ -0,0 +1,26 @@
+"""Custom registries for some preference containers."""
+
+from dynamic_preferences.registries import PerInstancePreferenceRegistry
+
+
+class SitePreferenceRegistry(PerInstancePreferenceRegistry):
+    """Registry for preferences valid for a site."""
+
+    pass
+
+
+class PersonPreferenceRegistry(PerInstancePreferenceRegistry):
+    """Registry for preferences valid for a person."""
+
+    pass
+
+
+class GroupPreferenceRegistry(PerInstancePreferenceRegistry):
+    """Registry for preferences valid for members of a group."""
+
+    pass
+
+
+site_preferences_registry = SitePreferenceRegistry()
+person_preferences_registry = PersonPreferenceRegistry()
+group_preferences_registry = GroupPreferenceRegistry()
diff --git a/aleksis/core/rules.py b/aleksis/core/rules.py
new file mode 100644
index 0000000000000000000000000000000000000000..18c0edae65ddd985b4f26f2bd52fac1f664c63f2
--- /dev/null
+++ b/aleksis/core/rules.py
@@ -0,0 +1,280 @@
+from rules import add_perm, always_allow
+
+from .models import AdditionalField, Announcement, Group, GroupType, Person
+from .util.predicates import (
+    has_any_object,
+    has_global_perm,
+    has_object_perm,
+    has_person,
+    is_current_person,
+    is_group_owner,
+    is_notification_recipient,
+)
+
+add_perm("core", always_allow)
+
+# View dashboard
+add_perm("core.view_dashboard", has_person)
+
+# Use search
+search_predicate = has_person & has_global_perm("core.search")
+add_perm("core.search", search_predicate)
+
+# View persons
+view_persons_predicate = has_person & (
+    has_global_perm("core.view_person") | has_any_object("core.view_person", Person)
+)
+add_perm("core.view_persons", view_persons_predicate)
+
+# View person
+view_person_predicate = has_person & (
+    has_global_perm("core.view_person") | has_object_perm("core.view_person") | is_current_person
+)
+add_perm("core.view_person", view_person_predicate)
+
+# View person address
+view_address_predicate = has_person & (
+    has_global_perm("core.view_address") | has_object_perm("core.view_address") | is_current_person
+)
+add_perm("core.view_address", view_address_predicate)
+
+# View person contact details
+view_contact_details_predicate = has_person & (
+    has_global_perm("core.view_contact_details")
+    | has_object_perm("core.view_contact_details")
+    | is_current_person
+)
+add_perm("core.view_contact_details", view_contact_details_predicate)
+
+# View person photo
+view_photo_predicate = has_person & (
+    has_global_perm("core.view_photo") | has_object_perm("core.view_photo") | is_current_person
+)
+add_perm("core.view_photo", view_photo_predicate)
+
+# View persons groups
+view_groups_predicate = has_person & (
+    has_global_perm("core.view_person_groups")
+    | has_object_perm("core.view_person_groups")
+    | is_current_person
+)
+add_perm("core.view_person_groups", view_groups_predicate)
+
+# Edit person
+edit_person_predicate = has_person & (
+    has_global_perm("core.change_person") | has_object_perm("core.change_person")
+)
+add_perm("core.edit_person", edit_person_predicate)
+
+# Delete person
+delete_person_predicate = has_person & (
+    has_global_perm("core.delete_person") | has_object_perm("core.delete_person")
+)
+add_perm("core.delete_person", delete_person_predicate)
+
+# Link persons with accounts
+link_persons_accounts_predicate = has_person & has_global_perm("core.link_persons_accounts")
+add_perm("core.link_persons_accounts", link_persons_accounts_predicate)
+
+# View groups
+view_groups_predicate = has_person & (
+    has_global_perm("core.view_group") | has_any_object("core.view_group", Group)
+)
+add_perm("core.view_groups", view_groups_predicate)
+
+# View group
+view_group_predicate = has_person & (
+    has_global_perm("core.view_group") | has_object_perm("core.view_group")
+)
+add_perm("core.view_group", view_group_predicate)
+
+# Edit group
+edit_group_predicate = has_person & (
+    has_global_perm("core.change_group") | has_object_perm("core.change_group")
+)
+add_perm("core.edit_group", edit_group_predicate)
+
+# Delete group
+delete_group_predicate = has_person & (
+    has_global_perm("core.delete_group") | has_object_perm("core.delete_group")
+)
+add_perm("core.delete_group", delete_group_predicate)
+
+# Assign child groups to groups
+assign_child_groups_to_groups_predicate = has_person & has_global_perm(
+    "core.assign_child_groups_to_groups"
+)
+add_perm("core.assign_child_groups_to_groups", assign_child_groups_to_groups_predicate)
+
+# Edit school information
+edit_school_information_predicate = has_person & has_global_perm("core.change_school")
+add_perm("core.edit_school_information", edit_school_information_predicate)
+
+# Manage data
+manage_data_predicate = has_person & has_global_perm("core.manage_data")
+add_perm("core.manage_data", manage_data_predicate)
+
+# Mark notification as read
+mark_notification_as_read_predicate = has_person & is_notification_recipient
+add_perm("core.mark_notification_as_read", mark_notification_as_read_predicate)
+
+# View announcements
+view_announcements_predicate = has_person & (
+    has_global_perm("core.view_announcement")
+    | has_any_object("core.view_announcement", Announcement)
+)
+add_perm("core.view_announcements", view_announcements_predicate)
+
+# Create or edit announcement
+create_or_edit_announcement_predicate = has_person & (
+    has_global_perm("core.add_announcement")
+    & (has_global_perm("core.change_announcement") | has_object_perm("core.change_announcement"))
+)
+add_perm("core.create_or_edit_announcement", create_or_edit_announcement_predicate)
+
+# Delete announcement
+delete_announcement_predicate = has_person & (
+    has_global_perm("core.delete_announcement") | has_object_perm("core.delete_announcement")
+)
+add_perm("core.delete_announcement", delete_announcement_predicate)
+
+# Use impersonate
+impersonate_predicate = has_person & has_global_perm("core.impersonate")
+add_perm("core.impersonate", impersonate_predicate)
+
+# View system status
+view_system_status_predicate = has_person & has_global_perm("core.view_system_status")
+add_perm("core.view_system_status", view_system_status_predicate)
+
+# View people menu (persons + objects)
+add_perm(
+    "core.view_people_menu",
+    has_person
+    & (
+        view_persons_predicate
+        | view_groups_predicate
+        | link_persons_accounts_predicate
+        | assign_child_groups_to_groups_predicate
+    ),
+)
+
+# View person personal details
+view_personal_details_predicate = has_person & (
+    has_global_perm("core.view_personal_details")
+    | has_object_perm("core.view_personal_details")
+    | is_current_person
+)
+add_perm("core.view_personal_details", view_personal_details_predicate)
+
+# Change site preferences
+change_site_preferences = has_person & (
+    has_global_perm("core.change_site_preferences")
+    | has_object_perm("core.change_site_preferences")
+)
+add_perm("core.change_site_preferences", change_site_preferences)
+
+# Change person preferences
+change_person_preferences = has_person & (
+    has_global_perm("core.change_person_preferences")
+    | has_object_perm("core.change_person_preferences")
+    | is_current_person
+)
+add_perm("core.change_person_preferences", change_person_preferences)
+
+# Change group preferences
+change_group_preferences = has_person & (
+    has_global_perm("core.change_group_preferences")
+    | has_object_perm("core.change_group_preferences")
+    | is_group_owner
+)
+add_perm("core.change_group_preferences", change_group_preferences)
+
+
+# Edit additional field
+change_additional_field_predicate = has_person & (
+    has_global_perm("core.change_additionalfield") | has_object_perm("core.change_additionalfield")
+)
+add_perm("core.change_additionalfield", change_additional_field_predicate)
+
+# Edit additional field
+create_additional_field_predicate = has_person & (
+    has_global_perm("core.create_additionalfield") | has_object_perm("core.create_additionalfield")
+)
+add_perm("core.create_additionalfield", create_additional_field_predicate)
+
+
+# Delete additional field
+delete_additional_field_predicate = has_person & (
+    has_global_perm("core.delete_additionalfield") | has_object_perm("core.delete_additionalfield")
+)
+add_perm("core.delete_additionalfield", delete_additional_field_predicate)
+
+# View additional fields
+view_additional_field_predicate = has_person & (
+    has_global_perm("core.view_additionalfield")
+    | has_any_object("core.view_additionalfield", AdditionalField)
+)
+add_perm("core.view_additionalfield", view_additional_field_predicate)
+
+# Edit group type
+change_group_type_predicate = has_person & (
+    has_global_perm("core.change_grouptype") | has_object_perm("core.change_grouptype")
+)
+add_perm("core.edit_grouptype", 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")
+)
+add_perm("core.create_grouptype", create_group_type_predicate)
+
+
+# Delete group type
+delete_group_type_predicate = has_person & (
+    has_global_perm("core.delete_grouptype") | has_object_perm("core.delete_grouptype")
+)
+add_perm("core.delete_grouptype", delete_group_type_predicate)
+
+# View group types
+view_group_type_predicate = has_person & (
+    has_global_perm("core.view_grouptype") | has_any_object("core.view_grouptype", GroupType)
+)
+add_perm("core.view_grouptype", view_group_type_predicate)
+
+# Create person
+create_person_predicate = has_person & (
+    has_global_perm("core.create_person") | has_object_perm("core.create_person")
+)
+add_perm("core.create_person", create_person_predicate)
+
+# Create group
+create_group_predicate = has_person & (
+    has_global_perm("core.create_group") | has_object_perm("core.create_group")
+)
+add_perm("core.create_group", create_group_predicate)
+
+# School years
+view_school_term_predicate = has_person & has_global_perm("core.view_schoolterm")
+add_perm("core.view_schoolterm", view_school_term_predicate)
+
+create_school_term_predicate = has_person & has_global_perm("core.add_schoolterm")
+add_perm("core.create_schoolterm", create_school_term_predicate)
+
+edit_school_term_predicate = has_person & has_global_perm("core.change_schoolterm")
+add_perm("core.edit_schoolterm", edit_school_term_predicate)
+
+# View admin menu
+view_admin_menu_predicate = has_person & (
+    manage_data_predicate
+    | view_school_term_predicate
+    | impersonate_predicate
+    | view_system_status_predicate
+    | view_announcements_predicate
+)
+add_perm("core.view_admin_menu", view_admin_menu_predicate)
+
+# View group stats
+view_group_stats_predicate = has_person & (
+    has_global_perm("core.view_group_stats") | has_object_perm("core.view_group_stats")
+)
+add_perm("core.view_group_stats", view_group_stats_predicate)
diff --git a/aleksis/core/search_indexes.py b/aleksis/core/search_indexes.py
index 5828e0c52391423cc6dd0bad43f6a15310a85e1f..7583a774eaadebb1cbfdb35d08dc0ddcfb355eea 100644
--- a/aleksis/core/search_indexes.py
+++ b/aleksis/core/search_indexes.py
@@ -1,10 +1,14 @@
-from .models import Person, Group
+from .models import Group, Person
 from .util.search import Indexable, SearchIndex
 
 
 class PersonIndex(SearchIndex, Indexable):
+    """Haystack index for searching persons."""
+
     model = Person
 
 
 class GroupIndex(SearchIndex, Indexable):
+    """Haystack index for searching groups."""
+
     model = Group
diff --git a/aleksis/core/settings.py b/aleksis/core/settings.py
index 81c00f57599429b58494b7e38fb5a6f5651fdbb1..ad5d7077eba8bccb0981f70b2ea7fffce7c9474c 100644
--- a/aleksis/core/settings.py
+++ b/aleksis/core/settings.py
@@ -1,17 +1,17 @@
 import os
-import sys
 from glob import glob
-from importlib import import_module
 
-from django.apps import apps
 from django.utils.translation import gettext_lazy as _
-from calendarweek.django import i18n_day_name_choices_lazy
 
 from dynaconf import LazySettings
-from easy_thumbnails.conf import Settings as thumbnail_settings
+from easy_thumbnails.conf import settings as thumbnail_settings
 
-from .util.core_helpers import get_app_packages, lazy_config, merge_app_settings
-from .util.notifications import get_notification_choices_lazy
+from .util.core_helpers import (
+    get_app_packages,
+    lazy_get_favicon_url,
+    lazy_preference,
+    merge_app_settings,
+)
 
 ENVVAR_PREFIX_FOR_DYNACONF = "ALEKSIS"
 DIRS_FOR_DYNACONF = ["/etc/aleksis"]
@@ -30,6 +30,8 @@ _settings = LazySettings(
 # Build paths inside the project like this: os.path.join(BASE_DIR, ...)
 BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
 
+SILENCED_SYSTEM_CHECKS = []
+
 # SECURITY WARNING: keep the secret key used in production secret!
 SECRET_KEY = _settings.get("secret_key", "DoNotUseInProduction")
 
@@ -55,6 +57,8 @@ INSTALLED_APPS = [
     "django.contrib.sites",
     "django.contrib.staticfiles",
     "django.contrib.humanize",
+    "guardian",
+    "rules.apps.AutodiscoverRulesConfig",
     "haystack",
     "polymorphic",
     "django_global_request",
@@ -62,8 +66,6 @@ INSTALLED_APPS = [
     "settings_context_processor",
     "sass_processor",
     "easyaudit",
-    "constance",
-    "constance.backends.database",
     "django_any_js",
     "django_yarnpkg",
     "django_tables2",
@@ -71,6 +73,7 @@ INSTALLED_APPS = [
     "image_cropping",
     "maintenance_mode",
     "menu_generator",
+    "reversion",
     "phonenumber_field",
     "debug_toolbar",
     "django_select2",
@@ -82,6 +85,13 @@ INSTALLED_APPS = [
     "django_otp",
     "otp_yubikey",
     "aleksis.core",
+    "health_check",
+    "health_check.db",
+    "health_check.cache",
+    "health_check.storage",
+    "health_check.contrib.psutil",
+    "dynamic_preferences",
+    "dynamic_preferences.users.apps.UserPreferencesConfig",
     "impersonate",
     "two_factor",
     "material",
@@ -90,6 +100,7 @@ INSTALLED_APPS = [
     "django_js_reverse",
     "colorfield",
     "django_bleach",
+    "favicon",
 ]
 
 merge_app_settings("INSTALLED_APPS", INSTALLED_APPS, True)
@@ -109,6 +120,7 @@ MIDDLEWARE = [
     "django.middleware.locale.LocaleMiddleware",
     "django.middleware.http.ConditionalGetMiddleware",
     "django_global_request.middleware.GlobalRequestMiddleware",
+    "django.contrib.sites.middleware.CurrentSiteMiddleware",
     "django.middleware.common.CommonMiddleware",
     "django.middleware.csrf.CsrfViewMiddleware",
     "django.contrib.auth.middleware.AuthenticationMiddleware",
@@ -119,6 +131,7 @@ MIDDLEWARE = [
     "django.middleware.clickjacking.XFrameOptionsMiddleware",
     "easyaudit.middleware.easyaudit.EasyAuditMiddleware",
     "maintenance_mode.middleware.MaintenanceModeMiddleware",
+    "aleksis.core.util.middlewares.EnsurePersonMiddleware",
     #    'django.middleware.cache.FetchFromCacheMiddleware'
 ]
 
@@ -137,7 +150,7 @@ TEMPLATES = [
                 "django.contrib.messages.context_processors.messages",
                 "maintenance_mode.context_processors.maintenance_mode",
                 "settings_context_processor.context_processors.settings",
-                "constance.context_processors.config",
+                "dynamic_preferences.processors.global_preferences",
                 "aleksis.core.util.core_helpers.custom_information_processor",
             ],
         },
@@ -194,7 +207,13 @@ AUTHENTICATION_BACKENDS = []
 if _settings.get("ldap.uri", None):
     # LDAP dependencies are not necessarily installed, so import them here
     import ldap  # noqa
-    from django_auth_ldap.config import LDAPSearch, NestedGroupOfNamesType, NestedGroupOfUniqueNamesType, PosixGroupType  # noqa
+    from django_auth_ldap.config import (
+        LDAPSearch,
+        LDAPSearchUnion,
+        NestedGroupOfNamesType,
+        NestedGroupOfUniqueNamesType,
+        PosixGroupType,
+    )  # noqa
 
     # Enable Django's integration to LDAP
     AUTHENTICATION_BACKENDS.append("django_auth_ldap.backend.LDAPBackend")
@@ -206,26 +225,44 @@ if _settings.get("ldap.uri", None):
         AUTH_LDAP_BIND_DN = _settings.get("ldap.bind.dn")
         AUTH_LDAP_BIND_PASSWORD = _settings.get("ldap.bind.password")
 
+    # The TOML config might contain either one table or an array of tables
+    _AUTH_LDAP_USER_SETTINGS = _settings.get("ldap.users.search")
+    if not isinstance(_AUTH_LDAP_USER_SETTINGS, list):
+        _AUTH_LDAP_USER_SETTINGS = [_AUTH_LDAP_USER_SETTINGS]
+
     # Search attributes to find users by username
-    AUTH_LDAP_USER_SEARCH = LDAPSearch(
-        _settings.get("ldap.users.base"),
-        ldap.SCOPE_SUBTREE,
-        _settings.get("ldap.users.filter", "(uid=%(user)s)"),
+    AUTH_LDAP_USER_SEARCH = LDAPSearchUnion(
+        *[
+            LDAPSearch(entry["base"], ldap.SCOPE_SUBTREE, entry.get("filter", "(uid=%(user)s)"),)
+            for entry in _AUTH_LDAP_USER_SETTINGS
+        ]
     )
 
     # Mapping of LDAP attributes to Django model fields
     AUTH_LDAP_USER_ATTR_MAP = {
-        "first_name": _settings.get("ldap.map.first_name", "givenName"),
-        "last_name": _settings.get("ldap.map.last_name", "sn"),
-        "email": _settings.get("ldap.map.email", "mail"),
+        "first_name": _settings.get("ldap.users.map.first_name", "givenName"),
+        "last_name": _settings.get("ldap.users.map.last_name", "sn"),
+        "email": _settings.get("ldap.users.map.email", "mail"),
     }
 
     # Discover flags by LDAP groups
-    if _settings.get("ldap.groups.base", None):
-        AUTH_LDAP_GROUP_SEARCH = LDAPSearch(
-            _settings.get("ldap.groups.base"),
-            ldap.SCOPE_SUBTREE,
-            _settings.get("ldap.groups.filter", "(objectClass=%s)" % _settings.get("ldap.groups.type", "groupOfNames")),
+    if _settings.get("ldap.groups.search", None):
+        group_type = _settings.get("ldap.groups.type", "groupOfNames")
+
+        # The TOML config might contain either one table or an array of tables
+        _AUTH_LDAP_GROUP_SETTINGS = _settings.get("ldap.groups.search")
+        if not isinstance(_AUTH_LDAP_GROUP_SETTINGS, list):
+            _AUTH_LDAP_GROUP_SETTINGS = [_AUTH_LDAP_GROUP_SETTINGS]
+
+        AUTH_LDAP_GROUP_SEARCH = LDAPSearchUnion(
+            *[
+                LDAPSearch(
+                    entry["base"],
+                    ldap.SCOPE_SUBTREE,
+                    entry.get("filter", f"(objectClass={group_type})"),
+                )
+                for entry in _AUTH_LDAP_GROUP_SETTINGS
+            ]
         )
 
         _group_type = _settings.get("ldap.groups.type", "groupOfNames").lower()
@@ -236,16 +273,20 @@ if _settings.get("ldap.uri", None):
         elif _group_type == "posixgroup":
             AUTH_LDAP_GROUP_TYPE = PosixGroupType()
 
-        AUTH_LDAP_USER_FLAGS_BY_GROUP = {
-        }
+        AUTH_LDAP_USER_FLAGS_BY_GROUP = {}
         for _flag in ["is_active", "is_staff", "is_superuser"]:
-            _dn = _settings.get("ldap.groups.flags.%s" % _flag, None)
+            _dn = _settings.get(f"ldap.groups.flags.{_flag}", None)
             if _dn:
                 AUTH_LDAP_USER_FLAGS_BY_GROUP[_flag] = _dn
 
         # Backend admin requires superusers to also be staff members
-        if "is_superuser" in AUTH_LDAP_USER_FLAGS_BY_GROUP and "is_staff" not in AUTH_LDAP_USER_FLAGS_BY_GROUP:
-            AUTH_LDAP_USER_FLAGS_BY_GROUP["is_staff"] = AUTH_LDAP_USER_FLAGS_BY_GROUP["is_superuser"]
+        if (
+            "is_superuser" in AUTH_LDAP_USER_FLAGS_BY_GROUP
+            and "is_staff" not in AUTH_LDAP_USER_FLAGS_BY_GROUP
+        ):
+            AUTH_LDAP_USER_FLAGS_BY_GROUP["is_staff"] = AUTH_LDAP_USER_FLAGS_BY_GROUP[
+                "is_superuser"
+            ]
 
 # Add ModelBckend last so all other backends get a chance
 # to verify passwords first
@@ -255,8 +296,10 @@ AUTHENTICATION_BACKENDS.append("django.contrib.auth.backends.ModelBackend")
 # https://docs.djangoproject.com/en/2.1/topics/i18n/
 
 LANGUAGES = [
-    ("de", _("German")),
     ("en", _("English")),
+    ("de", _("German")),
+    ("fr", _("French")),
+    ("nb", _("Norsk (bokmål)")),
 ]
 LANGUAGE_CODE = _settings.get("l10n.lang", "en")
 TIME_ZONE = _settings.get("l10n.tz", "UTC")
@@ -305,7 +348,10 @@ ANY_JS = {
         "css_url": JS_URL + "/material-design-icons-iconfont/dist/material-design-icons.css"
     },
     "paper-css": {"css_url": JS_URL + "/paper-css/paper.min.css"},
-    "select2-materialize": {"css_url": JS_URL + "/select2-materialize/select2-materialize.css", "js_url": JS_URL + "/select2-materialize/index.js"},
+    "select2-materialize": {
+        "css_url": JS_URL + "/select2-materialize/select2-materialize.css",
+        "js_url": JS_URL + "/select2-materialize/index.js",
+    },
 }
 
 merge_app_settings("ANY_JS", ANY_JS, True)
@@ -314,7 +360,7 @@ SASS_PROCESSOR_ENABLED = True
 SASS_PROCESSOR_AUTO_INCLUDE = False
 SASS_PROCESSOR_CUSTOM_FUNCTIONS = {
     "get-colour": "aleksis.core.util.sass_helpers.get_colour",
-    "get-config": "aleksis.core.util.sass_helpers.get_config",
+    "get-preference": "aleksis.core.util.sass_helpers.get_preference",
 }
 SASS_PROCESSOR_INCLUDE_DIRS = [
     _settings.get("materialize.sass_path", JS_ROOT + "/materialize-css/sass/"),
@@ -336,71 +382,16 @@ if _settings.get("mail.server.host", None):
         EMAIL_HOST_USER = _settings.get("mail.server.user")
         EMAIL_HOST_PASSWORD = _settings.get("mail.server.password")
 
-TEMPLATED_EMAIL_BACKEND = 'templated_email.backends.vanilla_django'
+TEMPLATED_EMAIL_BACKEND = "templated_email.backends.vanilla_django"
 TEMPLATED_EMAIL_AUTO_PLAIN = True
 
 
 TEMPLATE_VISIBLE_SETTINGS = ["ADMINS", "DEBUG"]
 
-CONSTANCE_BACKEND = "constance.backends.database.DatabaseBackend"
-CONSTANCE_ADDITIONAL_FIELDS = {
-    "char_field": ["django.forms.CharField", {}],
-    "image_field": ["django.forms.ImageField", {}],
-    "email_field": ["django.forms.EmailField", {}],
-    "url_field": ["django.forms.URLField", {}],
-    "integer_field": ["django.forms.IntegerField", {}],
-    "password_field": ["django.forms.CharField", {
-        'widget': 'django.forms.PasswordInput',
-    }],
-    "adressing-select": ['django.forms.fields.ChoiceField', {
-        'widget': 'django.forms.Select',
-        'choices': ((None, "-----"),
-                    # ("german", _("<first name>") + " " + _("<last name>")),
-                    # ("english", _("<last name>") + ", " + _("<first name>")),
-                    # ("netherlands", _("<last name>") + " " + _("<first name>")),
-                    ("german", "John Doe"),
-                    ("english", "Doe, John"),
-                    ("dutch", "Doe John"),
-                    )
-    }],
-    "notifications-select": ["django.forms.fields.MultipleChoiceField", {
-        "widget": "django.forms.CheckboxSelectMultiple",
-        "choices": get_notification_choices_lazy,
-    }],
-    "weekday_field": ["django.forms.fields.ChoiceField", {
-        'widget': 'django.forms.Select',
-        "choices":  i18n_day_name_choices_lazy
-    }],
-    "colour_field": ["django.forms.CharField", {
-        "widget": "colorfield.widgets.ColorWidget"
-    }],
-}
-CONSTANCE_CONFIG = {
-    "SITE_TITLE": ("AlekSIS", _("Site title"), "char_field"),
-    "SITE_DESCRIPTION": ("The Free School Information System", _("Site description")),
-    "COLOUR_PRIMARY": ("#0d5eaf", _("Primary colour"), "colour_field"),
-    "COLOUR_SECONDARY": ("#0d5eaf", _("Secondary colour"), "colour_field"),
-    "MAIL_OUT_NAME": ("AlekSIS", _("Mail out name")),
-    "MAIL_OUT": (DEFAULT_FROM_EMAIL, _("Mail out address"), "email_field"),
-    "PRIVACY_URL": ("", _("Link to privacy policy"), "url_field"),
-    "IMPRINT_URL": ("", _("Link to imprint"), "url_field"),
-    "ADRESSING_NAME_FORMAT": ("german", _("Name format of adresses"), "adressing-select"),
-    "NOTIFICATION_CHANNELS": (["email"], _("Channels to allow for notifications"), "notifications-select"),
-    "PRIMARY_GROUP_PATTERN": ("", _("Regular expression to match primary group, e.g. '^Class .*'"), str),
-}
-CONSTANCE_CONFIG_FIELDSETS = {
-    "General settings": ("SITE_TITLE", "SITE_DESCRIPTION"),
-    "Theme settings": ("COLOUR_PRIMARY", "COLOUR_SECONDARY"),
-    "Mail settings": ("MAIL_OUT_NAME", "MAIL_OUT"),
-    "Notification settings": ("NOTIFICATION_CHANNELS", "ADRESSING_NAME_FORMAT"),
-    "Footer settings": ("PRIVACY_URL", "IMPRINT_URL"),
-    "Account settings": ("PRIMARY_GROUP_PATTERN",),
+DYNAMIC_PREFERENCES = {
+    "REGISTRY_MODULE": "preferences",
 }
 
-merge_app_settings("CONSTANCE_ADDITIONAL_FIELDS", CONSTANCE_ADDITIONAL_FIELDS, False)
-merge_app_settings("CONSTANCE_CONFIG", CONSTANCE_CONFIG, False)
-merge_app_settings("CONSTANCE_CONFIG_FIELDSETS", CONSTANCE_CONFIG_FIELDSETS, False)
-
 MAINTENANCE_MODE = _settings.get("maintenance.enabled", None)
 MAINTENANCE_MODE_IGNORE_IP_ADDRESSES = _settings.get(
     "maintenance.ignore_ips", _settings.get("maintenance.internal_ips", [])
@@ -451,7 +442,12 @@ if _settings.get("twilio.sid", None):
     TWILIO_CALLER_ID = _settings.get("twilio.callerid")
 
 if _settings.get("celery.enabled", False):
-    INSTALLED_APPS += ("django_celery_beat", "django_celery_results")
+    INSTALLED_APPS += (
+        "django_celery_beat",
+        "django_celery_results",
+        "celery_progress",
+        "health_check.contrib.celery",
+    )
     CELERY_BROKER_URL = _settings.get("celery.broker", "redis://localhost")
     CELERY_RESULT_BACKEND = "django-db"
     CELERY_CACHE_BACKEND = "django-cache"
@@ -461,92 +457,177 @@ if _settings.get("celery.enabled", False):
         INSTALLED_APPS += ("djcelery_email",)
         EMAIL_BACKEND = "djcelery_email.backends.CeleryEmailBackend"
 
-PWA_APP_NAME = lazy_config("SITE_TITLE")
-PWA_APP_DESCRIPTION = lazy_config("SITE_DESCRIPTION")
-PWA_APP_THEME_COLOR = lazy_config("COLOUR_PRIMARY")
+PWA_APP_NAME = lazy_preference("general", "title")
+PWA_APP_DESCRIPTION = lazy_preference("general", "description")
+PWA_APP_THEME_COLOR = lazy_preference("theme", "primary")
 PWA_APP_BACKGROUND_COLOR = "#ffffff"
 PWA_APP_DISPLAY = "standalone"
 PWA_APP_ORIENTATION = "any"
-PWA_APP_ICONS = [  # three icons to upload dbsettings
-    {"src": STATIC_URL + "/icons/android_192.png", "sizes": "192x192"},
-    {"src": STATIC_URL + "/icons/android_512.png", "sizes": "512x512"},
+PWA_APP_ICONS = [
+    {
+        "src": lazy_get_favicon_url(
+            title="pwa_icon", size=192, rel="android", default=STATIC_URL + "icons/android_192.png"
+        ),
+        "sizes": "192x192",
+    },
+    {
+        "src": lazy_get_favicon_url(
+            title="pwa_icon", size=512, rel="android", default=STATIC_URL + "icons/android_512.png"
+        ),
+        "sizes": "512x512",
+    },
 ]
 PWA_APP_ICONS_APPLE = [
-    {"src": STATIC_URL + "/icons/apple_76.png", "sizes": "76x76"},
-    {"src": STATIC_URL + "/icons/apple_114.png", "sizes": "114x114"},
-    {"src": STATIC_URL + "/icons/apple_152.png", "sizes": "152x152"},
-    {"src": STATIC_URL + "/icons/apple_180.png", "sizes": "180x180"},
+    {
+        "src": lazy_get_favicon_url(
+            title="pwa_icon", size=192, rel="apple", default=STATIC_URL + "icons/apple_76.png"
+        ),
+        "sizes": "76x76",
+    },
+    {
+        "src": lazy_get_favicon_url(
+            title="pwa_icon", size=192, rel="apple", default=STATIC_URL + "icons/apple_114.png"
+        ),
+        "sizes": "114x114",
+    },
+    {
+        "src": lazy_get_favicon_url(
+            title="pwa_icon", size=192, rel="apple", default=STATIC_URL + "icons/apple_152.png"
+        ),
+        "sizes": "152x152",
+    },
+    {
+        "src": lazy_get_favicon_url(
+            title="pwa_icon", size=192, rel="apple", default=STATIC_URL + "icons/apple_180.png"
+        ),
+        "sizes": "180x180",
+    },
 ]
 PWA_APP_SPLASH_SCREEN = [
     {
-        "src": STATIC_URL + "/icons/android_512.png",
-        "media": "(device-width: 320px) and (device-height: 568px) and (-webkit-device-pixel-ratio: 2)",
+        "src": lazy_get_favicon_url(
+            title="pwa_icon", size=192, rel="apple", default=STATIC_URL + "icons/apple_180.png"
+        ),
+        "media": (
+            "(device-width: 320px) and (device-height: 568px) and" "(-webkit-device-pixel-ratio: 2)"
+        ),
     }
 ]
+
+
 PWA_SERVICE_WORKER_PATH = os.path.join(STATIC_ROOT, "js", "serviceworker.js")
 
 SITE_ID = 1
 
 CKEDITOR_CONFIGS = {
-    'default': {
-        'toolbar_Basic': [
-            ['Source', '-', 'Bold', 'Italic']
-        ],
-        'toolbar_Full': [
-            {'name': 'document', 'items': ['Source', '-', 'Save', 'NewPage', 'Preview', 'Print', '-', 'Templates']},
-            {'name': 'clipboard', 'items': ['Cut', 'Copy', 'Paste', 'PasteText', 'PasteFromWord', '-', 'Undo', 'Redo']},
-            {'name': 'editing', 'items': ['Find', 'Replace', '-', 'SelectAll']},
-            {'name': 'insert',
-             'items': ['Image', 'Table', 'HorizontalRule', 'Smiley', 'SpecialChar', 'PageBreak', 'Iframe']},
-            '/',
-            {'name': 'basicstyles',
-             'items': ['Bold', 'Italic', 'Underline', 'Strike', 'Subscript', 'Superscript', '-', 'RemoveFormat']},
-            {'name': 'paragraph',
-             'items': ['NumberedList', 'BulletedList', '-', 'Outdent', 'Indent', '-', 'Blockquote', 'CreateDiv', '-',
-                       'JustifyLeft', 'JustifyCenter', 'JustifyRight', 'JustifyBlock', '-', 'BidiLtr', 'BidiRtl',
-                       'Language']},
-            {'name': 'links', 'items': ['Link', 'Unlink', 'Anchor']},
-            '/',
-            {'name': 'styles', 'items': ['Styles', 'Format', 'Font', 'FontSize']},
-            {'name': 'colors', 'items': ['TextColor', 'BGColor']},
-            {'name': 'tools', 'items': ['Maximize', 'ShowBlocks']},
-            {'name': 'about', 'items': ['About']},
-            {'name': 'customtools', 'items': [
-                'Preview',
-                'Maximize',
-            ]},
+    "default": {
+        "toolbar_Basic": [["Source", "-", "Bold", "Italic"]],
+        "toolbar_Full": [
+            {
+                "name": "document",
+                "items": ["Source", "-", "Save", "NewPage", "Preview", "Print", "-", "Templates"],
+            },
+            {
+                "name": "clipboard",
+                "items": [
+                    "Cut",
+                    "Copy",
+                    "Paste",
+                    "PasteText",
+                    "PasteFromWord",
+                    "-",
+                    "Undo",
+                    "Redo",
+                ],
+            },
+            {"name": "editing", "items": ["Find", "Replace", "-", "SelectAll"]},
+            {
+                "name": "insert",
+                "items": [
+                    "Image",
+                    "Table",
+                    "HorizontalRule",
+                    "Smiley",
+                    "SpecialChar",
+                    "PageBreak",
+                    "Iframe",
+                ],
+            },
+            "/",
+            {
+                "name": "basicstyles",
+                "items": [
+                    "Bold",
+                    "Italic",
+                    "Underline",
+                    "Strike",
+                    "Subscript",
+                    "Superscript",
+                    "-",
+                    "RemoveFormat",
+                ],
+            },
+            {
+                "name": "paragraph",
+                "items": [
+                    "NumberedList",
+                    "BulletedList",
+                    "-",
+                    "Outdent",
+                    "Indent",
+                    "-",
+                    "Blockquote",
+                    "CreateDiv",
+                    "-",
+                    "JustifyLeft",
+                    "JustifyCenter",
+                    "JustifyRight",
+                    "JustifyBlock",
+                    "-",
+                    "BidiLtr",
+                    "BidiRtl",
+                    "Language",
+                ],
+            },
+            {"name": "links", "items": ["Link", "Unlink", "Anchor"]},
+            "/",
+            {"name": "styles", "items": ["Styles", "Format", "Font", "FontSize"]},
+            {"name": "colors", "items": ["TextColor", "BGColor"]},
+            {"name": "tools", "items": ["Maximize", "ShowBlocks"]},
+            {"name": "about", "items": ["About"]},
+            {"name": "customtools", "items": ["Preview", "Maximize",]},
         ],
-        'toolbar': 'Full',
-        'tabSpaces': 4,
-        'extraPlugins': ','.join([
-            'uploadimage',
-            'div',
-            'autolink',
-            'autoembed',
-            'embedsemantic',
-            'autogrow',
-            # 'devtools',
-            'widget',
-            'lineutils',
-            'clipboard',
-            'dialog',
-            'dialogui',
-            'elementspath'
-        ]),
+        "toolbar": "Full",
+        "tabSpaces": 4,
+        "extraPlugins": ",".join(
+            [
+                "uploadimage",
+                "div",
+                "autolink",
+                "autoembed",
+                "embedsemantic",
+                "autogrow",
+                # 'devtools',
+                "widget",
+                "lineutils",
+                "clipboard",
+                "dialog",
+                "dialogui",
+                "elementspath",
+            ]
+        ),
     }
 }
 
 # Which HTML tags are allowed
-BLEACH_ALLOWED_TAGS = ['p', 'b', 'i', 'u', 'em', 'strong', 'a', 'div']
+BLEACH_ALLOWED_TAGS = ["p", "b", "i", "u", "em", "strong", "a", "div"]
 
 # Which HTML attributes are allowed
-BLEACH_ALLOWED_ATTRIBUTES = ['href', 'title', 'style']
+BLEACH_ALLOWED_ATTRIBUTES = ["href", "title", "style"]
 
 # Which CSS properties are allowed in 'style' attributes (assuming
 # style is an allowed attribute)
-BLEACH_ALLOWED_STYLES = [
-    'font-family', 'font-weight', 'text-decoration', 'font-variant'
-]
+BLEACH_ALLOWED_STYLES = ["font-family", "font-weight", "text-decoration", "font-variant"]
 
 # Strip unknown tags if True, replace with HTML escaped characters if
 # False
@@ -556,52 +637,55 @@ BLEACH_STRIP_TAGS = True
 BLEACH_STRIP_COMMENTS = True
 
 LOGGING = {
-    'version': 1,
-    'disable_existing_loggers': False,
-    'handlers': {
-        'console': {
-            'class': 'logging.StreamHandler',
-            'formatter': "verbose"
-        },
-    },
-    'formatters': {
-        'verbose': {
-            'format': '%(levelname)s %(asctime)s %(module)s: %(message)s'
-        }
-    },
-    'root': {
-        'handlers': ['console'],
-        'level': _settings.get("logging.level", "WARNING"),
-    },
+    "version": 1,
+    "disable_existing_loggers": False,
+    "handlers": {"console": {"class": "logging.StreamHandler", "formatter": "verbose"},},
+    "formatters": {"verbose": {"format": "%(levelname)s %(asctime)s %(module)s: %(message)s"}},
+    "root": {"handlers": ["console"], "level": _settings.get("logging.level", "WARNING"),},
 }
 
+# Rules and permissions
+
+GUARDIAN_RAISE_403 = True
+ANONYMOUS_USER_NAME = None
+
+SILENCED_SYSTEM_CHECKS.append("guardian.W001")
+
+# Append authentication backends
+AUTHENTICATION_BACKENDS.append("rules.permissions.ObjectPermissionBackend")
+
 HAYSTACK_BACKEND_SHORT = _settings.get("search.backend", "simple")
 
 if HAYSTACK_BACKEND_SHORT == "simple":
     HAYSTACK_CONNECTIONS = {
-        'default': {
-            'ENGINE': 'haystack.backends.simple_backend.SimpleEngine',
-        },
+        "default": {"ENGINE": "haystack.backends.simple_backend.SimpleEngine",},
     }
 elif HAYSTACK_BACKEND_SHORT == "xapian":
     HAYSTACK_CONNECTIONS = {
-        'default': {
-            'ENGINE': 'xapian_backend.XapianEngine',
-            'PATH': _settings.get("search.index", os.path.join(BASE_DIR, "xapian_index")),
+        "default": {
+            "ENGINE": "xapian_backend.XapianEngine",
+            "PATH": _settings.get("search.index", os.path.join(BASE_DIR, "xapian_index")),
         },
     }
 elif HAYSTACK_BACKEND_SHORT == "whoosh":
     HAYSTACK_CONNECTIONS = {
-        'default': {
-            'ENGINE': 'haystack.backends.whoosh_backend.WhooshEngine',
-            'PATH': _settings.get("search.index", os.path.join(BASE_DIR, "whoosh_index")),
+        "default": {
+            "ENGINE": "haystack.backends.whoosh_backend.WhooshEngine",
+            "PATH": _settings.get("search.index", os.path.join(BASE_DIR, "whoosh_index")),
         },
     }
 
 if _settings.get("celery.enabled", False) and _settings.get("search.celery", True):
     INSTALLED_APPS.append("celery_haystack")
-    HAYSTACK_SIGNAL_PROCESSOR = 'celery_haystack.signals.CelerySignalProcessor'
+    HAYSTACK_SIGNAL_PROCESSOR = "celery_haystack.signals.CelerySignalProcessor"
 else:
-    HAYSTACK_SIGNAL_PROCESSOR = 'haystack.signals.RealtimeSignalProcessor'
+    HAYSTACK_SIGNAL_PROCESSOR = "haystack.signals.RealtimeSignalProcessor"
 
 HAYSTACK_SEARCH_RESULTS_PER_PAGE = 10
+
+DJANGO_EASY_AUDIT_WATCH_REQUEST_EVENTS = False
+
+HEALTH_CHECK = {
+    "DISK_USAGE_MAX": _settings.get("health.disk_usage_max_percent", 90),
+    "MEMORY_MIN": _settings.get("health.memory_min_mb", 500),
+}
diff --git a/aleksis/core/signals.py b/aleksis/core/signals.py
deleted file mode 100644
index bcd55fe623bc3f3952a0853e8f8688630978194a..0000000000000000000000000000000000000000
--- a/aleksis/core/signals.py
+++ /dev/null
@@ -1,13 +0,0 @@
-import os
-from glob import glob
-
-from django.conf import settings
-
-
-def clean_scss(*args, **kwargs) -> None:
-    for source_map in glob(os.path.join(settings.STATIC_ROOT, "*.css.map")):
-        try:
-            os.unlink(source_map)
-        except OSError:
-            # Ignore because old is better than nothing
-            pass  # noqa
diff --git a/aleksis/core/static/icons/android_192.png b/aleksis/core/static/icons/android_192.png
index 3de63ed87e9a3c6389b8cbf686922d5d2b1d38ec..b5f7fec68184883830f68dabc606f7a95c45e8bb 100644
Binary files a/aleksis/core/static/icons/android_192.png and b/aleksis/core/static/icons/android_192.png differ
diff --git a/aleksis/core/static/icons/android_512.png b/aleksis/core/static/icons/android_512.png
index 3de63ed87e9a3c6389b8cbf686922d5d2b1d38ec..5e042b4f98ef6dd8056ce643ad16ba085658684e 100644
Binary files a/aleksis/core/static/icons/android_512.png and b/aleksis/core/static/icons/android_512.png differ
diff --git a/aleksis/core/static/icons/apple_114.png b/aleksis/core/static/icons/apple_114.png
index 859930c3204e69df15c42d5437786ace0c921310..eb8db13ad567105a819dab802b4f7ddd360e34be 100644
Binary files a/aleksis/core/static/icons/apple_114.png and b/aleksis/core/static/icons/apple_114.png differ
diff --git a/aleksis/core/static/icons/apple_152.png b/aleksis/core/static/icons/apple_152.png
index d554e0ddb5e53d6f67b7e411c82388e1222cae0d..51eaa8d28988a16629e2b884d8de4df58fbadccc 100644
Binary files a/aleksis/core/static/icons/apple_152.png and b/aleksis/core/static/icons/apple_152.png differ
diff --git a/aleksis/core/static/icons/apple_180.png b/aleksis/core/static/icons/apple_180.png
index 69dacfa6d08225ea888812f578cd1b8e9df21789..5c6b70846586ef0bf4bd44d177f6f79b49d1cddd 100644
Binary files a/aleksis/core/static/icons/apple_180.png and b/aleksis/core/static/icons/apple_180.png differ
diff --git a/aleksis/core/static/icons/apple_76.png b/aleksis/core/static/icons/apple_76.png
index 9f89e66cac53188c090f030022cf5047a5ca3ea6..3751b84654d01943148fc6f1b90125a4195d4af9 100644
Binary files a/aleksis/core/static/icons/apple_76.png and b/aleksis/core/static/icons/apple_76.png differ
diff --git a/aleksis/core/static/icons/favicon_16.png b/aleksis/core/static/icons/favicon_16.png
index c6a2906384295d750a2b2f66a7a967bacd5a1159..4af3f9529ea0e3a19de31643cb782e1c08b46d5e 100644
Binary files a/aleksis/core/static/icons/favicon_16.png and b/aleksis/core/static/icons/favicon_16.png differ
diff --git a/aleksis/core/static/icons/favicon_32.png b/aleksis/core/static/icons/favicon_32.png
index 0b3dbb819c40cfa9942139197d8b9d6ed320e562..3b9a9c2d025fbbb137a1b84f805716fffe1859c8 100644
Binary files a/aleksis/core/static/icons/favicon_32.png and b/aleksis/core/static/icons/favicon_32.png differ
diff --git a/aleksis/core/static/icons/favicon_48.png b/aleksis/core/static/icons/favicon_48.png
index 06886725806fa482c887c3443686be0cfde828fe..766a8bb798fc70665aa241e058b0462e688cd309 100644
Binary files a/aleksis/core/static/icons/favicon_48.png and b/aleksis/core/static/icons/favicon_48.png differ
diff --git a/aleksis/core/static/img/aleksis-banner.svg b/aleksis/core/static/img/aleksis-banner.svg
new file mode 100644
index 0000000000000000000000000000000000000000..844059bf50ed4b861e4e78ec896cf61d825a3ec5
--- /dev/null
+++ b/aleksis/core/static/img/aleksis-banner.svg
@@ -0,0 +1,457 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!-- Created with Inkscape (http://www.inkscape.org/) -->
+
+<svg
+   xmlns:osb="http://www.openswatchbook.org/uri/2009/osb"
+   xmlns:dc="http://purl.org/dc/elements/1.1/"
+   xmlns:cc="http://creativecommons.org/ns#"
+   xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+   xmlns:svg="http://www.w3.org/2000/svg"
+   xmlns="http://www.w3.org/2000/svg"
+   xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+   xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+   width="293.99948mm"
+   height="96.732994mm"
+   viewBox="0 0 293.99948 96.732994"
+   version="1.1"
+   id="svg8"
+   inkscape:version="0.92.4 (5da689c313, 2019-01-14)"
+   sodipodi:docname="aleksis-banner.svg">
+  <defs
+     id="defs2">
+    <linearGradient
+       id="linearGradient2292"
+       osb:paint="solid">
+      <stop
+         style="stop-color:#000000;stop-opacity:1;"
+         offset="0"
+         id="stop2290" />
+    </linearGradient>
+    <linearGradient
+       id="shadow-gradient"
+       x1="-19.530001"
+       x2="165.39999"
+       y1="-19.530001"
+       y2="165.39999"
+       gradientUnits="userSpaceOnUse">
+      <stop
+         id="stop9444"
+         offset="0" />
+      <stop
+         id="stop9446"
+         stop-opacity="0"
+         offset="1" />
+    </linearGradient>
+    <filter
+       style="color-interpolation-filters:sRGB"
+       inkscape:label="Drop Shadow"
+       id="filter1689">
+      <feFlood
+         flood-opacity="0.498039"
+         flood-color="rgb(0,0,0)"
+         result="flood"
+         id="feFlood1679" />
+      <feComposite
+         in="flood"
+         in2="SourceGraphic"
+         operator="in"
+         result="composite1"
+         id="feComposite1681" />
+      <feGaussianBlur
+         in="composite1"
+         stdDeviation="1"
+         result="blur"
+         id="feGaussianBlur1683" />
+      <feOffset
+         dx="3"
+         dy="3"
+         result="offset"
+         id="feOffset1685" />
+      <feComposite
+         in="SourceGraphic"
+         in2="offset"
+         operator="over"
+         result="composite2"
+         id="feComposite1687" />
+    </filter>
+    <filter
+       style="color-interpolation-filters:sRGB"
+       inkscape:label="Drop Shadow"
+       id="filter1701">
+      <feFlood
+         flood-opacity="0.498039"
+         flood-color="rgb(0,0,0)"
+         result="flood"
+         id="feFlood1691" />
+      <feComposite
+         in="flood"
+         in2="SourceGraphic"
+         operator="in"
+         result="composite1"
+         id="feComposite1693" />
+      <feGaussianBlur
+         in="composite1"
+         stdDeviation="1"
+         result="blur"
+         id="feGaussianBlur1695" />
+      <feOffset
+         dx="3"
+         dy="3"
+         result="offset"
+         id="feOffset1697" />
+      <feComposite
+         in="SourceGraphic"
+         in2="offset"
+         operator="over"
+         result="composite2"
+         id="feComposite1699" />
+    </filter>
+    <linearGradient
+       id="shadow-gradient-9"
+       x1="-19.530001"
+       x2="165.39999"
+       y1="-19.530001"
+       y2="165.39999"
+       gradientUnits="userSpaceOnUse">
+      <stop
+         id="stop9444-0"
+         offset="0" />
+      <stop
+         id="stop9446-0"
+         stop-opacity="0"
+         offset="1" />
+    </linearGradient>
+  </defs>
+  <sodipodi:namedview
+     id="base"
+     pagecolor="#ffffff"
+     bordercolor="#666666"
+     borderopacity="1.0"
+     inkscape:pageopacity="0.0"
+     inkscape:pageshadow="2"
+     inkscape:zoom="0.69999999"
+     inkscape:cx="281.56136"
+     inkscape:cy="-70.164043"
+     inkscape:document-units="mm"
+     inkscape:current-layer="layer1"
+     showgrid="false"
+     inkscape:window-width="1920"
+     inkscape:window-height="962"
+     inkscape:window-x="0"
+     inkscape:window-y="30"
+     inkscape:window-maximized="1"
+     showguides="true"
+     inkscape:guide-bbox="true"
+     fit-margin-top="0"
+     fit-margin-left="0"
+     fit-margin-right="0"
+     fit-margin-bottom="0">
+    <sodipodi:guide
+       position="232.19718,49.803852"
+       orientation="1,0"
+       id="guide2367"
+       inkscape:locked="false" />
+    <sodipodi:guide
+       position="41.63401,-6.3227481"
+       orientation="1,0"
+       id="guide2369"
+       inkscape:locked="false" />
+    <sodipodi:guide
+       position="112.86134,83.078911"
+       orientation="0,1"
+       id="guide2405"
+       inkscape:locked="false" />
+    <sodipodi:guide
+       position="106.44687,36.707652"
+       orientation="0,1"
+       id="guide2407"
+       inkscape:locked="false" />
+    <sodipodi:guide
+       position="87.943818,36.707662"
+       orientation="1,0"
+       id="guide2691"
+       inkscape:locked="false" />
+  </sodipodi:namedview>
+  <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></dc:title>
+      </cc:Work>
+    </rdf:RDF>
+  </metadata>
+  <g
+     inkscape:label="Layer 1"
+     inkscape:groupmode="layer"
+     id="layer1"
+     transform="translate(-3,-39.997002)">
+    <g
+       id="g938"
+       transform="matrix(1.5379515,0,0,1.5379515,-65.338762,-42.304096)">
+      <g
+         inkscape:export-ydpi="93"
+         inkscape:export-xdpi="93"
+         inkscape:export-filename="/tmp/aleksis-layout.png"
+         transform="matrix(0.68664702,0,0,0.68664702,219.7793,11.427418)"
+         id="g1522">
+        <g
+           transform="matrix(1.0000492,0,0,1.0000492,-255.36319,61.292102)"
+           id="background-with-glow">
+          <rect
+             style="fill:#0d5eaf;stroke-width:0.4104"
+             x="0"
+             id="background"
+             y="-3.357e-06"
+             width="67.730003"
+             height="67.730003"
+             rx="3.3069999"
+             ry="3.3069999" />
+          <path
+             style="opacity:0.2;fill:#ffffff;stroke-width:1.551"
+             inkscape:connector-curvature="0"
+             id="glow"
+             transform="scale(0.2646)"
+             d="M 9.959,0.2578 C 4.261,1.4258 0,6.4468 0,12.4978 v 52.31 a 234.1,86.8 0 0 0 188.9,35.71 234.1,86.8 0 0 0 67.09,-3.715 v -84.3 c 0,-4.319 -2.17,-8.112 -5.482,-10.36 -0.0155,-0.01049 -0.0313,-0.02083 -0.0469,-0.03125 -0.2971,-0.1993 -0.6034,-0.3849 -0.918,-0.5586 -0.0521,-0.02867 -0.1037,-0.05799 -0.1562,-0.08594 -0.2939,-0.1568 -0.5968,-0.3001 -0.9043,-0.4336 -0.0565,-0.02442 -0.111,-0.05257 -0.168,-0.07617 -0.3308,-0.1378 -0.6709,-0.2577 -1.016,-0.3672 -0.035,-0.01105 -0.0684,-0.02636 -0.1035,-0.03711 -0.3806,-0.1172 -0.7687,-0.2158 -1.164,-0.2969 h -236.1 z" />
+        </g>
+        <g
+           transform="matrix(1.0000492,0,0,1.0000492,-255.36319,61.292102)"
+           id="widgets-with-shadow">
+          <path
+             style="fill:url(#shadow-gradient);stroke-width:1.551"
+             inkscape:connector-curvature="0"
+             id="shadow"
+             transform="scale(0.2646)"
+             d="m 188.2,20.16 c -11.17,0.2955 -4.441,16.25 -17.54,12.56 -17.31,4.576 -18.64,24.39 -17.21,39.1 1.565,6.747 -1.9,1.315 -3.715,-0.1367 -5.666,-3.203 -12.24,3.777 -8.709,9.107 1.178,1.172 2.354,2.349 3.529,3.525 -1.648,-0.1733 -2.722,0.18 -3.406,0.8867 -20.9,-20.89 -41.81,-41.78 -62.73,-62.66 -1.667,-1.362 -3.817,-2.142 -5.979,-2.131 -9.823,2.627 -15.3,12.67 -22.91,19 -8.327,8.601 -17.24,16.68 -25.2,25.61 -4.688,8.603 4.095,13.55 8.873,18.91 1.473,2.302 7.219,4.605 2.852,6.906 -7.332,4.874 -9.54,15.75 -2.365,21.36 15.49,15.49 30.97,30.98 46.46,46.46 -0.4584,0.6368 -1.044,1.194 -1.809,1.637 -3.106,4.526 -4.08,-0.9818 -6.254,-1.748 -5.859,-6.192 -12.02,-12.08 -17.95,-18.21 h -22.08 c -12.43,0.005 -12.65,13.69 -12,22.8 0.3378,21.38 -0.6904,42.86 0.541,64.18 3.053,6.838 9.872,10.98 14.65,16.58 4.034,4.029 8.064,8.062 12.1,12.09 h 196.1 c 6.925,0 12.5,-5.575 12.5,-12.5 v -162.7 c -13.85,-14.24 -27.68,-28.5 -41.44,-42.83 -4.585,-4.531 -11.77,-4.442 -15.49,-9.955 -3.38,-2.915 -5.87,-7.7 -10.88,-7.838 z" />
+          <g
+             style="fill:#ffffff"
+             id="widgets">
+            <path
+               style="stroke-width:0.05441"
+               inkscape:connector-curvature="0"
+               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" />
+            <path
+               inkscape:connector-curvature="0"
+               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" />
+            <path
+               inkscape:connector-curvature="0"
+               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" />
+            <path
+               inkscape:connector-curvature="0"
+               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" />
+          </g>
+        </g>
+      </g>
+      <g
+         inkscape:export-ydpi="93"
+         inkscape:export-xdpi="93"
+         inkscape:export-filename="/tmp/aleksis-layout.png"
+         transform="translate(-9.1355604)"
+         id="text1709"
+         style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:66.17029572px;line-height:1.25;font-family:'Liberation Sans';-inkscape-font-specification:'Liberation Sans';letter-spacing:0px;word-spacing:0px;fill:#3d7ebf;fill-opacity:1;stroke:#3d7ebf;stroke-width:1.5;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+         aria-label="ALEKSIS">
+        <path
+           inkscape:connector-curvature="0"
+           id="path841"
+           style="font-style:normal;font-variant:normal;font-weight:500;font-stretch:normal;font-family:'Ostrich Sans Rounded';-inkscape-font-specification:'Ostrich Sans Rounded Medium';fill:#3d7ebf;fill-opacity:1;stroke:#3d7ebf;stroke-width:1.5;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+           d="m 126.22416,97.988904 c 0.19851,0.529362 -0.26468,1.058725 -0.79404,1.257236 h -0.26469 c -0.52936,0 -0.86021,-0.264682 -0.99255,-0.794044 l -2.97766,-12.837037 h -11.71215 l -2.97766,12.837037 c -0.19851,0.529362 -0.72787,0.992554 -1.25723,0.794044 -0.59554,-0.198511 -0.99256,-0.727874 -0.79405,-1.257236 l 9.85938,-42.5475 c 0.13234,-0.529363 0.46319,-0.794044 0.99255,-0.794044 0.59553,0 0.92638,0.264681 1.05872,0.794044 z m -10.9181,-37.717069 -5.35979,23.291944 h 10.78575 z" />
+        <path
+           inkscape:connector-curvature="0"
+           id="path843"
+           style="font-style:normal;font-variant:normal;font-weight:500;font-stretch:normal;font-family:'Ostrich Sans Rounded';-inkscape-font-specification:'Ostrich Sans Rounded Medium';fill:#3d7ebf;fill-opacity:1;stroke:#3d7ebf;stroke-width:1.5;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+           d="m 130.9605,99.24614 c -0.72787,0 -1.05872,-0.330852 -1.05872,-1.058725 v -42.5475 c 0,-0.529363 0.46319,-0.992555 1.05872,-0.992555 0.52937,0 0.99256,0.463192 0.99256,0.992555 V 97.19486 h 13.96193 c 0.59553,0 1.05873,0.463192 1.05873,0.992555 0,0.727873 -0.33086,1.058725 -1.05873,1.058725 z" />
+        <path
+           inkscape:connector-curvature="0"
+           id="path845"
+           style="font-style:normal;font-variant:normal;font-weight:500;font-stretch:normal;font-family:'Ostrich Sans Rounded';-inkscape-font-specification:'Ostrich Sans Rounded Medium';fill:#3d7ebf;fill-opacity:1;stroke:#3d7ebf;stroke-width:1.5;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+           d="M 151.86515,77.939304 V 97.19486 h 12.90321 c 0.59553,0 1.05872,0.463192 1.05872,0.992555 0,0.727873 -0.33085,1.058725 -1.05872,1.058725 H 150.8726 c -0.72788,0 -1.05873,-0.330852 -1.05873,-1.058725 v -42.5475 c 0,-0.529363 0.46319,-0.992555 1.05873,-0.992555 h 13.89576 c 0.59553,0 1.05872,0.463192 1.05872,0.992555 0,0.727873 -0.33085,1.058724 -1.05872,1.058724 h -12.90321 v 19.189386 h 12.90321 c 0.72787,0 1.05872,0.330852 1.05872,1.058725 0,0.529362 -0.46319,0.992554 -1.05872,0.992554 z" />
+        <path
+           inkscape:connector-curvature="0"
+           id="path847"
+           style="font-style:normal;font-variant:normal;font-weight:500;font-stretch:normal;font-family:'Ostrich Sans Rounded';-inkscape-font-specification:'Ostrich Sans Rounded Medium';fill:#3d7ebf;fill-opacity:1;stroke:#3d7ebf;stroke-width:1.5;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+           d="m 178.16784,75.160152 12.63853,22.564071 c 0.26468,0.463192 0.0662,1.124895 -0.39702,1.389576 l -0.52937,0.132341 c -0.39702,0 -0.72787,-0.198511 -0.92638,-0.529363 l -12.10916,-21.637687 -4.49958,6.749371 v 14.358954 c 0,0.595532 -0.4632,1.058725 -0.99256,1.058725 -0.72787,0 -1.05872,-0.330852 -1.05872,-1.058725 v -42.5475 c 0,-0.529363 0.46319,-0.992555 1.05872,-0.992555 0.52936,0 0.99256,0.463192 0.99256,0.992555 v 24.350668 l 16.67491,-24.880031 c 0.33085,-0.529362 0.99255,-0.595532 1.45575,-0.330851 0.46319,0.330851 0.59553,0.992554 0.26468,1.455746 z" />
+        <path
+           inkscape:connector-curvature="0"
+           id="path849"
+           style="font-style:normal;font-variant:normal;font-weight:500;font-stretch:normal;font-family:'Ostrich Sans Rounded';-inkscape-font-specification:'Ostrich Sans Rounded Medium';fill:#3d7ebf;fill-opacity:1;stroke:#3d7ebf;stroke-width:1.5;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+           d="m 204.19758,78.468667 -1.1249,-0.529363 c -1.3234,-0.529362 -3.44085,-1.455746 -5.22745,-3.308514 -1.85277,-1.786598 -3.37469,-4.30107 -3.37469,-8.205117 0,-6.6832 3.37469,-11.844483 9.52853,-11.844483 6.35234,0 9.59469,4.102558 9.59469,11.844483 0,0.529362 -0.46319,1.058725 -1.05873,1.058725 -0.59553,0 -1.05872,-0.529363 -1.05872,-1.058725 0,-6.6832 -2.4483,-9.793204 -7.47724,-9.793204 -4.6981,0 -7.47725,4.036388 -7.47725,9.793204 0,3.176174 1.19107,5.293624 2.77915,6.74937 1.52192,1.455747 3.30852,2.31596 4.63193,2.845323 l 1.05872,0.463192 c 1.1249,0.529362 3.30851,1.389576 5.16128,3.110004 1.85277,1.720427 3.44086,4.234899 3.44086,8.138946 0,3.242345 -0.86022,6.021497 -2.38213,8.072776 -1.72043,2.24979 -4.10256,3.374685 -7.21256,3.374685 -6.35235,0 -9.52853,-4.102558 -9.52853,-11.778312 0,-0.727874 0.33085,-1.058725 1.05873,-1.058725 0.52936,0 0.99255,0.463192 0.99255,1.058725 0,4.301069 1.1249,6.74937 2.58064,8.072776 1.45575,1.389576 3.30852,1.654257 4.89661,1.654257 4.76426,0 7.47724,-3.507026 7.47724,-9.396182 0,-3.176174 -1.25724,-5.293624 -2.77915,-6.6832 -0.79405,-0.661703 -1.58809,-1.191065 -2.4483,-1.588087 z" />
+        <path
+           inkscape:connector-curvature="0"
+           id="path851"
+           style="font-style:normal;font-variant:normal;font-weight:500;font-stretch:normal;font-family:'Ostrich Sans Rounded';-inkscape-font-specification:'Ostrich Sans Rounded Medium';fill:#3d7ebf;fill-opacity:1;stroke:#3d7ebf;stroke-width:1.5;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+           d="m 217.99822,55.639915 c 0,-0.529363 0.4632,-0.992555 1.05873,-0.992555 0.59553,0 1.05872,0.463192 1.05872,0.992555 v 42.5475 c 0,0.727873 -0.33085,1.058725 -1.05872,1.058725 -0.72787,0 -1.05873,-0.330852 -1.05873,-1.058725 z" />
+        <path
+           inkscape:connector-curvature="0"
+           id="path853"
+           style="font-style:normal;font-variant:normal;font-weight:500;font-stretch:normal;font-family:'Ostrich Sans Rounded';-inkscape-font-specification:'Ostrich Sans Rounded Medium';fill:#3d7ebf;fill-opacity:1;stroke:#3d7ebf;stroke-width:1.5;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+           d="m 234.43947,78.468667 -1.12489,-0.529363 c -1.32341,-0.529362 -3.44086,-1.455746 -5.22745,-3.308514 -1.85277,-1.786598 -3.37469,-4.30107 -3.37469,-8.205117 0,-6.6832 3.37469,-11.844483 9.52852,-11.844483 6.35235,0 9.5947,4.102558 9.5947,11.844483 0,0.529362 -0.4632,1.058725 -1.05873,1.058725 -0.59553,0 -1.05872,-0.529363 -1.05872,-1.058725 0,-6.6832 -2.4483,-9.793204 -7.47725,-9.793204 -4.69809,0 -7.47724,4.036388 -7.47724,9.793204 0,3.176174 1.19107,5.293624 2.77915,6.74937 1.52192,1.455747 3.30852,2.31596 4.63192,2.845323 l 1.05873,0.463192 c 1.12489,0.529362 3.30851,1.389576 5.16128,3.110004 1.85277,1.720427 3.44086,4.234899 3.44086,8.138946 0,3.242345 -0.86022,6.021497 -2.38213,8.072776 -1.72043,2.24979 -4.10256,3.374685 -7.21257,3.374685 -6.35235,0 -9.52852,-4.102558 -9.52852,-11.778312 0,-0.727874 0.33085,-1.058725 1.05873,-1.058725 0.52936,0 0.99255,0.463192 0.99255,1.058725 0,4.301069 1.12489,6.74937 2.58064,8.072776 1.45575,1.389576 3.30852,1.654257 4.8966,1.654257 4.76426,0 7.47725,-3.507026 7.47725,-9.396182 0,-3.176174 -1.25724,-5.293624 -2.77916,-6.6832 -0.79404,-0.661703 -1.58808,-1.191065 -2.4483,-1.588087 z" />
+      </g>
+      <g
+         inkscape:export-ydpi="93"
+         inkscape:export-xdpi="93"
+         inkscape:export-filename="/tmp/aleksis-layout.png"
+         transform="matrix(0.95095206,0,0,0.95095206,2.6877943,0.86956426)"
+         id="text2300"
+         style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:27.36661148px;line-height:1.25;font-family:'Liberation Sans';-inkscape-font-specification:'Liberation Sans';letter-spacing:0px;word-spacing:0px;fill:#3d7ebf;fill-opacity:1;stroke:#3d7ebf;stroke-width:0.99766129;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+         aria-label="The Free School Information System">
+        <path
+           inkscape:connector-curvature="0"
+           id="path2302"
+           style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:18.24440765px;font-family:'Ostrich Sans Rounded';-inkscape-font-specification:'Ostrich Sans Rounded';fill:#3d7ebf;fill-opacity:1;stroke:#3d7ebf;stroke-width:0.99766129;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+           d="m 50.719721,108.67707 c 0.164199,0 0.29191,0.12771 0.29191,0.27366 0,0.20069 -0.09122,0.29191 -0.29191,0.29191 h -2.645439 v 11.43925 c 0,0.1642 -0.145956,0.29191 -0.291911,0.29191 -0.200688,0 -0.29191,-0.0912 -0.29191,-0.29191 v -11.43925 h -2.663684 c -0.145955,0 -0.273666,-0.12771 -0.273666,-0.29191 0,-0.14595 0.127711,-0.27366 0.273666,-0.27366 z" />
+        <path
+           inkscape:connector-curvature="0"
+           id="path2304"
+           style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:18.24440765px;font-family:'Ostrich Sans Rounded';-inkscape-font-specification:'Ostrich Sans Rounded';fill:#3d7ebf;fill-opacity:1;stroke:#3d7ebf;stroke-width:0.99766129;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+           d="m 56.86552,120.70013 c 0,0.20069 -0.09122,0.29191 -0.29191,0.29191 -0.200689,0 -0.291911,-0.0912 -0.291911,-0.29191 v -5.56454 H 52.55984 v 5.56454 c 0,0.20069 -0.09122,0.29191 -0.29191,0.29191 -0.200689,0 -0.291911,-0.0912 -0.291911,-0.29191 v -11.73115 c 0,-0.14596 0.127711,-0.27367 0.291911,-0.27367 0.164199,0 0.29191,0.12771 0.29191,0.27367 v 5.58279 h 3.721859 v -5.58279 c 0,-0.14596 0.127711,-0.27367 0.291911,-0.27367 0.1642,0 0.29191,0.12771 0.29191,0.27367 z" />
+        <path
+           inkscape:connector-curvature="0"
+           id="path2306"
+           style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:18.24440765px;font-family:'Ostrich Sans Rounded';-inkscape-font-specification:'Ostrich Sans Rounded';fill:#3d7ebf;fill-opacity:1;stroke:#3d7ebf;stroke-width:0.99766129;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+           d="m 58.794866,115.11734 v 5.30913 h 3.557659 c 0.1642,0 0.291911,0.12771 0.291911,0.27366 0,0.20069 -0.09122,0.29191 -0.291911,0.29191 H 58.5212 c -0.200689,0 -0.291911,-0.0912 -0.291911,-0.29191 v -11.73115 c 0,-0.14596 0.127711,-0.27367 0.291911,-0.27367 h 3.831325 c 0.1642,0 0.291911,0.12771 0.291911,0.27367 0,0.20069 -0.09122,0.29191 -0.291911,0.29191 h -3.557659 v 5.29088 h 3.557659 c 0.200689,0 0.291911,0.0912 0.291911,0.29191 0,0.14595 -0.127711,0.27366 -0.291911,0.27366 z" />
+        <path
+           inkscape:connector-curvature="0"
+           id="path2308"
+           style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:18.24440765px;font-family:'Ostrich Sans Rounded';-inkscape-font-specification:'Ostrich Sans Rounded';fill:#3d7ebf;fill-opacity:1;stroke:#3d7ebf;stroke-width:0.99766129;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+           d="m 68.41765,114.55177 h 3.55766 c 0.200688,0 0.29191,0.0912 0.29191,0.29191 0,0.14595 -0.127711,0.27366 -0.29191,0.27366 h -3.55766 v 5.58279 c 0,0.1642 -0.127711,0.29191 -0.273666,0.29191 -0.200689,0 -0.291911,-0.0912 -0.291911,-0.29191 v -11.73115 c 0,-0.14596 0.127711,-0.27367 0.291911,-0.27367 h 3.831326 c 0.164199,0 0.29191,0.12771 0.29191,0.27367 0,0.20069 -0.09122,0.29191 -0.29191,0.29191 h -3.55766 z" />
+        <path
+           inkscape:connector-curvature="0"
+           id="path2310"
+           style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:18.24440765px;font-family:'Ostrich Sans Rounded';-inkscape-font-specification:'Ostrich Sans Rounded';fill:#3d7ebf;fill-opacity:1;stroke:#3d7ebf;stroke-width:0.99766129;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+           d="m 78.517642,120.68189 c 0,0.20069 -0.09122,0.29191 -0.291911,0.29191 -0.145955,0 -0.273666,-0.12771 -0.273666,-0.29191 v -3.1198 c 0,-1.51428 -0.839243,-2.35352 -2.298795,-2.35352 h -1.641997 v 5.47332 c 0,0.20069 -0.09122,0.29191 -0.291911,0.29191 -0.145955,0 -0.273666,-0.12771 -0.273666,-0.29191 v -11.73116 c 0,-0.14595 0.127711,-0.27366 0.273666,-0.27366 h 1.933908 c 1.952151,0 2.846127,1.00344 2.846127,3.2475 0,1.11291 -0.218933,1.93391 -0.675043,2.463 -0.200688,0.23717 -0.437866,0.41962 -0.748021,0.54733 0.237178,0.12771 0.437866,0.25542 0.638555,0.45611 0.529087,0.51084 0.802754,1.24062 0.802754,2.17108 z m -4.506369,-11.43925 v 5.40035 h 1.641997 c 1.587263,0 2.262306,-0.74802 2.262306,-2.71842 0,-1.89742 -0.656799,-2.68193 -2.262306,-2.68193 z" />
+        <path
+           inkscape:connector-curvature="0"
+           id="path2312"
+           style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:18.24440765px;font-family:'Ostrich Sans Rounded';-inkscape-font-specification:'Ostrich Sans Rounded';fill:#3d7ebf;fill-opacity:1;stroke:#3d7ebf;stroke-width:0.99766129;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+           d="m 80.317565,115.11734 v 5.30913 h 3.55766 c 0.164199,0 0.29191,0.12771 0.29191,0.27366 0,0.20069 -0.09122,0.29191 -0.29191,0.29191 h -3.831326 c -0.200688,0 -0.29191,-0.0912 -0.29191,-0.29191 v -11.73115 c 0,-0.14596 0.12771,-0.27367 0.29191,-0.27367 h 3.831326 c 0.164199,0 0.29191,0.12771 0.29191,0.27367 0,0.20069 -0.09122,0.29191 -0.29191,0.29191 h -3.55766 v 5.29088 h 3.55766 c 0.200688,0 0.29191,0.0912 0.29191,0.29191 0,0.14595 -0.127711,0.27366 -0.29191,0.27366 z" />
+        <path
+           inkscape:connector-curvature="0"
+           id="path2314"
+           style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:18.24440765px;font-family:'Ostrich Sans Rounded';-inkscape-font-specification:'Ostrich Sans Rounded';fill:#3d7ebf;fill-opacity:1;stroke:#3d7ebf;stroke-width:0.99766129;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+           d="m 86.018943,115.11734 v 5.30913 h 3.55766 c 0.164199,0 0.29191,0.12771 0.29191,0.27366 0,0.20069 -0.09122,0.29191 -0.29191,0.29191 h -3.831326 c -0.200689,0 -0.291911,-0.0912 -0.291911,-0.29191 v -11.73115 c 0,-0.14596 0.127711,-0.27367 0.291911,-0.27367 h 3.831326 c 0.164199,0 0.29191,0.12771 0.29191,0.27367 0,0.20069 -0.09122,0.29191 -0.29191,0.29191 h -3.55766 v 5.29088 h 3.55766 c 0.200688,0 0.29191,0.0912 0.29191,0.29191 0,0.14595 -0.127711,0.27366 -0.29191,0.27366 z" />
+        <path
+           inkscape:connector-curvature="0"
+           id="path2316"
+           style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:18.24440765px;font-family:'Ostrich Sans Rounded';-inkscape-font-specification:'Ostrich Sans Rounded';fill:#3d7ebf;fill-opacity:1;stroke:#3d7ebf;stroke-width:0.99766129;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+           d="m 97.739836,115.2633 -0.310155,-0.14596 c -0.364888,-0.14595 -0.948709,-0.40137 -1.441308,-0.91222 -0.510843,-0.4926 -0.930465,-1.18588 -0.930465,-2.2623 0,-1.84269 0.930465,-3.26575 2.627195,-3.26575 1.751463,0 2.645437,1.13115 2.645437,3.26575 0,0.14595 -0.12771,0.29191 -0.29191,0.29191 -0.164198,0 -0.291909,-0.14596 -0.291909,-0.29191 0,-1.84269 -0.675043,-2.70018 -2.061618,-2.70018 -1.295353,0 -2.061618,1.11291 -2.061618,2.70018 0,0.87573 0.328399,1.45955 0.766265,1.86093 0.419621,0.40137 0.91222,0.63855 1.277108,0.78451 l 0.291911,0.12771 c 0.310155,0.14595 0.91222,0.38313 1.423064,0.85748 0.510843,0.47436 0.948707,1.16765 0.948707,2.24407 0,0.89397 -0.23718,1.66024 -0.656797,2.22581 -0.474354,0.62031 -1.131153,0.93047 -1.98864,0.93047 -1.751463,0 -2.627195,-1.13116 -2.627195,-3.24751 0,-0.20068 0.09122,-0.29191 0.291911,-0.29191 0.145955,0 0.273666,0.12771 0.273666,0.29191 0,1.18589 0.310155,1.86093 0.711532,2.22582 0.401377,0.38313 0.91222,0.45611 1.350086,0.45611 1.313597,0 2.061618,-0.96695 2.061618,-2.5907 0,-0.87574 -0.346644,-1.45956 -0.766265,-1.84269 -0.218933,-0.18244 -0.437866,-0.3284 -0.675043,-0.43787 z" />
+        <path
+           inkscape:connector-curvature="0"
+           id="path2318"
+           style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:18.24440765px;font-family:'Ostrich Sans Rounded';-inkscape-font-specification:'Ostrich Sans Rounded';fill:#3d7ebf;fill-opacity:1;stroke:#3d7ebf;stroke-width:0.99766129;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+           d="m 101.39898,111.94282 c 0,-1.33185 0.34664,-2.17109 0.85748,-2.64544 0.51085,-0.47436 1.16765,-0.60207 1.75147,-0.60207 0.60206,0 1.25886,0.12771 1.76971,0.60207 0.51084,0.47435 0.85748,1.31359 0.85748,2.64544 0,0.20068 -0.0912,0.29191 -0.29191,0.29191 -0.20069,0 -0.29191,-0.0912 -0.29191,-0.29191 0,-1.82444 -0.6568,-2.68193 -2.04337,-2.68193 -1.33184,0 -2.04338,0.85749 -2.04338,2.68193 v 5.78347 c 0,1.82444 0.72978,2.70018 2.04338,2.70018 1.36833,0 2.04337,-0.87574 2.04337,-2.70018 0,-0.20068 0.0912,-0.29191 0.29191,-0.29191 0.20069,0 0.29191,0.0912 0.29191,0.29191 0,1.36833 -0.34664,2.15284 -0.85748,2.64544 -0.51085,0.4926 -1.16765,0.62031 -1.76971,0.62031 -0.58382,0 -1.24062,-0.12771 -1.75147,-0.62031 -0.51084,-0.4926 -0.85748,-1.27711 -0.85748,-2.64544 z" />
+        <path
+           inkscape:connector-curvature="0"
+           id="path2320"
+           style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:18.24440765px;font-family:'Ostrich Sans Rounded';-inkscape-font-specification:'Ostrich Sans Rounded';fill:#3d7ebf;fill-opacity:1;stroke:#3d7ebf;stroke-width:0.99766129;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+           d="m 112.59648,120.70013 c 0,0.20069 -0.0912,0.29191 -0.29191,0.29191 -0.20069,0 -0.29191,-0.0912 -0.29191,-0.29191 v -5.56454 h -3.72186 v 5.56454 c 0,0.20069 -0.0912,0.29191 -0.29191,0.29191 -0.20069,0 -0.29191,-0.0912 -0.29191,-0.29191 v -11.73115 c 0,-0.14596 0.12771,-0.27367 0.29191,-0.27367 0.1642,0 0.29191,0.12771 0.29191,0.27367 v 5.58279 h 3.72186 v -5.58279 c 0,-0.14596 0.12771,-0.27367 0.29191,-0.27367 0.1642,0 0.29191,0.12771 0.29191,0.27367 z" />
+        <path
+           inkscape:connector-curvature="0"
+           id="path2322"
+           style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:18.24440765px;font-family:'Ostrich Sans Rounded';-inkscape-font-specification:'Ostrich Sans Rounded';fill:#3d7ebf;fill-opacity:1;stroke:#3d7ebf;stroke-width:0.99766129;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+           d="m 113.94201,111.94282 c 0,-2.09811 0.87573,-3.24751 2.60895,-3.24751 1.75146,0 2.6272,1.13116 2.6272,3.24751 v 5.78347 c 0,1.36833 -0.34665,2.15284 -0.85749,2.64544 -0.51084,0.4926 -1.16764,0.62031 -1.76971,0.62031 -0.58382,0 -1.24062,-0.12771 -1.75146,-0.62031 -0.51084,-0.4926 -0.85749,-1.27711 -0.85749,-2.64544 z m 0.56558,5.78347 c 0,1.84269 0.67504,2.70018 2.04337,2.70018 1.33184,0 2.04338,-0.80276 2.04338,-2.70018 v -5.78347 c 0,-1.87918 -0.67505,-2.68193 -2.04338,-2.68193 -1.38657,0 -2.04337,0.85749 -2.04337,2.68193 z" />
+        <path
+           inkscape:connector-curvature="0"
+           id="path2324"
+           style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:18.24440765px;font-family:'Ostrich Sans Rounded';-inkscape-font-specification:'Ostrich Sans Rounded';fill:#3d7ebf;fill-opacity:1;stroke:#3d7ebf;stroke-width:0.99766129;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+           d="m 120.28479,111.94282 c 0,-2.09811 0.87573,-3.24751 2.60895,-3.24751 1.75147,0 2.6272,1.13116 2.6272,3.24751 v 5.78347 c 0,1.36833 -0.34665,2.15284 -0.85749,2.64544 -0.51084,0.4926 -1.16764,0.62031 -1.76971,0.62031 -0.58382,0 -1.24062,-0.12771 -1.75146,-0.62031 -0.51084,-0.4926 -0.85749,-1.27711 -0.85749,-2.64544 z m 0.56558,5.78347 c 0,1.84269 0.67504,2.70018 2.04337,2.70018 1.33184,0 2.04338,-0.80276 2.04338,-2.70018 v -5.78347 c 0,-1.87918 -0.67505,-2.68193 -2.04338,-2.68193 -1.38657,0 -2.04337,0.85749 -2.04337,2.68193 z" />
+        <path
+           inkscape:connector-curvature="0"
+           id="path2326"
+           style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:18.24440765px;font-family:'Ostrich Sans Rounded';-inkscape-font-specification:'Ostrich Sans Rounded';fill:#3d7ebf;fill-opacity:1;stroke:#3d7ebf;stroke-width:0.99766129;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+           d="m 126.82826,120.99204 c -0.20069,0 -0.29191,-0.0912 -0.29191,-0.29191 v -11.73115 c 0,-0.14596 0.12771,-0.27367 0.29191,-0.27367 0.14596,0 0.27367,0.12771 0.27367,0.27367 v 11.45749 h 3.84957 c 0.1642,0 0.29191,0.12771 0.29191,0.27366 0,0.20069 -0.0912,0.29191 -0.29191,0.29191 z" />
+        <path
+           inkscape:connector-curvature="0"
+           id="path2328"
+           style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:18.24440765px;font-family:'Ostrich Sans Rounded';-inkscape-font-specification:'Ostrich Sans Rounded';fill:#3d7ebf;fill-opacity:1;stroke:#3d7ebf;stroke-width:0.99766129;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+           d="m 136.00263,108.96898 c 0,-0.14596 0.12771,-0.27367 0.29191,-0.27367 0.1642,0 0.29191,0.12771 0.29191,0.27367 v 11.73115 c 0,0.20069 -0.0912,0.29191 -0.29191,0.29191 -0.20069,0 -0.29191,-0.0912 -0.29191,-0.29191 z" />
+        <path
+           inkscape:connector-curvature="0"
+           id="path2330"
+           style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:18.24440765px;font-family:'Ostrich Sans Rounded';-inkscape-font-specification:'Ostrich Sans Rounded';fill:#3d7ebf;fill-opacity:1;stroke:#3d7ebf;stroke-width:0.99766129;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+           d="m 142.79811,120.99204 c -0.12771,0 -0.21894,-0.0547 -0.25543,-0.18244 l -4.17797,-10.34458 v 10.23511 c 0,0.20069 -0.0912,0.29191 -0.29191,0.29191 -0.20068,0 -0.29191,-0.0912 -0.29191,-0.29191 v -11.73115 c 0,-0.31016 0.43787,-0.38313 0.56558,-0.0912 l 4.15973,10.34457 V 108.969 c 0,-0.14596 0.12771,-0.27367 0.29191,-0.27367 0.1642,0 0.29191,0.12771 0.29191,0.27367 v 11.73115 c 0,0.1642 -0.073,0.25542 -0.23718,0.29191 z" />
+        <path
+           inkscape:connector-curvature="0"
+           id="path2332"
+           style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:18.24440765px;font-family:'Ostrich Sans Rounded';-inkscape-font-specification:'Ostrich Sans Rounded';fill:#3d7ebf;fill-opacity:1;stroke:#3d7ebf;stroke-width:0.99766129;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+           d="m 144.99427,114.55177 h 3.55766 c 0.20069,0 0.29191,0.0912 0.29191,0.29191 0,0.14595 -0.12771,0.27366 -0.29191,0.27366 h -3.55766 v 5.58279 c 0,0.1642 -0.12771,0.29191 -0.27366,0.29191 -0.20069,0 -0.29191,-0.0912 -0.29191,-0.29191 v -11.73115 c 0,-0.14596 0.12771,-0.27367 0.29191,-0.27367 h 3.83132 c 0.1642,0 0.29191,0.12771 0.29191,0.27367 0,0.20069 -0.0912,0.29191 -0.29191,0.29191 h -3.55766 z" />
+        <path
+           inkscape:connector-curvature="0"
+           id="path2334"
+           style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:18.24440765px;font-family:'Ostrich Sans Rounded';-inkscape-font-specification:'Ostrich Sans Rounded';fill:#3d7ebf;fill-opacity:1;stroke:#3d7ebf;stroke-width:0.99766129;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+           d="m 149.96759,111.94282 c 0,-2.09811 0.87573,-3.24751 2.60895,-3.24751 1.75146,0 2.62719,1.13116 2.62719,3.24751 v 5.78347 c 0,1.36833 -0.34664,2.15284 -0.85749,2.64544 -0.51084,0.4926 -1.16764,0.62031 -1.7697,0.62031 -0.58383,0 -1.24062,-0.12771 -1.75147,-0.62031 -0.51084,-0.4926 -0.85748,-1.27711 -0.85748,-2.64544 z m 0.56557,5.78347 c 0,1.84269 0.67504,2.70018 2.04338,2.70018 1.33184,0 2.04337,-0.80276 2.04337,-2.70018 v -5.78347 c 0,-1.87918 -0.67504,-2.68193 -2.04337,-2.68193 -1.38658,0 -2.04338,0.85749 -2.04338,2.68193 z" />
+        <path
+           inkscape:connector-curvature="0"
+           id="path2336"
+           style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:18.24440765px;font-family:'Ostrich Sans Rounded';-inkscape-font-specification:'Ostrich Sans Rounded';fill:#3d7ebf;fill-opacity:1;stroke:#3d7ebf;stroke-width:0.99766129;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+           d="m 161.43705,120.68189 c 0,0.20069 -0.0912,0.29191 -0.29191,0.29191 -0.14595,0 -0.27366,-0.12771 -0.27366,-0.29191 v -3.1198 c 0,-1.51428 -0.83925,-2.35352 -2.2988,-2.35352 h -1.642 v 5.47332 c 0,0.20069 -0.0912,0.29191 -0.29191,0.29191 -0.14595,0 -0.27366,-0.12771 -0.27366,-0.29191 v -11.73116 c 0,-0.14595 0.12771,-0.27366 0.27366,-0.27366 h 1.93391 c 1.95215,0 2.84613,1.00344 2.84613,3.2475 0,1.11291 -0.21893,1.93391 -0.67505,2.463 -0.20068,0.23717 -0.43786,0.41962 -0.74802,0.54733 0.23718,0.12771 0.43787,0.25542 0.63856,0.45611 0.52909,0.51084 0.80275,1.24062 0.80275,2.17108 z m -4.50637,-11.43925 v 5.40035 h 1.642 c 1.58726,0 2.26231,-0.74802 2.26231,-2.71842 0,-1.89742 -0.6568,-2.68193 -2.26231,-2.68193 z" />
+        <path
+           inkscape:connector-curvature="0"
+           id="path2338"
+           style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:18.24440765px;font-family:'Ostrich Sans Rounded';-inkscape-font-specification:'Ostrich Sans Rounded';fill:#3d7ebf;fill-opacity:1;stroke:#3d7ebf;stroke-width:0.99766129;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+           d="m 169.33061,111.0306 -2.7914,9.76075 c -0.0547,0.12771 -0.14595,0.20069 -0.29191,0.20069 -0.12771,0 -0.21893,-0.073 -0.27366,-0.20069 l -2.7914,-9.76075 v 9.66953 c 0,0.20069 -0.0912,0.29191 -0.29191,0.29191 -0.20069,0 -0.29191,-0.0912 -0.29191,-0.29191 v -11.73115 c 0,-0.31016 0.47436,-0.38313 0.56558,-0.073 l 3.0833,10.7642 3.10155,-10.7642 c 0.0912,-0.31015 0.56558,-0.23718 0.56558,0.073 v 11.73115 c 0,0.1642 -0.14596,0.29191 -0.29191,0.29191 -0.20069,0 -0.29191,-0.0912 -0.29191,-0.29191 z" />
+        <path
+           inkscape:connector-curvature="0"
+           id="path2340"
+           style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:18.24440765px;font-family:'Ostrich Sans Rounded';-inkscape-font-specification:'Ostrich Sans Rounded';fill:#3d7ebf;fill-opacity:1;stroke:#3d7ebf;stroke-width:0.99766129;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+           d="m 177.33363,120.6454 c 0.0547,0.14595 -0.073,0.29191 -0.21894,0.34664 h -0.073 c -0.14596,0 -0.23718,-0.073 -0.27367,-0.21893 l -0.821,-3.53942 h -3.22926 l -0.821,3.53942 c -0.0547,0.14595 -0.20068,0.27367 -0.34664,0.21893 -0.1642,-0.0547 -0.27367,-0.20069 -0.21893,-0.34664 l 2.71841,-11.73116 c 0.0365,-0.14595 0.12771,-0.21893 0.27367,-0.21893 0.1642,0 0.25542,0.073 0.29191,0.21893 z m -3.01033,-10.39931 -1.4778,6.42203 h 2.97384 z" />
+        <path
+           inkscape:connector-curvature="0"
+           id="path2342"
+           style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:18.24440765px;font-family:'Ostrich Sans Rounded';-inkscape-font-specification:'Ostrich Sans Rounded';fill:#3d7ebf;fill-opacity:1;stroke:#3d7ebf;stroke-width:0.99766129;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+           d="m 184.16758,108.67707 c 0.1642,0 0.29191,0.12771 0.29191,0.27366 0,0.20069 -0.0912,0.29191 -0.29191,0.29191 h -2.64544 v 11.43925 c 0,0.1642 -0.14595,0.29191 -0.29191,0.29191 -0.20069,0 -0.29191,-0.0912 -0.29191,-0.29191 v -11.43925 h -2.66368 c -0.14596,0 -0.27367,-0.12771 -0.27367,-0.29191 0,-0.14595 0.12771,-0.27366 0.27367,-0.27366 z" />
+        <path
+           inkscape:connector-curvature="0"
+           id="path2344"
+           style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:18.24440765px;font-family:'Ostrich Sans Rounded';-inkscape-font-specification:'Ostrich Sans Rounded';fill:#3d7ebf;fill-opacity:1;stroke:#3d7ebf;stroke-width:0.99766129;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+           d="m 185.53336,108.96898 c 0,-0.14596 0.12771,-0.27367 0.29191,-0.27367 0.1642,0 0.29191,0.12771 0.29191,0.27367 v 11.73115 c 0,0.20069 -0.0912,0.29191 -0.29191,0.29191 -0.20069,0 -0.29191,-0.0912 -0.29191,-0.29191 z" />
+        <path
+           inkscape:connector-curvature="0"
+           id="path2346"
+           style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:18.24440765px;font-family:'Ostrich Sans Rounded';-inkscape-font-specification:'Ostrich Sans Rounded';fill:#3d7ebf;fill-opacity:1;stroke:#3d7ebf;stroke-width:0.99766129;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+           d="m 187.31161,111.94282 c 0,-2.09811 0.87573,-3.24751 2.60895,-3.24751 1.75146,0 2.62719,1.13116 2.62719,3.24751 v 5.78347 c 0,1.36833 -0.34664,2.15284 -0.85748,2.64544 -0.51085,0.4926 -1.16764,0.62031 -1.76971,0.62031 -0.58382,0 -1.24062,-0.12771 -1.75146,-0.62031 -0.51085,-0.4926 -0.85749,-1.27711 -0.85749,-2.64544 z m 0.56558,5.78347 c 0,1.84269 0.67504,2.70018 2.04337,2.70018 1.33184,0 2.04337,-0.80276 2.04337,-2.70018 v -5.78347 c 0,-1.87918 -0.67504,-2.68193 -2.04337,-2.68193 -1.38657,0 -2.04337,0.85749 -2.04337,2.68193 z" />
+        <path
+           inkscape:connector-curvature="0"
+           id="path2348"
+           style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:18.24440765px;font-family:'Ostrich Sans Rounded';-inkscape-font-specification:'Ostrich Sans Rounded';fill:#3d7ebf;fill-opacity:1;stroke:#3d7ebf;stroke-width:0.99766129;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+           d="m 198.67161,120.99204 c -0.12771,0 -0.21893,-0.0547 -0.25542,-0.18244 l -4.17797,-10.34458 v 10.23511 c 0,0.20069 -0.0912,0.29191 -0.29191,0.29191 -0.20069,0 -0.29191,-0.0912 -0.29191,-0.29191 v -11.73115 c 0,-0.31016 0.43786,-0.38313 0.56558,-0.0912 l 4.15972,10.34457 V 108.969 c 0,-0.14596 0.12771,-0.27367 0.29191,-0.27367 0.1642,0 0.29191,0.12771 0.29191,0.27367 v 11.73115 c 0,0.1642 -0.073,0.25542 -0.23718,0.29191 z" />
+        <path
+           inkscape:connector-curvature="0"
+           id="path2350"
+           style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:18.24440765px;font-family:'Ostrich Sans Rounded';-inkscape-font-specification:'Ostrich Sans Rounded';fill:#3d7ebf;fill-opacity:1;stroke:#3d7ebf;stroke-width:0.99766129;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+           d="m 206.81432,115.2633 -0.31016,-0.14596 c -0.36489,-0.14595 -0.94871,-0.40137 -1.44131,-0.91222 -0.51084,-0.4926 -0.93046,-1.18588 -0.93046,-2.2623 0,-1.84269 0.93046,-3.26575 2.62719,-3.26575 1.75147,0 2.64544,1.13115 2.64544,3.26575 0,0.14595 -0.12771,0.29191 -0.29191,0.29191 -0.1642,0 -0.29191,-0.14596 -0.29191,-0.29191 0,-1.84269 -0.67504,-2.70018 -2.06162,-2.70018 -1.29535,0 -2.06161,1.11291 -2.06161,2.70018 0,0.87573 0.3284,1.45955 0.76626,1.86093 0.41962,0.40137 0.91222,0.63855 1.27711,0.78451 l 0.29191,0.12771 c 0.31016,0.14595 0.91222,0.38313 1.42306,0.85748 0.51085,0.47436 0.94871,1.16765 0.94871,2.24407 0,0.89397 -0.23717,1.66024 -0.65679,2.22581 -0.47436,0.62031 -1.13116,0.93047 -1.98865,0.93047 -1.75146,0 -2.62719,-1.13116 -2.62719,-3.24751 0,-0.20068 0.0912,-0.29191 0.29191,-0.29191 0.14596,0 0.27367,0.12771 0.27367,0.29191 0,1.18589 0.31015,1.86093 0.71153,2.22582 0.40138,0.38313 0.91222,0.45611 1.35008,0.45611 1.3136,0 2.06162,-0.96695 2.06162,-2.5907 0,-0.87574 -0.34664,-1.45956 -0.76626,-1.84269 -0.21894,-0.18244 -0.43787,-0.3284 -0.67505,-0.43787 z" />
+        <path
+           inkscape:connector-curvature="0"
+           id="path2352"
+           style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:18.24440765px;font-family:'Ostrich Sans Rounded';-inkscape-font-specification:'Ostrich Sans Rounded';fill:#3d7ebf;fill-opacity:1;stroke:#3d7ebf;stroke-width:0.99766129;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+           d="m 214.14059,114.89841 v 5.80172 c 0,0.14596 -0.12772,0.27367 -0.29191,0.27367 -0.1642,0 -0.29192,-0.12771 -0.29192,-0.27367 v -5.80172 l -3.17452,-5.80172 c -0.073,-0.12771 -0.0365,-0.31016 0.10946,-0.38313 0.14596,-0.073 0.31016,-0.0182 0.40138,0.10946 l 2.9556,5.40035 2.97383,-5.40035 c 0.073,-0.12771 0.25543,-0.18244 0.38314,-0.10946 0.14595,0.073 0.20068,0.25542 0.10946,0.38313 z" />
+        <path
+           inkscape:connector-curvature="0"
+           id="path2354"
+           style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:18.24440765px;font-family:'Ostrich Sans Rounded';-inkscape-font-specification:'Ostrich Sans Rounded';fill:#3d7ebf;fill-opacity:1;stroke:#3d7ebf;stroke-width:0.99766129;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+           d="m 220.85395,115.2633 -0.31016,-0.14596 c -0.36489,-0.14595 -0.94871,-0.40137 -1.44131,-0.91222 -0.51084,-0.4926 -0.93046,-1.18588 -0.93046,-2.2623 0,-1.84269 0.93046,-3.26575 2.62719,-3.26575 1.75147,0 2.64544,1.13115 2.64544,3.26575 0,0.14595 -0.12771,0.29191 -0.29191,0.29191 -0.1642,0 -0.29191,-0.14596 -0.29191,-0.29191 0,-1.84269 -0.67504,-2.70018 -2.06162,-2.70018 -1.29535,0 -2.06162,1.11291 -2.06162,2.70018 0,0.87573 0.3284,1.45955 0.76627,1.86093 0.41962,0.40137 0.91222,0.63855 1.27711,0.78451 l 0.29191,0.12771 c 0.31015,0.14595 0.91222,0.38313 1.42306,0.85748 0.51085,0.47436 0.94871,1.16765 0.94871,2.24407 0,0.89397 -0.23718,1.66024 -0.6568,2.22581 -0.47435,0.62031 -1.13115,0.93047 -1.98864,0.93047 -1.75146,0 -2.62719,-1.13116 -2.62719,-3.24751 0,-0.20068 0.0912,-0.29191 0.29191,-0.29191 0.14595,0 0.27366,0.12771 0.27366,0.29191 0,1.18589 0.31016,1.86093 0.71154,2.22582 0.40137,0.38313 0.91222,0.45611 1.35008,0.45611 1.3136,0 2.06162,-0.96695 2.06162,-2.5907 0,-0.87574 -0.34664,-1.45956 -0.76627,-1.84269 -0.21893,-0.18244 -0.43786,-0.3284 -0.67504,-0.43787 z" />
+        <path
+           inkscape:connector-curvature="0"
+           id="path2356"
+           style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:18.24440765px;font-family:'Ostrich Sans Rounded';-inkscape-font-specification:'Ostrich Sans Rounded';fill:#3d7ebf;fill-opacity:1;stroke:#3d7ebf;stroke-width:0.99766129;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+           d="m 230.24185,108.67707 c 0.1642,0 0.29191,0.12771 0.29191,0.27366 0,0.20069 -0.0912,0.29191 -0.29191,0.29191 h -2.64544 v 11.43925 c 0,0.1642 -0.14596,0.29191 -0.29191,0.29191 -0.20069,0 -0.29191,-0.0912 -0.29191,-0.29191 v -11.43925 h -2.66369 c -0.14595,0 -0.27366,-0.12771 -0.27366,-0.29191 0,-0.14595 0.12771,-0.27366 0.27366,-0.27366 z" />
+        <path
+           inkscape:connector-curvature="0"
+           id="path2358"
+           style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:18.24440765px;font-family:'Ostrich Sans Rounded';-inkscape-font-specification:'Ostrich Sans Rounded';fill:#3d7ebf;fill-opacity:1;stroke:#3d7ebf;stroke-width:0.99766129;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+           d="m 232.04547,115.11734 v 5.30913 h 3.55766 c 0.1642,0 0.29191,0.12771 0.29191,0.27366 0,0.20069 -0.0912,0.29191 -0.29191,0.29191 h -3.83133 c -0.20068,0 -0.29191,-0.0912 -0.29191,-0.29191 v -11.73115 c 0,-0.14596 0.12771,-0.27367 0.29191,-0.27367 h 3.83133 c 0.1642,0 0.29191,0.12771 0.29191,0.27367 0,0.20069 -0.0912,0.29191 -0.29191,0.29191 h -3.55766 v 5.29088 h 3.55766 c 0.20069,0 0.29191,0.0912 0.29191,0.29191 0,0.14595 -0.12771,0.27366 -0.29191,0.27366 z" />
+        <path
+           inkscape:connector-curvature="0"
+           id="path2360"
+           style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:18.24440765px;font-family:'Ostrich Sans Rounded';-inkscape-font-specification:'Ostrich Sans Rounded';fill:#3d7ebf;fill-opacity:1;stroke:#3d7ebf;stroke-width:0.99766129;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+           d="m 243.84049,111.0306 -2.7914,9.76075 c -0.0547,0.12771 -0.14595,0.20069 -0.29191,0.20069 -0.12771,0 -0.21893,-0.073 -0.27366,-0.20069 l -2.7914,-9.76075 v 9.66953 c 0,0.20069 -0.0912,0.29191 -0.29191,0.29191 -0.20069,0 -0.29191,-0.0912 -0.29191,-0.29191 v -11.73115 c 0,-0.31016 0.47436,-0.38313 0.56558,-0.073 l 3.0833,10.7642 3.10155,-10.7642 c 0.0912,-0.31015 0.56558,-0.23718 0.56558,0.073 v 11.73115 c 0,0.1642 -0.14596,0.29191 -0.29191,0.29191 -0.20069,0 -0.29191,-0.0912 -0.29191,-0.29191 z" />
+      </g>
+    </g>
+  </g>
+</svg>
diff --git a/aleksis/core/static/js/main.js b/aleksis/core/static/js/main.js
index b625c105f4e0e8a51b581104049bd1d76bcbc200..ba4e56cd54d8909d1f74cb54bbdb3c669b4cfeab 100644
--- a/aleksis/core/static/js/main.js
+++ b/aleksis/core/static/js/main.js
@@ -45,6 +45,19 @@ $(document).ready( function () {
     // Initialize select [MAT]
     $('select').formSelect();
 
+    // If JS is activated, the language form will be auto-submitted
+    $('.language-field select').change(function () {
+
+        // Ugly bug fix to ensure correct value
+        const selectEl = $("select[name=language]");
+        selectEl.val(selectEl.val());
+
+        $(".language-form").submit();
+    });
+
+    // If auto-submit is activated (see above), the language submit must not be visible
+    $(".language-submit-p").hide();
+
     // Initalize print button
     $("#print").click(function () {
         window.print();
@@ -59,6 +72,9 @@ $(document).ready( function () {
     // Initialize Modals [MAT]
     $('.modal').modal();
 
+    // Intialize Tabs [Materialize]
+    $('.tabs').tabs();
+
     $('table.datatable').each(function (index) {
         $(this).DataTable({
             "paging": false
diff --git a/aleksis/core/static/js/progress.js b/aleksis/core/static/js/progress.js
new file mode 100644
index 0000000000000000000000000000000000000000..4d4c0692f39e78af60bdc621544ba7a2c2b720c4
--- /dev/null
+++ b/aleksis/core/static/js/progress.js
@@ -0,0 +1,69 @@
+const OPTIONS = getJSONScript("progress_options");
+
+const STYLE_CLASSES = {
+    10: 'info',
+    20: 'info',
+    25: 'success',
+    30: 'warning',
+    40: 'error',
+};
+
+const ICONS = {
+    10: 'info',
+    20: 'info',
+    25: 'check_circle',
+    30: 'warning',
+    40: 'error',
+};
+
+function setProgress(progress) {
+    $("#progress-bar").css("width", progress + "%");
+}
+
+function renderMessageBox(level, text) {
+    return '<div class="alert ' + STYLE_CLASSES[level] + '"><p><i class="material-icons left">' + ICONS[level] + '</i>' + text + '</p></div>';
+}
+
+function customProgress(progressBarElement, progressBarMessageElement, progress) {
+    setProgress(progress.percent);
+
+    if (progress.hasOwnProperty("messages")) {
+        const messagesBox = $("#messages");
+
+        // Clear container
+        messagesBox.html("")
+
+        // Render message boxes
+        $.each(progress.messages, function (i, message) {
+            messagesBox.append(renderMessageBox(message[0], message[1]));
+        })
+    }
+}
+
+
+function customSuccess(progressBarElement, progressBarMessageElement) {
+    setProgress(100);
+    $("#result-alert").addClass("success");
+    $("#result-icon").text("check_circle");
+    $("#result-text").text(OPTIONS.success);
+    $("#result-box").show();
+}
+
+function customError(progressBarElement, progressBarMessageElement) {
+    setProgress(100);
+    $("#result-alert").addClass("error");
+    $("#result-icon").text("error");
+    $("#result-text").text(OPTIONS.error);
+    $("#result-box").show();
+}
+
+$(document).ready(function () {
+    $("#progress-bar").removeClass("indeterminate").addClass("determinate");
+
+    var progressUrl = Urls["celeryProgress:taskStatus"](OPTIONS.task_id);
+    CeleryProgressBar.initProgressBar(progressUrl, {
+        onProgress: customProgress,
+        onSuccess: customSuccess,
+        onError: customError,
+    });
+});
diff --git a/aleksis/core/static/print.css b/aleksis/core/static/print.css
index 4c511a1aec0e871778890bbee305e48130d65d36..0e2389e1dfb308f2b763fae85887f8a682fd1ad7 100644
--- a/aleksis/core/static/print.css
+++ b/aleksis/core/static/print.css
@@ -38,7 +38,13 @@ header, main, footer {
     height: 0;
 }
 
-.print-layout-table td {
+.print-layout-table, .print-layout-td {
+    width: 190mm;
+    max-width: 190mm;
+    min-width: 190mm;
+}
+
+.print-layout-td {
     padding: 0;
 }
 
@@ -65,6 +71,18 @@ header .row, header .col {
     width: auto;
 }
 
+.page-break {
+    display: block;
+    text-align: center;
+    margin: auto;
+    margin-top: 20px;
+    margin-bottom: 20px;
+    width: 200px;
+    border-top: 1px dashed;
+    color: darkgrey;
+    page-break-after: always;
+}
+
 @media print {
     .header-space {
         height: 35mm;
@@ -87,4 +105,8 @@ header .row, header .col {
         position: fixed;
         bottom: 0;
     }
+
+    .page-break {
+        border: white;
+    }
 }
diff --git a/aleksis/core/static/style.scss b/aleksis/core/static/style.scss
index 10a00caa35ef329b248b7be2f927cd2714ebd244..e2ea3bbd9953477469350e1c477556947bf8f0e2 100644
--- a/aleksis/core/static/style.scss
+++ b/aleksis/core/static/style.scss
@@ -258,6 +258,15 @@ ul.footer-ul {
   line-height: 36px;
 }
 
+// Language form in footer
+
+.language-field .select-dropdown {
+  @extend .white-text;
+}
+
+.language-field svg path:first-child {
+  fill: white;
+}
 
 /* Collections */
 
diff --git a/aleksis/core/static/theme.scss b/aleksis/core/static/theme.scss
index 907c444f5bf99ef3616ae47a4c6df8b2e82b011e..1b38e9bda22c7b63601a4a227d58981e2e3a8d45 100644
--- a/aleksis/core/static/theme.scss
+++ b/aleksis/core/static/theme.scss
@@ -34,11 +34,11 @@
 // 1. Colors
 // ==========================================================================
 
-$primary-color: adjust-color(get-colour(get-config(COLOUR_PRIMARY)), $alpha: 1);
+$primary-color: adjust-color(get-colour(get-preference(theme, primary)), $alpha: 1);
 $primary-color-light: lighten($primary-color, 15%) !default;
 $primary-color-dark: darken($primary-color, 15%) !default;
 
-$secondary-color: adjust-color(get-colour(get-config(COLOUR_SECONDARY)), $alpha: 1);
+$secondary-color: adjust-color(get-colour(get-preference(theme, secondary)), $alpha: 1);
 $success-color: color("green", "base") !default;
 $error-color: color("red", "base") !default;
 $link-color: color("light-blue", "darken-1") !default;
diff --git a/aleksis/core/tables.py b/aleksis/core/tables.py
index b8190272916e32cc67007d31409963d42bd02b8d..f562b61a151487c5047ef2861255a00ef7a5f785 100644
--- a/aleksis/core/tables.py
+++ b/aleksis/core/tables.py
@@ -1,18 +1,72 @@
+from django.utils.translation import gettext_lazy as _
+
 import django_tables2 as tables
 from django_tables2.utils import A
 
 
+class SchoolTermTable(tables.Table):
+    """Table to list persons."""
+
+    class Meta:
+        attrs = {"class": "responsive-table highlight"}
+
+    name = tables.LinkColumn("edit_school_term", args=[A("id")])
+    date_start = tables.Column()
+    date_end = tables.Column()
+    edit = tables.LinkColumn(
+        "edit_school_term",
+        args=[A("id")],
+        text=_("Edit"),
+        attrs={"a": {"class": "btn-flat waves-effect waves-orange orange-text"}},
+        verbose_name=_("Actions"),
+    )
+
+
 class PersonsTable(tables.Table):
+    """Table to list persons."""
+
     class Meta:
-        attrs = {"class": "table table-striped table-bordered table-hover table-responsive-xl"}
+        attrs = {"class": "responsive-table highlight"}
 
     first_name = tables.LinkColumn("person_by_id", args=[A("id")])
     last_name = tables.LinkColumn("person_by_id", args=[A("id")])
 
 
 class GroupsTable(tables.Table):
+    """Table to list groups."""
+
     class Meta:
-        attrs = {"class": "table table-striped table-bordered table-hover table-responsive-xl"}
+        attrs = {"class": "responsive-table highlight"}
 
     name = tables.LinkColumn("group_by_id", args=[A("id")])
     short_name = tables.LinkColumn("group_by_id", args=[A("id")])
+    school_term = tables.Column()
+
+
+class AdditionalFieldsTable(tables.Table):
+    """Table to list group types."""
+
+    class Meta:
+        attrs = {"class": "responsive-table hightlight"}
+
+    title = tables.LinkColumn("edit_additional_field_by_id", args=[A("id")])
+    delete = tables.LinkColumn(
+        "delete_additional_field_by_id",
+        args=[A("id")],
+        verbose_name=_("Delete"),
+        text=_("Delete"),
+        attrs={"a": {"class": "btn-flat waves-effect waves-red"}},
+    )
+
+
+class GroupTypesTable(tables.Table):
+    """Table to list group types."""
+
+    class Meta:
+        attrs = {"class": "responsive-table highlight"}
+
+    name = tables.LinkColumn("edit_group_type_by_id", args=[A("id")])
+    description = tables.LinkColumn("edit_group_type_by_id", args=[A("id")])
+    delete = tables.LinkColumn(
+        "delete_group_type_by_id", args=[A("id")], verbose_name=_("Delete"), text=_("Delete")
+    )
diff --git a/aleksis/core/tasks.py b/aleksis/core/tasks.py
index 2c4c40a8f218d95efa0c26c2e30a8388cce305dd..f391fe05bd6dfd82eaf3e84ddb222bf322881b5f 100644
--- a/aleksis/core/tasks.py
+++ b/aleksis/core/tasks.py
@@ -1,3 +1,4 @@
+from django.conf import settings
 from django.core import management
 
 from .util.core_helpers import celery_optional
@@ -6,13 +7,23 @@ from .util.notifications import send_notification as _send_notification
 
 @celery_optional
 def send_notification(notification: int, resend: bool = False) -> None:
+    """Send a notification object to its recipient.
+
+    :param notification: primary key of the notification object to send
+    :param resend: Define whether to also send if the notification was already sent
+    """
     _send_notification(notification, resend)
 
 
 @celery_optional
 def backup_data() -> None:
+    """Backup database and media using django-dbbackup."""
+    # Assemble command-line options for dbbackup management command
     db_options = "-z " * settings.DBBACKUP_COMPRESS_DB + "-e" * settings.DBBACKUP_ENCRYPT_DB
-    media_options = "-z " * settings.DBBACKUP_COMPRESS_MEDIA + "-e" * settings.DBBACKUP_ENCRYPT_MEDIA
+    media_options = (
+        "-z " * settings.DBBACKUP_COMPRESS_MEDIA + "-e" * settings.DBBACKUP_ENCRYPT_MEDIA
+    )
 
+    # Hand off to dbbackup's management commands
     management.call_command("dbbackup", db_options)
     management.call_command("mediabackup", media_options)
diff --git a/aleksis/core/templates/403.html b/aleksis/core/templates/403.html
index 62ecac05226406b8ba79b61f299ca038c4a17b05..cbc962a93da9f8e8efd91d1af1b493c254834281 100644
--- a/aleksis/core/templates/403.html
+++ b/aleksis/core/templates/403.html
@@ -3,26 +3,26 @@
 
 
 {% block content %}
- <div class="container">
-  <div class="card red">
-   <div class="card white-text">
-    <i class="material-icons small">error_outline</i>
-    <span class="card-title">{% blocktrans %}Error (403): You are not allowed to access the requested page or object.{% endblocktrans %}</span>
-    <p>
-     {% blocktrans %}
-      If you think this is an error in AlekSIS, please contact your site
-     administrators:
-     {% endblocktrans %}
-    </p>
-    <ul>
-     {% for admin in ADMINS %}
-      <li>
-       {{ admin.0 }}
-       &lt;<a class="blue-text text-lighten-2" href="mailto:{{ admin.1 }}">{{ admin.1 }}</a>&gt;
-      </li>
-     {% endfor %}
-    </ul>
-   </div>
+  <div class="container">
+    <div class="card red">
+      <div class="card-content white-text">
+        <i class="material-icons small left">error_outline</i>
+        <span class="card-title">
+        {% if exception %}
+          {{ exception }}
+        {% else %}
+          {% trans "Error" %} (403): {% blocktrans %}You are not allowed to access the requested page or
+          object.{% endblocktrans %}
+        {% endif %}
+        </span>
+        <p>
+          {% blocktrans %}
+            If you think this is an error in AlekSIS, please contact your site
+            administrators:
+          {% endblocktrans %}
+        </p>
+        {% include "core/partials/admins_list.html" %}
+      </div>
+    </div>
   </div>
- </div>
 {% endblock %}
diff --git a/aleksis/core/templates/404.html b/aleksis/core/templates/404.html
index 4acb9e13f10840fefd44b583b1f1fa71b040171d..33c311fcaf4c106d44c53788b44426584d014c33 100644
--- a/aleksis/core/templates/404.html
+++ b/aleksis/core/templates/404.html
@@ -3,30 +3,24 @@
 
 
 {% block content %}
- <div class="container">
-  <div class="card red">
-   <div class="card-content white-text">
-    <i class="material-icons small left">error_outline</i>
-    <span class="card-title">{% blocktrans %}Error (404): The requested page or object was not found.{% endblocktrans %}</span>
-    <p>
-     {% blocktrans %}
-      If you were redirected by a link on an external page,
-      it is possible that that link was outdated.
-     {% endblocktrans %}
-     {% blocktrans %}
-      If you think this is an error in AlekSIS, please contact your site
-      administrators:
-     {% endblocktrans %}
-    </p>
-    <ul>
-     {% for admin in ADMINS %}
-      <li>
-       {{ admin.0 }}
-       &lt;<a class="blue-text text-lighten-2" href="mailto:{{ admin.1 }}">{{ admin.1 }}</a>&gt;
-      </li>
-     {% endfor %}
-    </ul>
-   </div>
+  <div class="container">
+    <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>
+        <p>
+          {% blocktrans %}
+            If you were redirected by a link on an external page,
+            it is possible that that link was outdated.
+          {% endblocktrans %}
+          {% blocktrans %}
+            If you think this is an error in AlekSIS, please contact your site
+            administrators:
+          {% endblocktrans %}
+        </p>
+        {% include "core/partials/admins_list.html" %}
+      </div>
+    </div>
   </div>
- </div>
 {% endblock %}
diff --git a/aleksis/core/templates/500.html b/aleksis/core/templates/500.html
index 195c4348769fb399a050e14e622dc51fde0f1a1c..a084d76f804f9fa1089c225ac4a16db7bf360f69 100644
--- a/aleksis/core/templates/500.html
+++ b/aleksis/core/templates/500.html
@@ -3,18 +3,20 @@
 
 
 {% block content %}
- <div class="container">
-  <div class="card red">
-   <div class="card-content white-text">
-    <div class="material-icons small">error_outline</div>
-    <span class="card-title">{% blocktrans %}Error (500): An unexpected error has occured..{% endblocktrans %}</span>
-    <p>
-     {% blocktrans %}
-      Your site administrators will automatically be notified about this
-     error.
-     {% endblocktrans %}
-    </p>
-   </div>
+  <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">{% trans "Error" %} (500): {% blocktrans %}An unexpected error has
+          occured.{% endblocktrans %}</span>
+        <p>
+          {% blocktrans %}
+            Your site administrators will automatically be notified about this
+            error. You can also contact them directly:
+          {% endblocktrans %}
+        </p>
+        {% include "core/partials/admins_list.html" %}
+      </div>
+    </div>
   </div>
- </div>
 {% endblock %}
diff --git a/aleksis/core/templates/503.html b/aleksis/core/templates/503.html
index 01f94395e38c363e771aa8b31f31ac84343ab824..dd65828745eb846ee1f6b934043996ffa06759e1 100644
--- a/aleksis/core/templates/503.html
+++ b/aleksis/core/templates/503.html
@@ -3,25 +3,19 @@
 
 
 {% block content %}
- <div class="container">
-  <div class="card red">
-   <div class="card-content white-text">
-    <div class="material-icons small">error_outline</div>
-    <span class="card-title">{% blocktrans %}The maintenance mode is currently enabled. Please try again later.{% endblocktrans %}</span>
-    <p>
-     {% blocktrans %}
-      This page is currently unavailable. If this error stays, contact your site administrators:
-     {% endblocktrans %}
-    </p>
-    <ul>
-     {% for admin in ADMINS %}
-      <li>
-       {{ admin.0 }}
-       &lt;<a class="blue-text text-lighten-2" href="mailto:{{ admin.1 }}">{{ admin.1 }}</a>&gt;
-      </li>
-     {% endfor %}
-    </ul>
-   </div>
+  <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 %}The maintenance mode is currently enabled. Please try again
+          later.{% endblocktrans %}</span>
+        <p>
+          {% blocktrans %}
+            This page is currently unavailable. If this error persists, contact your site administrators:
+          {% endblocktrans %}
+        </p>
+        {% include "core/partials/admins_list.html" %}
+      </div>
+    </div>
   </div>
- </div>
 {% endblock %}
diff --git a/aleksis/core/templates/components/chips.html b/aleksis/core/templates/components/chips.html
new file mode 100644
index 0000000000000000000000000000000000000000..6f88dee5f100801f68203500f0cbdac4aed37005
--- /dev/null
+++ b/aleksis/core/templates/components/chips.html
@@ -0,0 +1,29 @@
+{% load material_form_internal %}
+
+<div>
+  {% for group, items in form_field|select_options %}
+    {% for choice, value, selected in items %}
+      <label class="{% if selected %} active{% endif %}">
+        <input type="checkbox"
+               {% if value == None or value == '' %}disabled{% else %}value="{{ value }}"{% endif %}
+                {% if selected %} checked="checked"{% endif %} name="{{ form_field.name }}">
+        <span> {{ choice }} </span>
+      </label>
+    {% endfor %}
+  {% endfor %}
+</div>
+
+<script>
+  $(document).ready(function () {
+    $("input[type='checkbox']").each(function () {
+      $(this).addClass("chips-checkbox");
+      $(this).parent("label").addClass("chips-checkbox");
+    });
+
+    $("label.chips-checkbox > span").click(function () {
+      $(this).parent("label.chips-checkbox").toggleClass("active");
+      let input = $(this).next("input[type='checkbox']");
+      input.prop("checked", !input.prop("checked"));
+    });
+  });
+</script>
diff --git a/aleksis/core/templates/components/msgbox.html b/aleksis/core/templates/components/msgbox.html
index 1eb0d0b8ca211e6c1614c4e38d5db6b537cb9a30..8903ebd1a750f1965326cd59499605872b997fc0 100644
--- a/aleksis/core/templates/components/msgbox.html
+++ b/aleksis/core/templates/components/msgbox.html
@@ -1,10 +1,10 @@
 {% if msg %}
-    <div class="alert {{ status }}">
-        <div>
-            {% if icon != "" %}
-                <i class="material-icons left">{{ icon }}</i>
-            {% endif %}
-            {{ msg }}
-        </div>
+  <div class="alert {{ status }}">
+    <div>
+      {% if icon != "" %}
+        <i class="material-icons left">{{ icon }}</i>
+      {% endif %}
+      {{ msg }}
     </div>
+  </div>
 {% endif %}
diff --git a/aleksis/core/templates/components/pagination.html b/aleksis/core/templates/components/pagination.html
new file mode 100644
index 0000000000000000000000000000000000000000..5eb33cdf8fff272d766a628e1656c109ed87e32c
--- /dev/null
+++ b/aleksis/core/templates/components/pagination.html
@@ -0,0 +1,29 @@
+{% load django_tables2 %}
+
+{% if page and paginator.num_pages > 1 %}
+  <ul class="pagination center">
+    {% if page.has_previous %}
+      <li class="waves-effect">
+        <a href="?page={{ page.previous_page_number }}" class="page-link">
+          <i class="material-icons">chevron_left</i>
+        </a>
+      </li>
+    {% endif %}
+    {% if page.has_previous or page.has_next %}
+      {% for p in page|table_page_range:paginator %}
+        <li class="{% if page.number == p %} active{% endif %} waves-effect">
+          <a {% if p != '...' %}href="?page={{ p }}"{% endif %}>
+            {{ p }}
+          </a>
+        </li>
+      {% endfor %}
+    {% endif %}
+    {% if page.has_next %}
+      <li class="waves-effect">
+        <a href="?page={{ page.next_page_number }}" class="page-link">
+          <i class="material-icons">chevron_right</i>
+        </a>
+      </li>
+    {% endif %}
+  </ul>
+{% endif %}
diff --git a/aleksis/core/templates/core/additional_field/edit.html b/aleksis/core/templates/core/additional_field/edit.html
new file mode 100644
index 0000000000000000000000000000000000000000..cbfbdfffb0db5932710697a3e20eb5776f8a2e8b
--- /dev/null
+++ b/aleksis/core/templates/core/additional_field/edit.html
@@ -0,0 +1,17 @@
+{# -*- engine:django -*- #}
+
+{% extends "core/base.html" %}
+{% load material_form i18n %}
+
+{% block browser_title %}{% blocktrans %}Edit additional field{% endblocktrans %}{% endblock %}
+{% block page_title %}{% blocktrans %}Edit additional field{% endblocktrans %}{% endblock %}
+
+{% block content %}
+
+  <form method="post">
+    {% csrf_token %}
+    {% form form=edit_additional_field_form %}{% endform %}
+    {% include "core/partials/save_button.html" %}
+  </form>
+
+{% endblock %}
diff --git a/aleksis/core/templates/core/additional_field/list.html b/aleksis/core/templates/core/additional_field/list.html
new file mode 100644
index 0000000000000000000000000000000000000000..1ba2a77a32c528b8443f4dc5a4c201b5739414fe
--- /dev/null
+++ b/aleksis/core/templates/core/additional_field/list.html
@@ -0,0 +1,18 @@
+{# -*- engine:django -*- #}
+
+{% extends "core/base.html" %}
+
+{% load i18n %}
+{% load render_table from django_tables2 %}
+
+{% block browser_title %}{% blocktrans %}Additional fields{% endblocktrans %}{% endblock %}
+{% block page_title %}{% blocktrans %}Additional fields{% endblocktrans %}{% endblock %}
+
+{% block content %}
+  <a class="btn green waves-effect waves-light" href="{% url 'create_additional_field' %}">
+    <i class="material-icons left">add</i>
+    {% trans "Create additional field" %}
+  </a>
+
+  {% render_table additional_fields_table %}
+{% endblock %}
diff --git a/aleksis/core/templates/core/base.html b/aleksis/core/templates/core/base.html
index fe536557737d05a04c93d9bfbedfcc63f1bbe60b..07db50c7fb3a29dc169543ae03c4f26dff1ec25f 100644
--- a/aleksis/core/templates/core/base.html
+++ b/aleksis/core/templates/core/base.html
@@ -1,19 +1,19 @@
 {# -*- engine:django -*- #}
 
-
-{% load i18n menu_generator static sass_tags any_js pwa %}
+{% load i18n menu_generator static sass_tags any_js pwa rules %}
+{% get_current_language as LANGUAGE_CODE %}
 
 
 <!DOCTYPE html>
-<html lang="de">
+<html lang="{{ LANGUAGE_CODE }}">
 <head>
-  {% include "core/meta.html" %}
+  {% include "core/partials/meta.html" %}
 
   <title>
     {% block no_browser_title %}
       {% block browser_title %}{% endblock %} —
     {% endblock %}
-    {{ config.SITE_TITLE }}
+    {{ request.site.preferences.general__title }}
   </title>
 
   {# CSS #}
@@ -25,15 +25,13 @@
 
   {# Add i18n names for calendar (for use in datepicker) #}
   {# Passing the locale is not necessary for the scripts to work, but prevents caching issues #}
-  {% get_current_language as LANGUAGE_CODE %}
   <script src="{% url "javascript-catalog" %}?locale={{ LANGUAGE_CODE }}" type="text/javascript"></script>
-  <script src="{% url "calendarweek_i18n_js" %}?first_day=6&amp;locale={{ LANGUAGE_CODE }}" type="text/javascript"></script>
+  <script src="{% url "calendarweek_i18n_js" %}?first_day=6&amp;locale={{ LANGUAGE_CODE }}"
+          type="text/javascript"></script>
 
-  {# Include jQuery to provide $(document).ready #}
+  {# Include jQuery early to provide $(document).ready #}
   {% include_js "jQuery" %}
 
-  <script type="text/javascript" src="{% static 'js/search.js' %}"></script>
-
   {% block extra_head %}{% endblock %}
 </head>
 <body>
@@ -48,7 +46,7 @@
 
   <!-- Nav bar (logged in as, logout) -->
   <nav>
-    <a class="brand-logo" href="/">{{ config.SITE_TITLE }}</a>
+    <a class="brand-logo" href="/">{{ request.site.preferences.general__title }}</a>
 
     <div class="nav-wrapper">
       <ul id="nav-mobile" class="right hide-on-med-and-down">
@@ -65,13 +63,14 @@
   <!-- 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">
-        <object type="image/svg+xml" data="{% static 'img/aleksis-icon.svg' %}" class="aleksis-logo-svg" id="sidenav-logo">
-          <img src="{% static 'img/aleksis-icon.png' %}" alt="AlekSIS icon" />
-        </object>
+        <img src="{% firstof request.site.preferences.theme__logo.url aleksis_banner %}"
+             alt="{{ request.site.preferences.general__title }} – Logo">
       </a>
     </li>
-    {% if user.is_authenticated %}
+    {% has_perm 'core.search' user as search %}
+    {% if search %}
       <li class="search">
         <form method="get" action="{% url "haystack_search" %}" id="search-form" class="autocomplete">
           <div class="search-wrapper">
@@ -82,14 +81,14 @@
       </li>
     {% endif %}
     <li class="no-padding">
-      {% include "core/sidenav.html" %}
+      {% include "core/partials/sidenav.html" %}
     </li>
   </ul>
 </header>
 
 
 <main role="main">
-  {% include 'core/no_person.html' %}
+  {% include 'core/partials/no_person.html' %}
 
   {% if messages %}
     {% for message in messages %}
@@ -122,24 +121,22 @@
   <div class="container">
     <div class="row no-margin footer-row-large">
       <div class="col l6 s12 no-pad-left height-inherit">
-        <p class="white-text valign-bot">
-          {% include 'core/language_form.html' %}
-
-        </p>
+        <div class="white-text valign-bot">
+          {% include 'core/partials/language_form.html' %}
+        </div>
       </div>
       <div class="col xl15 l6 offset-xl01 s12 no-pad-right">
         <ul class="no-margin right">
-          {% include "core/footer-menu.html" %}
+          {% include "core/partials/footer-menu.html" %}
         </ul>
       </div>
     </div>
     <div class="row no-margin footer-row-small">
-            <span class="white-text make-it-higher">
-  {% include 'core/language_form.html' %}
-
-            </span>
+      <div class="white-text make-it-higher">
+        {% include 'core/partials/language_form.html' %}
+      </div>
       <ul class="no-margin footer-ul">
-        {% include "core/footer-menu.html" %}
+        {% include "core/partials/footer-menu.html" %}
       </ul>
     </div>
   </div>
@@ -153,16 +150,16 @@
       </div>
       <div class="right">
         <span id="doit"></span>
-        {% if DB_SETTINGS.footer.impress_url %}
-          <a class="blue-text text-lighten-4" href="{{ DB_SETTINGS.footer.impress_url }}">
+        {% if request.site.preferences.footer__impress_url %}
+          <a class="blue-text text-lighten-4" href="{{ request.site.preferences.footer__impress_url }}">
             {% trans "Impress" %}
           </a>
         {% endif %}
-        {% if DB_SETTINGS.footer.privacy_url and DB_SETTINGS.footer.impress_url %}
+        {% if request.site.preferences.footer__privacy_url and request.site.preferences.footer__impress_url %}
           ·
         {% endif %}
-        {% if DB_SETTINGS.footer.privacy_url %}
-          <a class="blue-text text-lighten-4" href="{{ DB_SETTINGS.footer.privacy_url }}">
+        {% if request.site.preferences.footer__privacy_url %}
+          <a class="blue-text text-lighten-4" href="{{ request.site.preferences.footer__privacy_url }}">
             {% trans "Privacy Policy" %}
           </a>
         {% endif %}
@@ -173,6 +170,7 @@
 
 
 {% include_js "materialize" %}
+<script type="text/javascript" src="{% static 'js/search.js' %}"></script>
 <script type="text/javascript" src="{% static 'js/main.js' %}"></script>
 </body>
 </html>
diff --git a/aleksis/core/templates/core/base_print.html b/aleksis/core/templates/core/base_print.html
index abd42a1c7205de95bacbc84d6d3aa8c29952ecd7..a229d723ed2afa4ec8ad1e7fa0e775889c9d9990 100644
--- a/aleksis/core/templates/core/base_print.html
+++ b/aleksis/core/templates/core/base_print.html
@@ -1,9 +1,11 @@
-{% load static i18n any_js sass_tags cropping %}
+{% load static i18n any_js sass_tags %}
+{% get_current_language as LANGUAGE_CODE %}
+
 
 <!DOCTYPE html>
-<html>
+<html lang="{{ LANGUAGE_CODE }}">
 <head>
-  {% include "core/meta.html" %}
+  {% include "core/partials/meta.html" %}
 
   <title>
     {% block no_browser_title %}
@@ -27,7 +29,7 @@
   <table class="print-layout-table">
     <thead>
     <tr class="no-border">
-      <td>
+      <td class="print-layout-td">
         <div class="header-space">&nbsp;</div>
       </td>
     </tr>
@@ -35,12 +37,14 @@
 
     <tbody>
     <tr class="no-border">
-      <td>
+      <td class="print-layout-td">
         <div class="content">
           <header>
             <div id="print-header" class="row">
               <div class="col s6 logo">
-                <img src="{% cropped_thumbnail SCHOOL 'logo_cropping' max_size="85x85" %}" alt="Logo" id="print-logo"/>
+                {% static "img/aleksis-banner.svg" as aleksis_banner %}
+                <img src="{% firstof request.site.preferences.theme__logo.url aleksis_banner %}" alt="Logo"
+                     id="print-logo"/>
               </div>
               <div class="col s6 right-align">
                 <h5>{% block page_title %}{% endblock %}</h5>
@@ -53,7 +57,7 @@
 
           <footer>
             <div class="left">
-              {{ SCHOOL.name }}
+              {{ request.site.preferences.school__name }}
             </div>
 
             <div class="right">
@@ -67,7 +71,7 @@
 
     <tfoot>
     <tr class="no-border">
-      <td>
+      <td class="print-layout-td">
         <div class="footer-space">&nbsp;</div>
       </td>
     </tr>
diff --git a/aleksis/core/templates/core/crud_events_ul.html b/aleksis/core/templates/core/crud_events_ul.html
deleted file mode 100644
index 8eef4639e7fc56f3ac8a563f2dd7488f8c922479..0000000000000000000000000000000000000000
--- a/aleksis/core/templates/core/crud_events_ul.html
+++ /dev/null
@@ -1,23 +0,0 @@
-<ul class="{{ class_ul }}">
-  {% for event in obj.crud_events %}
-    <li class="{{ class_li }}">
-      <div>
-        <p>{{ event.user.person.full_name }}</p>
-        <small>({{ event.datetime }})</small>
-      </div>
-      <span class="badge badge-light text-dark">
-        {% if event.event_type == event.CREATE %}
-          <span class="mdi mdi-plus"></span>
-        {% elif event.event_type == event.UPDATE %}
-          <span class="mdi mdi-pencil"></span>
-        {% elif event.event_type == event.DELETE %}
-          <span class="mdi mdi-delete"></span>
-        {% elif event.event_type == event.M2M_CHANGE %}
-          <span class="mdi mdi-pencil"></span>
-        {% elif event.event_type == event.M2M_CHANGE_REV %}
-          <span class="mdi mdi-pencil"></span>
-        {% endif %}
-      </span>
-    </li>
-  {% endfor %}
-</ul>
diff --git a/aleksis/core/templates/core/edit_school.html b/aleksis/core/templates/core/edit_school.html
deleted file mode 100644
index 11eeaf0016bc2a1979602abda5f7e45679f2dddf..0000000000000000000000000000000000000000
--- a/aleksis/core/templates/core/edit_school.html
+++ /dev/null
@@ -1,18 +0,0 @@
-{# -*- engine:django -*- #}
-
-{% extends "core/base.html" %}
-
-{% load material_form i18n %}
-
-
-{% block browser_title %}{% blocktrans %}Edit school{% endblocktrans %}{% endblock %}
-{% block page_title %}{% blocktrans %}Edit school{% endblocktrans %}{% endblock %}
-
-
-{% block content %}
-  <form method="post" enctype="multipart/form-data">
-    {% csrf_token %}
-    {% form form=edit_school_form %}{% endform %}
-    {% include "core/save_button.html" %}
-  </form>
-{% endblock %}
diff --git a/aleksis/core/templates/core/group/child_groups.html b/aleksis/core/templates/core/group/child_groups.html
new file mode 100644
index 0000000000000000000000000000000000000000..bb5a429f6ec09effe094e14663f86187258919d3
--- /dev/null
+++ b/aleksis/core/templates/core/group/child_groups.html
@@ -0,0 +1,154 @@
+{# -*- engine:django -*- #}
+
+{% extends "core/base.html" %}
+
+{% load i18n material_form %}
+
+{% block browser_title %}{% blocktrans %}Assign child groups to groups{% endblocktrans %}{% endblock %}
+{% block page_title %}
+  {% blocktrans %}Assign child groups to groups{% endblocktrans %}
+{% endblock %}
+
+
+{% block content %}
+  {% if not page %}
+    <div class="alert info">
+      <p>
+        <i class="material-icons left">info</i>
+        {% blocktrans %}
+          You can use this to assign child groups to groups. Please use the filters below to select groups you want to
+          change and click "Next".
+        {% endblocktrans %}
+      </p>
+    </div>
+
+    <form method="get">
+      {% csrf_token %}
+      {% form form=filter.form %}{% endform %}
+
+      <button type="submit" class="btn green waves-effect waves-light">
+        <i class="material-icons left">refresh</i>
+        {% trans "Update selection" %}
+      </button>
+      <a href="{% url "groups_child_groups" %}" class="btn red waves-effect waves-light">
+        <i class="material-icons left">clear</i>
+        {% trans "Clear all filters" %}
+      </a>
+    </form>
+
+    <h5>{% trans "Currently selected groups" %}</h5>
+
+    {% for group in filter.qs %}
+      <div class="chip">
+        {{ group }}
+      </div>
+    {% endfor %}
+
+    {% if filter.qs %}
+      <p>
+      <form method="post">
+        {% csrf_token %}
+        <button class="btn btn-primary waves-effect waves-light" type="submit" name="page" value="1">
+          {% trans "Start assigning child groups for this groups" %}
+          <i class="material-icons right">arrow_forward</i>
+        </button>
+      </form>
+      </p>
+    {% else %}
+      <div class="alert warning">
+        <p>
+          <i class="material-icons left">warning</i>
+          {% blocktrans %}
+            Please select some groups in order to go on with assigning.
+          {% endblocktrans %}
+        </p>
+      </div>
+    {% endif %}
+  {% else %}
+    <form method="post">
+      <input type="hidden" name="old_page" value="{{ page.number }}">
+
+      <p class="flow-text">
+        {% trans "Current group:" %} {{ group }}
+      </p>
+
+      <div class="alert warning">
+        <p>
+          <i class="material-icons left">warning</i>
+          <strong>{% blocktrans %}Please be careful!{% endblocktrans %}</strong><br/>
+          {% blocktrans %}
+            If you click "Back" or "Next" the current group assignments are not saved.
+            If you click "Save", you will overwrite all existing child group relations for this group with what you
+            selected on this page.
+          {% endblocktrans %}
+        </p>
+
+      </div>
+
+      <div class="row">
+        <p class="left">
+          {% if page.has_previous %}
+            <button class="btn grey waves-effect waves-light" name="page" value="{{ page.previous_page_number }}">
+              <i class="material-icons left">arrow_back</i>
+              {% trans "Back" %}
+            </button>
+          {% endif %}
+          {% if page.has_next %}
+            <button class="btn grey waves-effect waves-light" type="submit" name="page"
+                    value="{{ page.next_page_number }}">
+              {% trans "Next" %}
+              <i class="material-icons right">arrow_forward</i>
+            </button>
+          {% endif %}
+        </p>
+        <p class="right">
+          <button class="btn green waves-effect waves-light" type="submit" name="save">
+            {% trans "Save" %}
+            <i class="material-icons left">save</i>
+          </button>
+          {% if page.has_next %}
+            <button class="btn green waves-effect waves-light" type="submit" name="save"
+                    value="{{ page.next_page_number }}">
+              {% trans "Save and next" %}
+              <i class="material-icons left">save</i>
+            </button>
+          {% endif %}
+        </p>
+      </div>
+
+
+      {% csrf_token %}
+
+      {% include "components/chips.html" with form_field=form.child_groups %}
+
+      <p class="left">
+        {% if page.has_previous %}
+          <button class="btn grey waves-effect waves-light" name="page" value="{{ page.previous_page_number }}">
+            <i class="material-icons left">arrow_back</i>
+            {% trans "Back" %}
+          </button>
+        {% endif %}
+        {% if page.has_next %}
+          <button class="btn grey waves-effect waves-light" type="submit" name="page"
+                  value="{{ page.next_page_number }}">
+            {% trans "Next" %}
+            <i class="material-icons right">arrow_forward</i>
+          </button>
+        {% endif %}
+      </p>
+      <p class="right">
+        <button class="btn green waves-effect waves-light" type="submit" name="save">
+          {% trans "Save" %}
+          <i class="material-icons left">save</i>
+        </button>
+        {% if page.has_next %}
+          <button class="btn green waves-effect waves-light" type="submit" name="save"
+                  value="{{ page.next_page_number }}">
+            {% trans "Save and next" %}
+            <i class="material-icons left">save</i>
+          </button>
+        {% endif %}
+      </p>
+    </form>
+  {% endif %}
+{% endblock %}
diff --git a/aleksis/core/templates/core/edit_group.html b/aleksis/core/templates/core/group/edit.html
similarity index 88%
rename from aleksis/core/templates/core/edit_group.html
rename to aleksis/core/templates/core/group/edit.html
index e7b3188076125020958105ee97ab0de00bd5f766..b26a28d1efc5ee292a257220bca00754512c1b99 100644
--- a/aleksis/core/templates/core/edit_group.html
+++ b/aleksis/core/templates/core/group/edit.html
@@ -11,7 +11,7 @@
   <form method="post">
     {% csrf_token %}
     {% form form=edit_group_form %}{% endform %}
-    {% include "core/save_button.html" %}
+    {% include "core/partials/save_button.html" %}
   </form>
 
 {% endblock %}
diff --git a/aleksis/core/templates/core/group/full.html b/aleksis/core/templates/core/group/full.html
new file mode 100644
index 0000000000000000000000000000000000000000..40856b88f067c90292b93a7ee29d2c89066b2d66
--- /dev/null
+++ b/aleksis/core/templates/core/group/full.html
@@ -0,0 +1,98 @@
+{# -*- engine:django -*- #}
+
+{% extends "core/base.html" %}
+{% load rules %}
+
+{% load i18n static material_form %}
+{% load render_table from django_tables2 %}
+
+{% block browser_title %}{{ group.name }}{% endblock %}
+
+{% block content %}
+  <h4>{{ group.name }} <small class="grey-text">{{ group.short_name }}</small></h4>
+
+  {% has_perm 'core.edit_group' user group as can_change_group %}
+  {% has_perm 'core.change_group_preferences' user group as can_change_group_preferences %}
+  {% has_perm 'core.delete_group' user group as can_delete_group %}
+  {% has_perm 'core.view_group_stats' user group as can_view_group_stats %}
+
+  {% if can_change_group or can_change_group_preferences or can_delete_group %}
+    <p>
+      {% if can_change_group %}
+        <a href="{% url 'edit_group_by_id' group.id %}" class="btn waves-effect waves-light">
+          <i class="material-icons left">edit</i>
+          {% trans "Edit" %}
+        </a>
+      {% endif %}
+
+      {% if can_delete_group %}
+        <a href="{% url 'delete_group_by_id' group.id %}" class="btn waves-effect waves-light red">
+          <i class="material-icons left">delete</i>
+          {% trans "Delete" %}
+        </a>
+      {% endif %}
+
+      {% if can_change_group_preferences %}
+        <a href="{% url "preferences_group" group.id %}" class="btn waves-effect waves-light">
+          <i class="material-icons left">settings</i>
+          {% trans "Change preferences" %}
+        </a>
+      {% endif %}
+    </p>
+  {% endif %}
+
+  <table>
+    <tr>
+      <th>
+        <i class="material-icons center" title="{% trans "Group type" %}">category</i>
+      </th>
+      <td>
+        {{ group.group_type }}
+      </td>
+    </tr>
+    <tr>
+      <th>
+        <i class="material-icons center" title="{% trans "Parent groups" %}">vertical_align_top</i>
+      </th>
+      <td>
+        {{ group.parent_groups.all|join:", " }}
+      </td>
+    </tr>
+  </table>
+
+  {% if can_view_group_stats %}
+    <h5>{% blocktrans %}Statistics{% endblocktrans %}</h5>
+    <ul>
+      <li>
+        {% trans "Count of members" %}: {{ stats.members }}
+      </li>
+      {% if stats.age_avg %}
+        <li>
+          {% trans "Average age" %}: {{ stats.age_avg|floatformat }}
+        </li>
+      {% endif %}
+      {% if stats.age_range_min %}
+        <li>
+          {% trans "Age range" %}: {{ stats.age_range_min }} {% trans "years to" %} {{ stats.age_range_max }} {% trans "years "%}
+        </li>
+      {% endif %}
+    </ul>
+  {% endif %}
+
+  <h5>{% blocktrans %}Owners{% endblocktrans %}</h5>
+  <form method="get">
+    {% form form=owners_filter.form %}{% endform %}
+    {% trans "Search" as caption %}
+    {% include "core/partials/save_button.html" with caption=caption icon="search" %}
+  </form>
+  {% render_table owners_table %}
+
+  <h5>{% blocktrans %}Members{% endblocktrans %}</h5>
+  <form method="get">
+    {% form form=members_filter.form %}{% endform %}
+    {% trans "Search" as caption %}
+    {% include "core/partials/save_button.html" with caption=caption icon="search" %}
+  </form>
+  {% render_table members_table %}
+
+{% endblock %}
diff --git a/aleksis/core/templates/core/groups.html b/aleksis/core/templates/core/group/list.html
similarity index 67%
rename from aleksis/core/templates/core/groups.html
rename to aleksis/core/templates/core/group/list.html
index fab23516a458f55347ebcc152ee6ffac20e004d5..5dff8c86ade16e1b9f77aee7e2494fddb7ea0ba3 100644
--- a/aleksis/core/templates/core/groups.html
+++ b/aleksis/core/templates/core/group/list.html
@@ -2,7 +2,7 @@
 
 {% extends "core/base.html" %}
 
-{% load i18n %}
+{% load i18n material_form %}
 {% load render_table from django_tables2 %}
 
 {% block browser_title %}{% blocktrans %}Groups{% endblocktrans %}{% endblock %}
@@ -14,5 +14,11 @@
     {% trans "Create group" %}
   </a>
 
+  <form method="get">
+    {% form form=groups_filter.form %}{% endform %}
+    {% trans "Search" as caption %}
+    {% include "core/partials/save_button.html" with caption=caption icon="search" %}
+  </form>
+
   {% render_table groups_table %}
 {% endblock %}
diff --git a/aleksis/core/templates/core/group_full.html b/aleksis/core/templates/core/group_full.html
deleted file mode 100644
index 881cfc1158803a50b9dffa49eb235acbf47b4fbe..0000000000000000000000000000000000000000
--- a/aleksis/core/templates/core/group_full.html
+++ /dev/null
@@ -1,42 +0,0 @@
-{# -*- engine:django -*- #}
-
-{% extends "core/base.html" %}
-
-{% load i18n static %}
-{% load render_table from django_tables2 %}
-
-{% block browser_title %}{{ group.name }}{% endblock %}
-
-{% block content %}
-  <h4>{{ group.name }} <small class="grey-text">{{ group.short_name }}</small></h4>
-  <p>
-    <a href="{% url 'edit_group_by_id' group.id %}" class="btn waves-effect waves-light">
-      <i class="material-icons left">edit</i>
-      {% trans "Edit" %}
-    </a>
-  </p>
-
-  <h5>{% blocktrans %}Statistics{% endblocktrans %}</h5>
-  <ul>
-    <li>
-     {% trans "Count of members" %}: {{ stats.members }}
-    </li>
-    {% if stats.age_avg %}
-     <li>
-      {% trans "Average age" %}: {{ stats.age_avg|floatformat }}
-     </li>
-    {% endif %}
-    {% if stats.age_range_min %}
-     <li>
-      {% trans "Age range" %}: {{ stats.age_range_min }} {% trans "years to" %} {{ stats.age_range_max }} {% trans "years "%}
-     </li>
-    {% endif %}
-  </ul>
-
-  <h5>{% blocktrans %}Owners{% endblocktrans %}</h5>
-  {% render_table owners_table %}
-
-  <h5>{% blocktrans %}Members{% endblocktrans %}</h5>
-  {% render_table members_table %}
-
-{% endblock %}
diff --git a/aleksis/core/templates/core/group_type/edit.html b/aleksis/core/templates/core/group_type/edit.html
new file mode 100644
index 0000000000000000000000000000000000000000..843975b16bb6ccbd7cf8eeed8f2d5713f5320e23
--- /dev/null
+++ b/aleksis/core/templates/core/group_type/edit.html
@@ -0,0 +1,17 @@
+{# -*- engine:django -*- #}
+
+{% extends "core/base.html" %}
+{% load material_form i18n %}
+
+{% block browser_title %}{% blocktrans %}Edit group type{% endblocktrans %}{% endblock %}
+{% block page_title %}{% blocktrans %}Edit group type{% endblocktrans %}{% endblock %}
+
+{% block content %}
+
+  <form method="post">
+    {% csrf_token %}
+    {% form form=edit_group_type_form %}{% endform %}
+    {% include "core/partials/save_button.html" %}
+  </form>
+
+{% endblock %}
diff --git a/aleksis/core/templates/core/group_type/list.html b/aleksis/core/templates/core/group_type/list.html
new file mode 100644
index 0000000000000000000000000000000000000000..6344416b1a8292fd5400e57d63c056e1b2839ef8
--- /dev/null
+++ b/aleksis/core/templates/core/group_type/list.html
@@ -0,0 +1,18 @@
+{# -*- engine:django -*- #}
+
+{% extends "core/base.html" %}
+
+{% load i18n %}
+{% load render_table from django_tables2 %}
+
+{% block browser_title %}{% blocktrans %}Group types{% endblocktrans %}{% endblock %}
+{% block page_title %}{% blocktrans %}Group types{% endblocktrans %}{% endblock %}
+
+{% block content %}
+  <a class="btn green waves-effect waves-light" href="{% url 'create_group_type' %}">
+    <i class="material-icons left">add</i>
+    {% trans "Create group type" %}
+  </a>
+
+  {% render_table group_types_table %}
+{% endblock %}
diff --git a/aleksis/core/templates/core/index.html b/aleksis/core/templates/core/index.html
index 2ace14e98652ded4e56c840c180b10690b1fb7d6..683adca900d46a8ba50790829db4d3ecd0ba2716 100644
--- a/aleksis/core/templates/core/index.html
+++ b/aleksis/core/templates/core/index.html
@@ -2,14 +2,13 @@
 {% load i18n static dashboard %}
 
 {% block browser_title %}{% blocktrans %}Home{% endblocktrans %}{% endblock %}
+{% block page_title %}{{ request.site.preferences.general__title }}{% endblock %}
 
 {% block extra_head %}
   {{ media }}
 {% endblock %}
 
 {% block content %}
-  <p class="flow-text">{% blocktrans %}AlekSIS (School Information System){% endblocktrans %}</p>
-
   {% if user.is_authenticated %}
     {% for notification in unread_notifications %}
       <div class="alert primary scale-transition">
@@ -28,14 +27,14 @@
       </div>
     {% endfor %}
 
-    {% include "core/announcements.html" with announcements=announcements %}
+    {% include "core/partials/announcements.html" with announcements=announcements %}
 
     <div class="row" id="live_load">
-        {% for widget in widgets %}
-          <div class="col s12 m12 l6 xl4">
-              {% include_widget widget %}
-          </div>
-        {% endfor %}
+      {% for widget in widgets %}
+        <div class="col s12 m12 l6 xl4">
+          {% include_widget widget %}
+        </div>
+      {% endfor %}
     </div>
 
     <div class="row">
diff --git a/aleksis/core/templates/core/language_form.html b/aleksis/core/templates/core/language_form.html
deleted file mode 100644
index e2ad9fab677925ed83c2c99be174cdc7fd13d696..0000000000000000000000000000000000000000
--- a/aleksis/core/templates/core/language_form.html
+++ /dev/null
@@ -1,18 +0,0 @@
-{# -*- engine:django -*- #}
-
-{% load i18n %}
-
-
-<form action="{% url 'set_language' %}" method="post">
-  {% csrf_token %}
-  <input name="next" type="hidden" value="{{ request.get_full_path }}">
-
-  {% get_current_language as LANGUAGE_CODE %}
-  {% get_available_languages as LANGUAGES %}
-  {% get_language_info_list for LANGUAGES as languages %}
-  {% for language in languages %}
-    <button type="submit" value="{{ language.code }}" name="language" class="blue-text text-lighten-4 btn-flat {% if language == LANGUAGE_CODE %} disabled {% endif %}">
-      {{ language.code }}
-    </button>
-  {% endfor %}
-</form>
diff --git a/aleksis/core/templates/core/data_management.html b/aleksis/core/templates/core/management/data_management.html
similarity index 81%
rename from aleksis/core/templates/core/data_management.html
rename to aleksis/core/templates/core/management/data_management.html
index 53293ffd76e9a6d07251033c356bcae9cc2ee36f..fa8e61f5dac1d1e01e5a773b68015cc9befe8d8f 100644
--- a/aleksis/core/templates/core/data_management.html
+++ b/aleksis/core/templates/core/management/data_management.html
@@ -3,10 +3,10 @@
 {% load i18n menu_generator %}
 
 
-{% block browser_title %}{% blocktrans %}Data management{% endblocktrans%}{% endblock %}
+{% block browser_title %}{% blocktrans %}Data management{% endblocktrans %}{% endblock %}
 {% block page_title %}{% blocktrans %}Data management{% endblocktrans %}{% endblock %}
 
 {% block content %}
   {% get_menu "DATA_MANAGEMENT_MENU" as menu %}
-  {% include "core/on_page_menu.html" %}
+  {% include "core/partials/on_page_menu.html" %}
 {% endblock %}
diff --git a/aleksis/core/templates/core/meta.html b/aleksis/core/templates/core/meta.html
deleted file mode 100644
index c4364430ce15a888e2f5d610ade92d26244b4856..0000000000000000000000000000000000000000
--- a/aleksis/core/templates/core/meta.html
+++ /dev/null
@@ -1,15 +0,0 @@
-{% load i18n static pwa %}
-
-{# Basic meta #}
-<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="{{ config.SITE_DESCRIPTION }}">
-
-{# Favicons #}
-<link href="{% static "icons/favicon_16.png" %}" rel="icon" type="image/png" sizes="16x16">
-<link href="{% static "icons/favicon_32.png" %}" rel="icon" type="image/png" sizes="32x32">
-<link href="{% static "icons/favicon_48.png" %}" rel="icon" type="image/png" sizes="48x48">
-
-{# PWA meta #}
-{% progressive_web_app_meta %}
diff --git a/aleksis/core/templates/core/no_person.html b/aleksis/core/templates/core/no_person.html
deleted file mode 100644
index 3f506809af721cda657d6a3681a4bfa8e7138916..0000000000000000000000000000000000000000
--- a/aleksis/core/templates/core/no_person.html
+++ /dev/null
@@ -1,19 +0,0 @@
-{# -*- engine:django -*- #}
-
-{% load i18n %}
-
-{% if not user.person and not user.is_anonymous %}
-  <div class="alert error">
-    <div>
-      <i class="material-icons left">error</i>
-
-      <p>
-        {% blocktrans %}
-          Your user account is not linked to a person. This means you
-          cannot access any school-related information. Please contact
-          the managers of AlekSIS at your school.
-        {% endblocktrans %}
-      </p>
-    </div>
-  </div>
-{% endif %}
diff --git a/aleksis/core/templates/core/offline.html b/aleksis/core/templates/core/offline.html
deleted file mode 100644
index 5256cbcd91d9073525451ce40ef71a4fba0d8b23..0000000000000000000000000000000000000000
--- a/aleksis/core/templates/core/offline.html
+++ /dev/null
@@ -1,25 +0,0 @@
-{% extends "core/base.html" %}
-
-{% load i18n %}
-
-{% block content %}
-    <h3><i class="material-icons left medium" style="font-size: 2.92rem;">signal_wifi_off</i>{% blocktrans %}No internet connection.{% endblocktrans %}</h3>
-
-    <p class="flow-text">
-      {% blocktrans %}
-        There was an error accessing this page. You probably don't have an internet connection. Check to see if your WiFi or mobile data is turned on and try again. If you think you are connected, please contact the system administrators:
-      {% endblocktrans %}
-      <ul>
-        {% for admin in ADMINS %}
-          <li>
-            {{ admin.0 }}
-            &lt;
-            <a href="mailto:{{ admin.1 }}">
-              {{ admin.1 }}
-            </a>
-            &gt;
-          </li>
-        {% endfor %}
-      </ul>
-    </p>
-{% endblock %}
diff --git a/aleksis/core/templates/core/about.html b/aleksis/core/templates/core/pages/about.html
similarity index 93%
rename from aleksis/core/templates/core/about.html
rename to aleksis/core/templates/core/pages/about.html
index 5b9097f8e294b099151d90dc91fbb583bd9e6dc9..787f18f535967058479861c3547fe69098cc2202 100644
--- a/aleksis/core/templates/core/about.html
+++ b/aleksis/core/templates/core/pages/about.html
@@ -16,8 +16,8 @@
           <p>
             {% blocktrans %}
               This platform is powered by AlekSIS, a web-based school information system (SIS) which can be used
-              to manage and/or publish organisational subjects of educational institutions. AlekSIS is free software and
-              can be used by everyone.
+              to manage and/or publish organisational artifacts of educational institutions. AlekSIS is free software and
+              can be used by anyone.
             {% endblocktrans %}
           </p>
         </div>
@@ -36,7 +36,7 @@
           <p>
             {% blocktrans %}
               The core and the official apps of AlekSIS are licenced under the EUPL, version 1.2 or later. For licence
-              information from third-party apps, if installed, see directly at the respective components below. The
+              information from third-party apps, if installed, refer to the respective components below. The
               licences are marked like this:
             {% endblocktrans %}
           </p>
diff --git a/aleksis/core/templates/core/pages/offline.html b/aleksis/core/templates/core/pages/offline.html
new file mode 100644
index 0000000000000000000000000000000000000000..a6a70dc19f074e8c3f3ede50b5e9b5b80b5682f1
--- /dev/null
+++ b/aleksis/core/templates/core/pages/offline.html
@@ -0,0 +1,17 @@
+{% extends "core/base.html" %}
+
+{% load i18n %}
+
+{% block content %}
+  <h3><i class="material-icons left medium" style="font-size: 2.92rem;">signal_wifi_off</i>{% blocktrans %}No internet
+    connection.{% endblocktrans %}</h3>
+
+  <p class="flow-text">
+    {% blocktrans %}
+      There was an error accessing this page. You probably don't have an internet connection. Check to see if your WiFi
+      or mobile data is turned on and try again. If you think you are connected, please contact the system
+      administrators:
+    {% endblocktrans %}
+  </p>
+  {% include "core/partials/admins_list.html" %}
+{% endblock %}
diff --git a/aleksis/core/templates/core/pages/progress.html b/aleksis/core/templates/core/pages/progress.html
new file mode 100644
index 0000000000000000000000000000000000000000..6292fb0d243b1b2ecb72ff1cec7c3b6339409c18
--- /dev/null
+++ b/aleksis/core/templates/core/pages/progress.html
@@ -0,0 +1,57 @@
+{% extends "core/base.html" %}
+{% load i18n static %}
+
+{% block browser_title %}
+  {{ title }}
+{% endblock %}
+{% block page_title %}
+  {{ title }}
+{% endblock %}
+
+{% block content %}
+
+  <div class="container">
+    <div class="row">
+      <div class="progress center">
+        <div class="indeterminate" style="width: 0;" id="progress-bar"></div>
+      </div>
+      <h6 class="center">
+        {{ progress.title }}
+      </h6>
+    </div>
+    <div class="row">
+      <noscript>
+        <div class="alert warning">
+          <p>
+            <i class="material-icons left">warning</i>
+            {% blocktrans %}
+              Without activated JavaScript the progress status can't be updated.
+            {% endblocktrans %}
+          </p>
+        </div>
+      </noscript>
+    
+      <div id="messages"></div>
+
+      <div id="result-box" style="display: none;">
+        <div class="alert" id="result-alert">
+          <div>
+            <i class="material-icons left" id="result-icon">check_circle</i>
+            <p id="result-text"></p>
+          </div>
+        </div>
+
+        {% url "index" as index_url %}
+        <a class="btn waves-effect waves-light" href="{{ back_url|default:index_url }}">
+          <i class="material-icons left">arrow_back</i>
+          {% trans "Go back" %}
+        </a>
+      </div>
+    </div>
+  </div>
+
+  {{ progress|json_script:"progress_options" }}
+  <script src="{% static "js/helper.js" %}"></script>
+  <script src="{% static "celery_progress/celery_progress.js" %}"></script>
+  <script src="{% static "js/progress.js" %}"></script>
+{% endblock %}
diff --git a/aleksis/core/templates/core/pages/system_status.html b/aleksis/core/templates/core/pages/system_status.html
new file mode 100644
index 0000000000000000000000000000000000000000..89c6ed65e3982a0318838a66ef7f6716935d0af3
--- /dev/null
+++ b/aleksis/core/templates/core/pages/system_status.html
@@ -0,0 +1,166 @@
+{# -*- engine:django -*- #}
+{% extends "core/base.html" %}
+{% load i18n %}
+
+{% block browser_title %}{% blocktrans %}System status{% endblocktrans %}{% endblock %}
+
+{% block page_title %}{% blocktrans %}System status{% endblocktrans %}{% endblock %}
+
+{% block content %}
+  <div class="card">
+    <div class="card-content">
+      <span class="card-title"> {% blocktrans %}System checks{% endblocktrans %}</span>
+
+      {# Maintenance mode #}
+      <div class="row">
+        {% if maintenance_mode %}
+          <a class="btn-flat btn-flat-medium right waves-effect waves-red no-padding"
+             href="{% url 'maintenance_mode_off' %}"><i
+                  class="material-icons small red-text center">power_settings_new</i></a>
+          <div>
+            <p class="flow-text">{% blocktrans %}Maintenance mode enabled{% endblocktrans %}</p>
+            <p class="grey-text">
+              {% blocktrans %}
+                Only admin and visitors from internal IPs can access thesite.
+              {% endblocktrans %}
+            </p>
+          </div>
+          <span class="badge badge-danger mdi mdi-power"><a href="{% url 'maintenance_mode_off' %}"></a></span>
+        {% else %}
+          <a class="btn-flat btn-flat-medium right waves-effect waves-green no-padding"
+             href="{% url 'maintenance_mode_on' %}"><i
+                  class="material-icons small green-text center">power_settings_new</i></a>
+          <div>
+            <p class="flow-text">{% blocktrans %}Maintenance mode disabled{% endblocktrans %}</p>
+            <p class="grey-text">{% blocktrans %}Everyone can access the site.{% endblocktrans %}</p>
+          </div>
+        {% endif %}
+      </div>
+
+      {# Debug mode #}
+      <div class="row">
+        {% if DEBUG %}
+          <i class="material-icons small red-text right">power_settings_new</i>
+          <div>
+            <p class="flow-text">{% blocktrans %}Debug mode enabled{% endblocktrans %}</p>
+            <p class="grey-text">
+              {% blocktrans %}
+                The web server throws back debug information on errors. Do not use in production!
+              {% endblocktrans %}</p>
+          </div>
+        {% else %}
+          <i class="material-icons small green-text right">power_settings_new</i>
+          <div>
+            <p class="flow-text">{% blocktrans %}Debug mode disabled{% endblocktrans %}</p>
+            <p class="grey-text">
+              {% blocktrans %}
+                Debug mode is disabled. Default error pages are displayed on errors.
+              {% endblocktrans %}
+            </p>
+          </div>
+        {% endif %}
+      </div>
+    </div>
+  </div>
+
+  {# Health checks #}
+  <div class="card">
+    <div class="card-content">
+      <span class="card-title"> {% blocktrans %}System health checks{% endblocktrans %}</span>
+
+      <table>
+        <thead>
+          <tr>
+
+            <th colspan="2">{% trans "Service" %}</th>
+            <th>{% trans "Status" %}</th>
+            <th>{% trans "Time taken" %}</th>
+          </tr>
+        </thead>
+        <tbody>
+          {% for plugin in plugins %}
+            <tr>
+              <td>
+                <a class="tooltipped" data-position="top" data-tooltip="{{ plugin.pretty_status }}">
+                {% if plugin.status %}
+                  <i class="material-icons green-text" aria-hidden="true" title="{{ plugin.pretty_status }}">check</i>
+                {% else %}
+                  <i class="material-icons red-text" aria-hidden="true" title="{{ plugin.pretty_status }}">warning</i>
+                {% endif %}
+                </a>
+              </td>
+              <td>{{ plugin.identifier }}</td>
+              <td>
+                {{ plugin.pretty_status }}
+              </td>
+              <td>{{ plugin.time_taken|floatformat:4 }} {% trans "seconds" %}</td>
+            </tr>
+          {% endfor %}
+        </tbody>
+      </table>
+    </div>
+  </div>
+
+  {% if tasks %}
+    <div class="card">
+      <div class="card-content">
+        <span class="card-title"> {% blocktrans %}Celery task results{% endblocktrans %}</span>
+
+        <table>
+          <thead>
+            <tr>
+              <th>{% trans "Task" %}</th>
+              <th>{% trans "ID" %}</th>
+              <th>{% trans "Date done" %}</th>
+              <th>{% trans "Status" %}</th>
+            </tr>
+          </thead>
+          <tbody>
+            {% for task in tasks %}
+              {% if task != None %}
+                <tr>
+                  <td>{{ task.task_name }}</td>
+                  <td>{{ task.task_id }}</td>
+                  <td>{{ task.date_done }}</td>
+                  <td>
+                    {% if task.status == "PENDING" %}
+                      <a class="tooltipped" data-position="top"
+                      data-tooltip="{{ task.status }}">
+                        <i class="material-icons orange-text">hourglass_empty</i>
+                      </a>
+                    {% elif task.status == "STARTED" %}
+                      <a class="tooltipped" data-position="top"
+                      data-tooltip="{{ task.status }}">
+                        <i class="material-icons orange-text">directions_run</i>
+                      </a>
+                    {% elif task.status == "SUCCESS" %}
+                      <a class="tooltipped" data-position="top"
+                      data-tooltip="{{ task.status }}">
+                        <i class="material-icons green-text">done</i>
+                      </a>
+                    {% elif task.status == "FAILURE" %}
+                      <a class="tooltipped" data-position="top"
+                      data-tooltip="{{ task.status }}">
+                        <i class="material-icons red-text">error</i>
+                      </a>
+                    {% elif task.status == "RETRY" %}
+                      <a class="tooltipped" data-position="top"
+                      data-tooltip="{{ task.status }}">
+                        <i class="material-icons orange-text">hourglass_full</i>
+                      </a>
+                    {% elif task.status == "REVOKED" %}
+                      <a class="tooltipped" data-position="top"
+                      data-tooltip="{{ task.status }}">
+                        <i class="material-icons red-text">clear</i>
+                      </a>
+                    {% endif %}
+                  </td>
+                </tr>
+              {% endif %}
+            {% endfor %}
+          </tbody>
+        </table>
+      </div>
+    </div>
+  {% endif %}
+{% endblock %}
diff --git a/aleksis/core/templates/core/partials/admins_list.html b/aleksis/core/templates/core/partials/admins_list.html
new file mode 100644
index 0000000000000000000000000000000000000000..99de17243df9d66fa8a4986af461787ad688fa53
--- /dev/null
+++ b/aleksis/core/templates/core/partials/admins_list.html
@@ -0,0 +1,8 @@
+<ul>
+  {% for admin in ADMINS %}
+    <li>
+      {{ admin.0 }}
+      &lt;<a class="blue-text text-lighten-2" href="mailto:{{ admin.1 }}">{{ admin.1 }}</a>&gt;
+    </li>
+  {% endfor %}
+</ul>
diff --git a/aleksis/core/templates/core/announcements.html b/aleksis/core/templates/core/partials/announcements.html
similarity index 100%
rename from aleksis/core/templates/core/announcements.html
rename to aleksis/core/templates/core/partials/announcements.html
diff --git a/aleksis/core/templates/core/partials/crud_events.html b/aleksis/core/templates/core/partials/crud_events.html
new file mode 100644
index 0000000000000000000000000000000000000000..50e4f73cab492804b163b7ddfe0dda08b9e267b7
--- /dev/null
+++ b/aleksis/core/templates/core/partials/crud_events.html
@@ -0,0 +1,62 @@
+{% load i18n data_helpers %}
+
+<ul class="collection">
+  {% for event in obj.crud_events %}
+    {% if no_m2m and event.event_type == event.M2M_CHANGE or event.event_type == event.M2M_CHANGE_REV %}
+    {% else %}
+      <li class="collection-item">
+        <strong>
+          {% if event.event_type == event.CREATE %}
+            {% blocktrans with person=event.user.person %}
+              Created by {{ person }}
+            {% endblocktrans %}
+          {% elif event.event_type == event.UPDATE %}
+            {% blocktrans with person=event.user.person %}
+              Updated by {{ person }}
+            {% endblocktrans %}
+          {% elif event.event_type == event.DELETE %}
+            {% blocktrans with person=event.user.person %}
+              Deleted by {{ person }}
+            {% endblocktrans %}
+          {% elif event.event_type == event.M2M_CHANGE %}
+            {% blocktrans with person=event.user.person %}
+              Updated by {{ person }}
+            {% endblocktrans %}
+          {% elif event.event_type == event.M2M_CHANGE_REV %}
+            {% blocktrans with person=event.user.person %}
+              Updated by {{ person }}
+            {% endblocktrans %}
+          {% endif %}
+        </strong>
+
+        <div class="left" style="margin-right: 10px;">
+          {% if event.event_type == event.CREATE %}
+            <i class="material-icons">add_circle</i>
+          {% elif event.event_type == event.UPDATE %}
+            <i class="material-icons">edit</i>
+          {% elif event.event_type == event.DELETE %}
+            <i class="material-icons">delete</i>
+          {% elif event.event_type == event.M2M_CHANGE %}
+            <i class="material-icons">edit</i>
+          {% elif event.event_type == event.M2M_CHANGE_REV %}
+            <i class="material-icons">edit</i>
+          {% endif %}
+        </div>
+        <div class="right">
+          {{ event.datetime }}
+        </div>
+        {% parse_json event.changed_fields as changed_fields %}
+        {% if changed_fields %}
+          <ul>
+            {% for field, change in changed_fields.items %}
+              {% verbose_name event.content_type.app_label event.content_type.model field as verbose_name %}
+              <li>
+                {{ verbose_name }}: <s>{{ change.0 }}</s> → {{ change.1 }}
+              </li>
+            {% endfor %}
+          </ul>
+        {% endif %}
+      </li>
+    {% endif %}
+  {% endfor %}
+</ul>
diff --git a/aleksis/core/templates/core/footer-menu.html b/aleksis/core/templates/core/partials/footer-menu.html
similarity index 100%
rename from aleksis/core/templates/core/footer-menu.html
rename to aleksis/core/templates/core/partials/footer-menu.html
diff --git a/aleksis/core/templates/core/partials/language_form.html b/aleksis/core/templates/core/partials/language_form.html
new file mode 100644
index 0000000000000000000000000000000000000000..36ffa81a8c6017c4fdc86dac8749b0b681e0553f
--- /dev/null
+++ b/aleksis/core/templates/core/partials/language_form.html
@@ -0,0 +1,31 @@
+{# -*- engine:django -*- #}
+
+{% load i18n %}
+
+
+<form action="{% url 'set_language' %}" method="post" class="language-form">
+  {% csrf_token %}
+  <input name="next" type="hidden" value="{{ request.get_full_path }}">
+
+  {% get_current_language as LANGUAGE_CODE %}
+  {% get_available_languages as LANGUAGES %}
+  {% get_language_info_list for LANGUAGES as languages %}
+
+  {# Select #}
+  <div class="input-field language-field">
+    <span>{% trans "Language" %}</span>
+    <select name="language" id="language-select">
+      {% for language in languages %}
+        <option value="{{ language.code }}" {% if language.code == LANGUAGE_CODE %}
+                selected {% endif %}>{{ language.name_local }}</option>
+      {% endfor %}
+    </select>
+  </div>
+
+  {# Submit button (only visible if JS isn't activated #}
+  <p class="language-submit-p">
+    <button type="submit" class="btn-flat waves-effect waves-light white-text">
+      {% trans "Select language" %}
+    </button>
+  </p>
+</form>
diff --git a/aleksis/core/templates/core/partials/meta.html b/aleksis/core/templates/core/partials/meta.html
new file mode 100644
index 0000000000000000000000000000000000000000..04f917cf102276a4e6c8d8ab06b9cad0d69033d8
--- /dev/null
+++ b/aleksis/core/templates/core/partials/meta.html
@@ -0,0 +1,10 @@
+{% load pwa favtags %}
+
+<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="generator" content="AlekSIS School Information System"/>
+
+{% place_favicon %}
+{% progressive_web_app_meta %}
diff --git a/aleksis/core/templates/core/partials/no_person.html b/aleksis/core/templates/core/partials/no_person.html
new file mode 100644
index 0000000000000000000000000000000000000000..62b398a1af779b0fc17eef8aa2c0e845760a4e5d
--- /dev/null
+++ b/aleksis/core/templates/core/partials/no_person.html
@@ -0,0 +1,29 @@
+{# -*- engine:django -*- #}
+
+{% load i18n %}
+
+{% if user.person.is_dummy or not user.person and not user.is_anonymous %}
+  <div class="alert error">
+    <div>
+      <i class="material-icons left">error</i>
+
+      {% if user.person.is_dummy %}
+        <p>
+          {% blocktrans %}
+            Your administrator account is not linked to any person. Therefore,
+            a dummy person has been linked to your account.
+          {% endblocktrans %}
+        </p>
+      {% else %}
+        <p>
+          {% blocktrans %}
+            Your user account is not linked to a person. This means you
+            cannot access any school-related information. Please contact
+            the managers of AlekSIS at your school.
+          {% endblocktrans %}
+        </p>
+      {% endif %}
+
+    </div>
+  </div>
+{% endif %}
diff --git a/aleksis/core/templates/core/on_page_menu.html b/aleksis/core/templates/core/partials/on_page_menu.html
similarity index 100%
rename from aleksis/core/templates/core/on_page_menu.html
rename to aleksis/core/templates/core/partials/on_page_menu.html
diff --git a/aleksis/core/templates/core/save_button.html b/aleksis/core/templates/core/partials/save_button.html
similarity index 100%
rename from aleksis/core/templates/core/save_button.html
rename to aleksis/core/templates/core/partials/save_button.html
diff --git a/aleksis/core/templates/core/sidenav.html b/aleksis/core/templates/core/partials/sidenav.html
similarity index 86%
rename from aleksis/core/templates/core/sidenav.html
rename to aleksis/core/templates/core/partials/sidenav.html
index 80acaa19420d45f58a03dfe7a21a18497a61cca4..4c782285f35a3507a755c4e3d96ddffe007ea83f 100644
--- a/aleksis/core/templates/core/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 href="{{ item.url }}">
+        <a class="truncate" href="{{ item.url }}">
           {% if item.icon_class %}
             <i class="{{ item.icon_class }}"></i>
           {% elif item.icon %}
@@ -21,7 +21,7 @@
     {% endif %}
     {% if item.submenu %}
       <li class="bold {% if item.selected %} active {% endif %}">
-        <a class="collapsible-header waves-effect waves-primary" href="{{ item.url|default:"#" }}">
+        <a class="collapsible-header waves-effect waves-primary truncate" href="{{ item.url|default:"#" }}">
           {% if item.icon_class %}
             <i class="{{ item.icon_class }}"></i>
           {% elif item.icon %}
@@ -33,7 +33,7 @@
           <ul>
             {% for menu in item.submenu %}
               <li class="{% if menu.selected %} active {% endif %}">
-                <a class="" href="{{ menu.url }}">
+                <a class="truncate" href="{{ menu.url }}">
                   {% if menu.icon_class %}
                     <i class="{{ menu.icon_class }}"></i>
                   {% elif menu.icon %}
diff --git a/aleksis/core/templates/core/turnable.html b/aleksis/core/templates/core/partials/turnable.html
similarity index 100%
rename from aleksis/core/templates/core/turnable.html
rename to aleksis/core/templates/core/partials/turnable.html
diff --git a/aleksis/core/templates/core/persons_accounts.html b/aleksis/core/templates/core/person/accounts.html
similarity index 100%
rename from aleksis/core/templates/core/persons_accounts.html
rename to aleksis/core/templates/core/person/accounts.html
diff --git a/aleksis/core/templates/core/person/collection.html b/aleksis/core/templates/core/person/collection.html
new file mode 100644
index 0000000000000000000000000000000000000000..c23fa2360c00977485a9d32373ae826d7fe66b6d
--- /dev/null
+++ b/aleksis/core/templates/core/person/collection.html
@@ -0,0 +1,8 @@
+<div class="collection">
+  {% for person in persons %}
+    <a class="collection-item" href="{% url "person_by_id" person.pk %}">
+      <i class="material-icons left">person</i>
+      {{ person }}
+    </a>
+  {% endfor %}
+</div>
diff --git a/aleksis/core/templates/core/edit_person.html b/aleksis/core/templates/core/person/edit.html
similarity index 89%
rename from aleksis/core/templates/core/edit_person.html
rename to aleksis/core/templates/core/person/edit.html
index 8a5d0ca39a8fa0cbfd437519a09723ce59278c2d..8f854610e3424b9da47f142cef41b71c9e0fb097 100644
--- a/aleksis/core/templates/core/edit_person.html
+++ b/aleksis/core/templates/core/person/edit.html
@@ -14,7 +14,7 @@
   <form method="post" enctype="multipart/form-data">
     {% csrf_token %}
     {% form form=edit_person_form %}{% endform %}
-    {% include "core/save_button.html" %}
+    {% include "core/partials/save_button.html" %}
   </form>
 
 {% endblock %}
diff --git a/aleksis/core/templates/core/person/full.html b/aleksis/core/templates/core/person/full.html
new file mode 100644
index 0000000000000000000000000000000000000000..bc7b66d8ea52cce7c4e64a9e0b3e443c5b18c37b
--- /dev/null
+++ b/aleksis/core/templates/core/person/full.html
@@ -0,0 +1,144 @@
+{# -*- engine:django -*- #}
+
+{% extends "core/base.html" %}
+
+{% load i18n static cropping rules material_form %}
+{% load render_table from django_tables2 %}
+
+{% block browser_title %}{{ person.first_name }} {{ person.last_name }}{% endblock %}
+
+{% block content %}
+  <h4>{{ person.first_name }} {{ person.last_name }}</h4>
+
+  {% has_perm 'core.edit_person' user person as can_change_person %}
+  {% has_perm 'core.change_person_preferences' user person as can_change_person_preferences %}
+  {% has_perm 'core.delete_person' user person as can_delete_person %}
+
+  {% if can_change_person or can_change_person_preferences or can_delete_person %}
+    <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 %}
+
+      {% 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 %}
+    </p>
+  {% endif %}
+
+  <h5>{% blocktrans %}Contact details{% endblocktrans %}</h5>
+  <div class="row">
+    <div class="col s12 m4">
+      {% has_perm 'core.view_photo' user person as can_view_photo %}
+      {% if person.photo and can_view_photo %}
+        <img class="person-img" src="{% cropped_thumbnail person 'photo_cropping' max_size='300x400' %}"
+             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">
+
+          </td>
+          <td>
+            <i class="material-icons small">person</i>
+          </td>
+          <td>{{ person.first_name }}</td>
+          <td>{{ person.additional_name }}</td>
+          <td>{{ person.last_name }}</td>
+        </tr>
+        <tr>
+          <td>
+            <i class="material-icons small">face</i>
+          </td>
+          <td colspan="3">{{ person.get_sex_display }}</td>
+        </tr>
+        {% has_perm 'core.view_address' user person as can_view_address %}
+        {% if can_view_address %}
+          <tr>
+            <td>
+              <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>
+          </tr>
+        {% endif %}
+        {% has_perm 'core.view_contact_details' user person as can_view_contact_details %}
+        {% if can_view_contact_details %}
+          <tr>
+            <td>
+              <i class="material-icons small">phone</i>
+            </td>
+            <td>{{ person.phone_number }}</td>
+            <td>{{ person.mobile_number }}</td>
+          </tr>
+          <tr>
+            <td>
+              <i class="material-icons small">email</i>
+            </td>
+            <td colspan="3">{{ person.email }}</td>
+          </tr>
+        {% endif %}
+        {% has_perm 'core.view_personal_details' user person as can_view_personal_details %}
+        {% if can_view_personal_details %}
+          <tr>
+            <td>
+              <i class="material-icons small">cake</i>
+            </td>
+            <td colspan="3">{{ person.date_of_birth|date }}</td>
+          </tr>
+        {% endif %}
+      </table>
+    </div>
+    {% if person.description %}
+      <div class="col s12 m12">
+        <h5>{% trans "Description" %}</h5>
+        <p>
+          {{ person.description }}
+        </p>
+      </div>
+    {% endif %}
+  </div>
+
+  {% if person.children.all and can_view_personal_details %}
+    <div class="col s12 m12">
+      <h5>{% trans "Children" %}</h5>
+      {% include "core/person/collection.html" with persons=person.children.all %}
+    </div>
+  {% endif %}
+
+  {% if person.guardians.all and can_view_personal_details %}
+    <div class="col s12 m12">
+      <h5>{% trans "Guardians / Parents" %}</h5>
+      {% include "core/person/collection.html" with persons=person.guardians.all %}
+    </div>
+  {% endif %}
+
+  {% has_perm 'core.view_person_groups' user person as can_view_groups %}
+  {% if can_view_groups %}
+    <h5>{% blocktrans %}Groups{% endblocktrans %}</h5>
+    <form method="get">
+      {% form form=groups_filter.form %}{% endform %}
+      {% trans "Search" as caption %}
+      {% include "core/partials/save_button.html" with caption=caption icon="search" %}
+    </form>
+    {% render_table groups_table %}
+  {% endif %}
+{% endblock %}
diff --git a/aleksis/core/templates/core/person/list.html b/aleksis/core/templates/core/person/list.html
new file mode 100644
index 0000000000000000000000000000000000000000..816e7254b8bc58d8a6a155afdd97d17310042509
--- /dev/null
+++ b/aleksis/core/templates/core/person/list.html
@@ -0,0 +1,28 @@
+{# -*- engine:django -*- #}
+
+{% extends "core/base.html" %}
+
+{% load i18n rules material_form %}
+{% load render_table from django_tables2 %}
+
+{% block browser_title %}{% blocktrans %}Persons{% endblocktrans %}{% endblock %}
+{% block page_title %}{% blocktrans %}Persons{% endblocktrans %}{% endblock %}
+
+{% block content %}
+  {% has_perm 'core.create_person' user person as can_create_person %}
+
+  {% if can_create_person %}
+    <a class="btn green waves-effect waves-light" href="{% url 'create_person' %}">
+      <i class="material-icons left">add</i>
+      {% trans "Create person" %}
+    </a>
+  {% endif %}
+
+  <form method="get">
+    {% form form=persons_filter.form %}{% endform %}
+    {% trans "Search" as caption %}
+    {% include "core/partials/save_button.html" with caption=caption icon="search" %}
+  </form>
+
+  {% render_table persons_table %}
+{% endblock %}
diff --git a/aleksis/core/templates/core/person_full.html b/aleksis/core/templates/core/person_full.html
deleted file mode 100644
index 8f9ceed60aa7ca1a99bde9e3682f5988d2f29f75..0000000000000000000000000000000000000000
--- a/aleksis/core/templates/core/person_full.html
+++ /dev/null
@@ -1,81 +0,0 @@
-{# -*- engine:django -*- #}
-
-{% extends "core/base.html" %}
-
-{% load i18n static cropping %}
-{% load render_table from django_tables2 %}
-
-{% block browser_title %}{{ person.first_name }} {{ person.last_name }}{% endblock %}
-
-{% block content %}
-  <h4>{{ person.first_name }} {{ person.last_name }}</h4>
-  <p>
-    <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>
-  </p>
-
-  <h5>{% blocktrans %}Contact details{% endblocktrans %}</h5>
-  <div class="row">
-    <div class="col s12 m4">
-      {% if person.photo %}
-        <img class="person-img" src="{% cropped_thumbnail person 'photo_cropping' max_size='300x400' %}"
-             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="table table-responsive-xl table-border table-striped">
-        <tr>
-          <td rowspan="6">
-
-          </td>
-          <td>
-            <i class="material-icons small">person</i>
-          </td>
-          <td>{{ person.first_name }}</td>
-          <td>{{ person.additional_name }}</td>
-          <td>{{ person.last_name }}</td>
-        </tr>
-        <tr>
-          <td>
-            <i class="material-icons small">face</i>
-          </td>
-          <td colspan="3">{{ person.get_sex_display }}</td>
-        </tr>
-        <tr>
-          <td>
-            <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>
-        </tr>
-        <tr>
-          <td>
-            <i class="material-icons small">phone</i>
-          </td>
-          <td>{{ person.phone_number }}</td>
-          <td>{{ person.mobile_number }}</td>
-        </tr>
-        <tr>
-          <td>
-            <i class="material-icons small">email</i>
-          </td>
-          <td colspan="3">{{ person.email }}</td>
-        </tr>
-        <tr>
-          <td>
-            <i class="material-icons small">cake</i>
-          </td>
-          <td colspan="3">{{ person.date_of_birth|date }}</td>
-        </tr>
-      </table>
-    </div>
-  </div>
-
-  <h5>{% blocktrans %}Groups{% endblocktrans %}</h5>
-  {% render_table groups_table %}
-{% endblock %}
diff --git a/aleksis/core/templates/core/persons.html b/aleksis/core/templates/core/persons.html
deleted file mode 100644
index dfecfb7c52b64cbdf49e70d06d1148e82128577f..0000000000000000000000000000000000000000
--- a/aleksis/core/templates/core/persons.html
+++ /dev/null
@@ -1,13 +0,0 @@
-{# -*- engine:django -*- #}
-
-{% extends "core/base.html" %}
-
-{% load i18n %}
-{% load render_table from django_tables2 %}
-
-{% block browser_title %}{% blocktrans %}Persons{% endblocktrans %}{% endblock %}
-{% block page_title %}{% blocktrans %}Persons{% endblocktrans %}{% endblock %}
-
-{% block content %}
-  {% render_table persons_table %}
-{% endblock %}
diff --git a/aleksis/core/templates/core/school_management.html b/aleksis/core/templates/core/school_management.html
deleted file mode 100644
index 10fa00621866199f18877f8836d4e762efc474e4..0000000000000000000000000000000000000000
--- a/aleksis/core/templates/core/school_management.html
+++ /dev/null
@@ -1,12 +0,0 @@
-{# -*- engine:django -*- #}
-{% extends "core/base.html" %}
-{% load i18n menu_generator %}
-
-
-{% block browser_title %}{% blocktrans %}School management{% endblocktrans %}{% endblock %}
-{% block page_title %}{% blocktrans %}School management{% endblocktrans %}{% endblock %}
-
-{% block content %}
-  {% get_menu "SCHOOL_MANAGEMENT_MENU" as menu %}
-  {% include "core/on_page_menu.html" %}
-{% endblock %}
diff --git a/aleksis/core/templates/core/school_term/create.html b/aleksis/core/templates/core/school_term/create.html
new file mode 100644
index 0000000000000000000000000000000000000000..a3e049112caeaf84095dde68c7fd8d7a32f75602
--- /dev/null
+++ b/aleksis/core/templates/core/school_term/create.html
@@ -0,0 +1,17 @@
+{# -*- engine:django -*- #}
+
+{% extends "core/base.html" %}
+{% load material_form i18n %}
+
+{% block browser_title %}{% blocktrans %}Create school term{% endblocktrans %}{% endblock %}
+{% block page_title %}{% blocktrans %}Create school term{% endblocktrans %}{% endblock %}
+
+{% block content %}
+
+  <form method="post">
+    {% csrf_token %}
+    {% form form=form %}{% endform %}
+    {% include "core/partials/save_button.html" %}
+  </form>
+
+{% endblock %}
diff --git a/aleksis/core/templates/core/edit_schoolterm.html b/aleksis/core/templates/core/school_term/edit.html
similarity index 79%
rename from aleksis/core/templates/core/edit_schoolterm.html
rename to aleksis/core/templates/core/school_term/edit.html
index 8b5c0b98bc73a5476d99c2d504b698a7bc98a652..aa1b1dcf5015e876d0b9aa316d25da31673f0a3f 100644
--- a/aleksis/core/templates/core/edit_schoolterm.html
+++ b/aleksis/core/templates/core/school_term/edit.html
@@ -1,10 +1,8 @@
 {# -*- engine:django -*- #}
 
 {% extends "core/base.html" %}
-
 {% load material_form i18n %}
 
-
 {% block browser_title %}{% blocktrans %}Edit school term{% endblocktrans %}{% endblock %}
 {% block page_title %}{% blocktrans %}Edit school term{% endblocktrans %}{% endblock %}
 
@@ -12,8 +10,8 @@
 
   <form method="post">
     {% csrf_token %}
-    {% form form=edit_term_form %}{% endform %}
-    {% include "core/save_button.html" %}
+    {% form form=form %}{% endform %}
+    {% include "core/partials/save_button.html" %}
   </form>
 
 {% endblock %}
diff --git a/aleksis/core/templates/core/school_term/list.html b/aleksis/core/templates/core/school_term/list.html
new file mode 100644
index 0000000000000000000000000000000000000000..14aa2f0a697a3efd54b27154b431ef3f424923de
--- /dev/null
+++ b/aleksis/core/templates/core/school_term/list.html
@@ -0,0 +1,18 @@
+{# -*- engine:django -*- #}
+
+{% extends "core/base.html" %}
+
+{% load i18n %}
+{% load render_table from django_tables2 %}
+
+{% block browser_title %}{% blocktrans %}School terms{% endblocktrans %}{% endblock %}
+{% block page_title %}{% blocktrans %}School terms{% endblocktrans %}{% endblock %}
+
+{% block content %}
+  <a class="btn green waves-effect waves-light" href="{% url 'create_school_term' %}">
+    <i class="material-icons left">add</i>
+    {% trans "Create school term" %}
+  </a>
+
+  {% render_table table %}
+{% endblock %}
diff --git a/aleksis/core/templates/core/system_status.html b/aleksis/core/templates/core/system_status.html
deleted file mode 100644
index 2b0e80732538d250ac9e35902b4d7363160bc6ea..0000000000000000000000000000000000000000
--- a/aleksis/core/templates/core/system_status.html
+++ /dev/null
@@ -1,65 +0,0 @@
-{# -*- engine:django -*- #}
-{% extends "core/base.html" %}
-{% load i18n %}
-
-{% block browser_title %}{% blocktrans %}System status{% endblocktrans %}{% endblock %}
-
-{% block page_title %}{% blocktrans %}System status{% endblocktrans %}{% endblock %}
-
-{% block content %}
-  <div class="card">
-    <div class="card-content">
-      <span class="card-title"> {% blocktrans %}System checks{% endblocktrans %}</span>
-
-      {# Maintenance mode #}
-      <div class="row">
-        {% if maintenance_mode %}
-          <a class="btn btn-flat btn-flat-medium right waves-effect waves-red no-padding"
-             href="{% url 'maintenance_mode_off' %}"><i
-                  class="material-icons small red-text center">power_settings_new</i></a>
-          <div>
-            <p class="flow-text">{% blocktrans %}Maintenance mode enabled{% endblocktrans %}</p>
-            <p class="grey-text">
-              {% blocktrans %}
-                Only admin and visitors from internal IPs can access thesite.
-              {% endblocktrans %}
-            </p>
-          </div>
-          <span class="badge badge-danger mdi mdi-power"><a href="{% url 'maintenance_mode_off' %}"></a></span>
-        {% else %}
-          <a class="btn btn-flat btn-flat-medium right waves-effect waves-green no-padding"
-             href="{% url 'maintenance_mode_on' %}"><i
-                  class="material-icons small green-text center">power_settings_new</i></a>
-          <div>
-            <p class="flow-text">{% blocktrans %}Maintenance mode disabled{% endblocktrans %}</p>
-            <p class="grey-text">{% blocktrans %}Everyone can access the site.{% endblocktrans %}</p>
-          </div>
-        {% endif %}
-      </div>
-
-      {# Debug mode #}
-      <div class="row">
-        {% if DEBUG %}
-          <i class="material-icons small red-text right">power_settings_new</i>
-          <div>
-            <p class="flow-text">{% blocktrans %}Debug mode enabled{% endblocktrans %}</p>
-            <p class="grey-text">
-              {% blocktrans %}
-                The web server throws back debug information on errors. Do not use in production!
-              {% endblocktrans %}</p>
-          </div>
-        {% else %}
-          <i class="material-icons small green-text right">power_settings_new</i>
-          <div>
-            <p class="flow-text">{% blocktrans %}Debug mode disabled{% endblocktrans %}</p>
-            <p class="grey-text">
-              {% blocktrans %}
-                Debug mode is disabled. Default error pages are displayed on errors.
-              {% endblocktrans %}
-            </p>
-          </div>
-        {% endif %}
-      </div>
-    </div>
-  </div>
-{% endblock %}
diff --git a/aleksis/core/templates/dynamic_preferences/form.html b/aleksis/core/templates/dynamic_preferences/form.html
new file mode 100644
index 0000000000000000000000000000000000000000..da3d608285c43673bd27932d0d19cef303b1cef7
--- /dev/null
+++ b/aleksis/core/templates/dynamic_preferences/form.html
@@ -0,0 +1,28 @@
+{% extends "core/base.html" %}
+{% load i18n material_form %}
+
+{% block browser_title %}
+  {% trans "Preferences" %}
+{% endblock %}
+{% block page_title %}
+  {% if registry_name == "site" %}
+    {% blocktrans %}Site preferences{% endblocktrans %}
+  {% elif registry_name == "person" and instance == request.user.person %}
+    {% blocktrans %}My preferences{% endblocktrans %}
+  {% else %}
+    {% blocktrans with instace=instance %}Preferences for {{ instance }}{% endblocktrans %}
+  {% endif %}
+{% endblock %}
+
+{% block content %}
+  <div class="row">
+    {% include "dynamic_preferences/sections.html" with registry=registry sections=registry.sections active_section=section %}
+  </div>
+  <div class="row">
+    <form action="" enctype="multipart/form-data" method="post">
+      {% csrf_token %}
+      {% form form=form %}{% endform %}
+      {% include "core/partials/save_button.html" with caption=_("Save preferences") %}
+    </form>
+  </div>
+{% endblock %}
diff --git a/aleksis/core/templates/dynamic_preferences/sections.html b/aleksis/core/templates/dynamic_preferences/sections.html
new file mode 100644
index 0000000000000000000000000000000000000000..39b97496fd4542cfbad440f3571eb2b6f8df86d2
--- /dev/null
+++ b/aleksis/core/templates/dynamic_preferences/sections.html
@@ -0,0 +1,18 @@
+{% load i18n %}
+<ul class="tabs">
+  <li class="tab ">
+    <a href="{% url registry_url %}"
+       class="{% if not active_section %}active{% endif %}"
+       target="_self">
+      {% trans "All" %}
+    </a>
+    {% for section in registry.section_objects.values %}
+      <li class="tab">
+        <a class="{% if active_section == section.name %}active{% endif %}"
+           href="{% url registry_url section.name %}"
+           target="_self">
+          {{ section.verbose_name }}
+        </a>
+      </li>
+    {% endfor %}
+</ul>
diff --git a/aleksis/core/templates/martor/editor.html b/aleksis/core/templates/martor/editor.html
deleted file mode 100644
index dccea1c07d93851a03392b1fef97228098912b9e..0000000000000000000000000000000000000000
--- a/aleksis/core/templates/martor/editor.html
+++ /dev/null
@@ -1,71 +0,0 @@
-{% load i18n %}
-<div class="main-martor main-martor-{{ field_name }} card" data-field-name="{{ field_name }}">
-    <div class="section-martor card-content">
-        <div class="row">
-            <ul class="tabs col s12 m3 primary-color-text">
-                <li class="tab">
-                    <a class="item" data-tab="editor-tab-{{ field_name }}" href="#editor-{{ field_name }}">
-                        <i class="material-icons">edit</i>
-                        {#                        {% trans "Editor" %}#}
-                    </a>
-                </li>
-                <li class="tab">
-                    <a class="item" data-tab="preview-tab-{{ field_name }}" href="#preview-{{ field_name }}">
-                        <i class="material-icons">slideshow</i>
-                        {#                        {% trans "Preview" %}#}
-                    </a>
-                </li>
-
-            </ul>
-            <div class="col s12 m9">
-                {% include "martor/toolbar.html" %}
-            </div>
-        </div>
-        <div class="col s12 tab segment" data-tab="editor-tab-{{ field_name }}"
-             id="editor-{{ field_name }}">
-            <div class="ui active dimmer upload-progress" style="display:none">
-                <div class="ui text loader">{% trans "Uploading... please wait..." %}</div>
-            </div>
-
-            <div id="martor-{{ field_name }}" class="martor-field martor-field-{{ field_name }}"></div>
-            {{ martor }}
-            <i class="angle double down grey icon expand-editor"></i>
-        </div>
-        <div class="martor-preview col s12 tab segment" data-tab="preview-tab-{{ field_name }}"
-             id="preview-{{ field_name }}">
-            <p>{% trans "Nothing to preview" %}</p>
-        </div>
-    </div><!-- end  /.section-martor -->
-
-    {% include 'martor/guide.html' %}
-    {% include 'martor/emoji.html' %}
-    <script type="text/javascript">
-        $(document).ready(function () {
-            $('.tabs').tabs();
-            $('.dropdown-trigger').dropdown();
-            $('.modal').modal();
-        });
-    </script>
-    <style type="text/css">
-        .main-martor .card-content {
-            margin: 0;
-            padding: 0;
-        }
-
-        .martor-toolbar .btn-flat {
-            padding: 0 6px;
-        }
-
-        .maximize::before {
-            content: "fullscreen";
-        }
-
-        .minimize::before {
-            content: "fullscreen_exit";
-        }
-
-        .main-martor .tab a i {
-            line-height: inherit;
-        }
-    </style>
-</div>
diff --git a/aleksis/core/templates/martor/emoji.html b/aleksis/core/templates/martor/emoji.html
deleted file mode 100644
index 61bc0a1843efa512bc15147b786d21e94758b2a5..0000000000000000000000000000000000000000
--- a/aleksis/core/templates/martor/emoji.html
+++ /dev/null
@@ -1,14 +0,0 @@
-{% load i18n %}
-<div class="ui large modal scrolling transition modal-emoji">
-    <i class="close icon"></i>
-    <div class="header"><i class="help circle icon"></i> {% trans "Select Emoji to Insert" %}</div>
-    <div class="content emoji-content-base">
-        <div class="ui segment emoji-loader-init">
-            <div class="ui active inverted dimmer">
-                <div class="ui text loader">{% trans "Preparing emojis..." %}</div>
-            </div>
-        </div>
-        <div class="ui grid emoji-content-body">
-        </div>
-    </div>
-</div>
diff --git a/aleksis/core/templates/martor/guide.html b/aleksis/core/templates/martor/guide.html
deleted file mode 100644
index 60d35b70403144b39a63df6b4d7ee084cc844268..0000000000000000000000000000000000000000
--- a/aleksis/core/templates/martor/guide.html
+++ /dev/null
@@ -1,176 +0,0 @@
-{% load i18n static %}
-<div class="ui medium modal scrolling transition modal-help-guide modal" id="modal-{{ field_name }}">
-    {#  <i class="close icon"></i>#}
-
-    <div class="modal-content">
-        <a href="#" class="modal-close btn-flat right"><i class="material-icons">close</i></a>
-
-        <h4><i class="help circle icon"></i> {% trans "Markdown Guide" %}</h4>
-        <p>{% blocktrans with doc_url='http://commonmark.org/help/' %}This site is powered by Markdown. For full
-            documentation,
-            <a href="{{ doc_url }}" target="_blank">click here</a>{% endblocktrans %}</p>
-        <table class="ui celled table markdown-reference">
-            <thead>
-            <tr>
-                <th>{% trans "Code" %}</th>
-                <th>{% trans "Or" %}</th>
-                <th>Linux/Windows</th>
-                <th>Mac OS</th>
-                <th>{% trans "... to Get" %}</th>
-            </tr>
-            </thead>
-            <tbody>
-            {#            <tr>#}
-            {#                <td>:emoji_name:</td>#}
-            {#                <td>&mdash;</td>#}
-            {#                <td>&mdash;</td>#}
-            {#                <td>&mdash;</td>#}
-            {#                <td><img class="marked-emoji" src="{% static 'plugins/images/heart.png' %}"></td>#}
-            {#            </tr>#}
-            {#            <tr>#}
-            {#                <td>@[username]</td>#}
-            {#                <td>&mdash;</td>#}
-            {#                <td>Ctrl+M</td>#}
-            {#                <td>Command+M</td>#}
-            {#                <td><a href="#">@username</a></td>#}
-            {#            </tr>#}
-            {#            <tr>#}
-            {#                <td colspan="5"></td>#}
-            {#            </tr>#}
-            <tr>
-                <td>*Italic*</td>
-                <td>_Italic_</td>
-                <td>Ctrl+I</td>
-                <td>Command+I</td>
-                <td><em>Italic</em></td>
-            </tr>
-            <tr>
-                <td>**Bold**</td>
-                <td>__Bold__</td>
-                <td>Ctrl+B</td>
-                <td>Command+B</td>
-                <td><strong>Bold</strong></td>
-            </tr>
-            <tr>
-                <td>++Underscores++</td>
-                <td>&mdash;</td>
-                <td>Shift+U</td>
-                <td>Option+U</td>
-                <td>
-                    <ins>Underscores</ins>
-                </td>
-            </tr>
-            <tr>
-                <td>~~Strikethrough~~</td>
-                <td>&mdash;</td>
-                <td>Shift+S</td>
-                <td>Option+S</td>
-                <td>
-                    <del>Strikethrough</del>
-                </td>
-            </tr>
-            <tr>
-                <td># Heading 1</td>
-                <td>Heading 1<br> =========</td>
-                <td>Ctrl+Alt+1</td>
-                <td>Command+Option+1</td>
-                <td><h1>Heading 1</h1></td>
-            </tr>
-            <tr>
-                <td>## Heading 2</td>
-                <td>Heading 2<br> -----------</td>
-                <td>Ctrl+Alt+2</td>
-                <td>Command+Option+2</td>
-                <td><h2>Heading 2</h2></td>
-            </tr>
-            <tr>
-                <td>[Link](http://a.com)</td>
-                <td>[Link][1]<br> &#8285;<br> [1]: http://b.org</td>
-                <td>Ctrl+L</td>
-                <td>Command+L</td>
-                <td>
-                    <a href="http://commonmark.org/">Link</a>
-                </td>
-            </tr>
-            {#            <tr>#}
-            {#                <td>![Image](http://url/a.png)</td>#}
-            {#                <td>![Image][1]<br> &#8285;<br> [1]: http://url/b.jpg</td>#}
-            {#                <td>Ctrl+Shift+I</td>#}
-            {#                <td>Command+Option+I</td>#}
-            {#                <td><img src="{% static 'plugins/images/commonmark.png' %}" width="36" height="36" alt="Markdown"></td>#}
-            {#            </tr>#}
-            <tr>
-                <td>&gt; Blockquote</td>
-                <td>&mdash;</td>
-                <td>Ctrl+Q</td>
-                <td>Command+Q</td>
-                <td>
-                    <blockquote>Blockquote</blockquote>
-                </td>
-            </tr>
-            <tr>
-                <td>A paragraph.<br><br> A paragraph after 1 blank line.</td>
-                <td>&mdash;</td>
-                <td>&mdash;</td>
-                <td>&mdash;</td>
-                <td><p>A paragraph.</p>
-                    <p>A paragraph after 1 blank line.</p></td>
-            </tr>
-            <tr>
-                <td><p>* List<br> * List<br> * List</p></td>
-                <td><p> - List<br> - List<br> - List<br></p></td>
-                <td>Ctrl+U</td>
-                <td>Command+U</td>
-                <td>
-                    <ul>
-                        <li>List</li>
-                        <li>List</li>
-                        <li>List</li>
-                    </ul>
-                </td>
-            </tr>
-            <tr>
-                <td><p> 1. One<br> 2. Two<br> 3. Three</p></td>
-                <td><p> 1) One<br> 2) Two<br> 3) Three</p></td>
-                <td>Ctrl+Shift+O</td>
-                <td>Command+Option+O</td>
-                <td>
-                    <ol>
-                        <li>One</li>
-                        <li>Two</li>
-                        <li>Three</li>
-                    </ol>
-                </td>
-            </tr>
-            <tr>
-                <td>Horizontal Rule<br><br> -----------</td>
-                <td>Horizontal Rule<br><br> ***********</td>
-                <td>Ctrl+H</td>
-                <td>Command+H</td>
-                <td>Horizontal Rule
-                    <hr>
-                </td>
-            </tr>
-            <tr>
-                <td>`Inline code` with backticks</td>
-                <td>&mdash;</td>
-                <td>Ctrl+Alt+C</td>
-                <td>Command+Option+C</td>
-                <td><code>Inline code</code> with backticks</td>
-            </tr>
-            <tr>
-                <td>```<br> def whatever(foo):<br>&nbsp;&nbsp;&nbsp;&nbsp;return foo<br>```</td>
-                <td><b>with tab / 4 spaces</b><br>....def whatever(foo):<br>....&nbsp;&nbsp;&nbsp;&nbsp;return foo</td>
-                <td>Ctrl+Alt+P</td>
-                <td>Command+Option+P</td>
-                <td>
-                    <pre>def whatever(foo):<br/>    return foo</pre>
-                </td>
-            </tr>
-            </tbody>
-        </table>
-    </div>
-    <div class="modal-footer">
-        <a href="#" class="modal-close waves-effect waves-green btn-flat">Close</a>
-    </div>
-</div>
diff --git a/aleksis/core/templates/martor/toolbar.html b/aleksis/core/templates/martor/toolbar.html
deleted file mode 100644
index da37e7a4d5c8936f34b28e8975846cc24fa172b1..0000000000000000000000000000000000000000
--- a/aleksis/core/templates/martor/toolbar.html
+++ /dev/null
@@ -1,89 +0,0 @@
-{% load i18n %}
-<div class="ui right floated item martor-toolbar">
-    <a class="markdown-selector btn-flat markdown-bold" title="{% trans 'Bold' %} (Ctrl+B)">
-        <i class="material-icons">format_bold</i>
-    </a>
-    <a class="markdown-selector btn-flat markdown-italic" title="{% trans 'Italic' %} (Ctrl+I)">
-        <i class="material-icons">format_italic</i>
-    </a>
-    <a class="markdown-selector btn-flat markdown-horizontal"
-       title="{% trans 'Horizontal Line' %} (Ctrl+H)">
-        <i class="material-icons">remove</i>
-    </a>
-
-
-    <a class="dropdown-trigger btn-flat" title="{% trans 'Heading' %}" data-target="dropdown-heading-{{ field_name }}">
-        <i class="material-icons">format_size</i>
-    </a>
-    <ul class="dropdown-content" id="dropdown-heading-{{ field_name }}">
-        <li>
-            <a class=" markdown-selector markdown-h1" title="{% trans 'H' %} 1 (Ctrl+Alt+1)">{% trans 'H' %} 1</a>
-        </li>
-        <li>
-            <a class=" markdown-selector markdown-h2" title="{% trans 'H' %} 2 (Ctrl+Alt+2)">{% trans 'H' %} 2</a>
-        </li>
-        <li>
-            <a class=" markdown-selector markdown-h3" title="{% trans 'H' %} 3 (Ctrl+Alt+3)">{% trans 'H' %} 3</a>
-        </li>
-    </ul>
-
-
-    <a class="dropdown-trigger btn-flat" title="{% trans 'Pre or Code' %}"
-       data-target="dropdown-precode-{{ field_name }}">
-        <i class="material-icons">code</i>
-    </a>
-
-    <ul class="dropdown-content" id="dropdown-precode-{{ field_name }}">
-        <li>
-            <a class="item markdown-selector markdown-pre" title="{% trans 'Pre' %} (Ctrl+Alt+P)">{% trans 'Pre' %}</a>
-        </li>
-        <li>
-            <a class="item markdown-selector markdown-code"
-               title="{% trans 'Code' %} (Ctrl+Alt+C)">{% trans 'Code' %}</a>
-        </li>
-    </ul>
-
-
-    <a class="markdown-selector btn-flat markdown-blockquote"
-       title="{% trans 'Quote' %} (Ctrl+Q)">
-        <i class="material-icons">format_quote</i>
-    </a>
-    <a class="markdown-selector btn-flat markdown-unordered-list"
-       title="{% trans 'Unordered List' %} (Ctrl+U)">
-        <i class="material-icons">format_list_bulleted</i>
-    </a>
-    <a class="markdown-selector btn-flat markdown-ordered-list"
-       title="{% trans 'Ordered List' %} (Ctrl+Shift+O)">
-        <i class="material-icons">format_list_numbered</i>
-    </a>
-
-    <a class="markdown-selector btn-flat markdown-link" title="{% trans 'URL/Link' %} (Ctrl+L)">
-        <i class="material-icons">insert_link</i>
-    </a>
-    {#    <a class="markdown-selector btn-flat markdown-image-link"#}
-    {#       title="{% trans 'Insert Image Link' %} (Ctrl+Shift+I)">#}
-    {#        <i class="material-icons">insert_photo</i>#}
-    {#    </a>#}
-    {#    <a class="markdown-selector btn-flat markdown-image-upload"#}
-    {#       title="{% trans 'Upload an Image' %}">#}
-    {#        <i class="material-icons">file_upload</i>#}
-    {#        <input name="markdown-image-upload" class="button" type="file" accept="image/*"#}
-    {#               title="{% trans 'Upload an Image' %}">#}
-    {#    </a>#}
-    {#    <a class="markdown-selector btn-flat markdown-emoji" title="{% trans 'Insert Emoji' %}">#}
-    {#        <i class="material-icons">face</i>#}
-    {#    </a>#}
-    {#    <a class="markdown-selector btn-flat markdown-direct-mention"#}
-    {#       title="{% trans 'Direct Mention a User' %} (Ctrl+M)">#}
-    {#        <i class="material-icons">people</i>#}
-    {#    </a>#}
-
-    <a class="markdown-selector btn-flat markdown-toggle-maximize"
-       title="{% trans 'Full Screen' %}">
-        <i class="material-icons maximize icon"></i>
-    </a>
-    <a class="markdown-selector btn-flat markdown-help modal-trigger"
-       title="{% trans 'Markdown Guide (Help)' %}" href="#modal-{{ field_name }}">
-        <i class="material-icons">help</i>
-    </a>
-</div>
diff --git a/aleksis/core/templates/registration/logged_out.html b/aleksis/core/templates/registration/logged_out.html
deleted file mode 100644
index 48a7d1535dfb6071d29e3f2fee1fd2247650b763..0000000000000000000000000000000000000000
--- a/aleksis/core/templates/registration/logged_out.html
+++ /dev/null
@@ -1,8 +0,0 @@
-{% include 'partials/header.html' %}
-
-<main>
-    <p class="flow-text">Du bist nun abgemeldet.</p>
-    <a href="{% url 'login' %}">Wieder anmelden?</a>
-</main>
-
-{% include 'partials/footer.html' %}
diff --git a/aleksis/core/templates/registration/login.html b/aleksis/core/templates/registration/login.html
deleted file mode 100644
index e38f93a893ed8c8ad9a1a65db9e6ed3b3666b029..0000000000000000000000000000000000000000
--- a/aleksis/core/templates/registration/login.html
+++ /dev/null
@@ -1,24 +0,0 @@
-{# -*- engine:django -*- #}
-{% extends "core/base.html" %}
-{% load material_form i18n %}
-
-{% block content %}
-  {% if next %}
-    {% if user.is_authenticated %}
-      <p>
-        Your account doesn't have access to this page. To proceed,
-        please login with an account that has access.
-      </p>
-    {% else %}
-      <p>Please login to see this page.</p>
-    {% endif %}
-  {% endif %}
-
-  <form method="post" action="{% url 'login' %}">
-    {% csrf_token %}
-    {% form form=form %}{% endform %}
-    <button type="submit" class="btn waves-effect waves-light green">
-      {% blocktrans %}Login{% endblocktrans %}
-    </button>
-  </form>
-{% endblock %}
diff --git a/aleksis/core/templates/search/search.html b/aleksis/core/templates/search/search.html
index d463f1569793168d6644c80a1dc63fe60cb06234..caff1cc6f62f30c67dcb0e2fe082dfa08db389a8 100644
--- a/aleksis/core/templates/search/search.html
+++ b/aleksis/core/templates/search/search.html
@@ -14,18 +14,7 @@
     <input type="text" name="{{ form.q.name }}" id="{{ form.q.id }}" value="{% firstof form.q.value "" %}"
            placeholder="{% trans "Search Term" %}">
 
-    <div>
-      {% for group, items in form.models|select_options %}
-        {% for choice, value, selected in items %}
-          <label class="{% if selected %} active{% endif %}">
-            <input type="checkbox"
-                   {% if value == None or value == '' %}disabled{% else %}value="{{ value }}"{% endif %}
-                    {% if selected %} checked="checked"{% endif %} name="{{ form.models.name }}">
-            <span> {{ choice }} </span>
-          </label>
-        {% endfor %}
-      {% endfor %}
-    </div>
+    {% include "components/chips.html" with form_field=form.models %}
 
     <p>
       <button type="submit" class="btn waves-effect waves-light green">
@@ -61,14 +50,20 @@
               </a>
             </li>
           {% else %}
-            <li class="disabled"><a href="#"><i class="material-icons">chevron_left</i></a></li>
+            <li class="disabled">
+              <a href="#"><i class="material-icons">chevron_left</i></a>
+            </li>
           {% endif %}
 
           {% for page_num in page.paginator.page_range %}
             {% if page.number == page_num %}
-              <li class="active"><a href="#">{{ page_num }}</a></li>
+              <li class="active">
+                <a href="#">{{ page_num }}</a>
+              </li>
             {% else %}
-              <li class="waves-effect"><a href="?q={{ query }}&amp;page={{ page_num }}">{{ page_num }}</a></li>
+              <li class="waves-effect">
+                <a href="?q={{ query }}&amp;page={{ page_num }}">{{ page_num }}</a>
+              </li>
             {% endif %}
           {% endfor %}
 
@@ -79,7 +74,9 @@
               </a>
             </li>
           {% else %}
-            <li class="disabled"><a href="#"><i class="material-icons">chevron_right</i></a></li>
+            <li class="disabled">
+              <a href="#"><i class="material-icons">chevron_right</i></a>
+            </li>
           {% endif %}
         </ul>
       {% endif %}
@@ -95,19 +92,4 @@
 
 
   </form>
-
-  <script>
-    $(document).ready(function () {
-      $("input[type='checkbox']").each(function () {
-        $(this).addClass("chips-checkbox");
-        $(this).parent("label").addClass("chips-checkbox");
-      });
-
-      $("label.chips-checkbox > span").click(function () {
-        $(this).parent("label.chips-checkbox").toggleClass("active");
-        let input = $(this).next("input[type='checkbox']");
-        input.prop("checked", !input.prop("checked"));
-      });
-    });
-  </script>
 {% endblock %}
diff --git a/aleksis/core/templates/templated_email/notification.email b/aleksis/core/templates/templated_email/notification.email
index bb39f5861876b1dc1c4c39639de649ec6d6590eb..8e61fd0df28b5bc6342c891921831e147e9cc8d9 100644
--- a/aleksis/core/templates/templated_email/notification.email
+++ b/aleksis/core/templates/templated_email/notification.email
@@ -2,21 +2,48 @@
 
 {% block subject %} {% trans "New notification for" %} {{ notification_user }} {% endblock %}
 
+{% block plain %}
+    {% blocktrans with notification_user=notification_user %}Dear {{ notification_user }},{% endblocktrans %}
+
+    {% trans "we got a new notification for you:" %}
+
+    {{ notification.title }}
+
+    {{ notification.description }}
+
+    {% if notification.link %}
+        {% trans "More information" %} → {{ notification.link }}
+    {% endif %}
+
+    {% blocktrans with trans_sender=notification.sender trans_created_at=notification.created_at %}
+        Sent by {{ trans_sender }} at {{ trans_created_at }}
+    {% endblocktrans %}
+
+    {% trans "Your AlekSIS team" %}
+{% endblock %}
+
 {% block html %}
 <main>
-    <p>{% trans "Dear" %} {{ notification_user }}, <br>
-        {% trans "we got a new notification for you:" %}</p>
+    <p>{% blocktrans with notification_user=notification_user %}Dear {{ notification_user }},{% endblocktrans %}</p>
+
+    <p>{% trans "we got a new notification for you:" %}</p>
+
     <blockquote>
+        <h5>{{ notification.title }}</h5>
         <p>{{ notification.description }}</p>
         {% if notification.link %}
             <a href="{{ notification.link }}">{% trans "More information" %} →</a>
         {% endif %}
     </blockquote>
 
-    {% blocktrans with trans_sender=notification.sender trans_created_at=notification.created_at %}
-    <p>By {{ trans_sender }} at {{ trans_created_at }}</p>
+    <p>
+        {% blocktrans with trans_sender=notification.sender trans_created_at=notification.created_at %}
+            Sent by {{ trans_sender }} at {{ trans_created_at }}
+        {% endblocktrans %}
+    </p>
 
-    <i>Your AlekSIS team</i>
-    {% endblocktrans %}
+    <p>
+        <i>{% trans "Your AlekSIS team" %}</i>
+    </p>
 </main>
 {% endblock %}
diff --git a/aleksis/core/templates/two_factor/core/login.html b/aleksis/core/templates/two_factor/core/login.html
index 3357221f0fa5b1c2d2093d742fd399afe3a2d74c..6e0df1a4e4fa35a411f9e71cb41868808c942045 100644
--- a/aleksis/core/templates/two_factor/core/login.html
+++ b/aleksis/core/templates/two_factor/core/login.html
@@ -9,29 +9,38 @@
 {% block content %}
   <h4>{% trans "Login" %}</h4>
 
-  <div class="alert primary">
-    <p>
-      <i class="material-icons left">info</i>
+  {% if wizard.steps.current == "auth" and user.is_authenticated %}
+    <div class="alert warning">
+      <p>
+        <i class="material-icons left">warning</i>
+        {% blocktrans %}You have no permission to view this page. Please login with an other account.{% endblocktrans %}
+      </p>
+    </div>
+  {% else %}
+    <div class="alert primary">
+      <p>
+        <i class="material-icons left">info</i>
 
-      {% if wizard.steps.current == 'auth' %}
-        {% blocktrans %}Enter your credentials.{% endblocktrans %}
-      {% elif wizard.steps.current == 'token' %}
-        {% if device.method == 'call' %}
-          {% blocktrans %}We are calling your phone right now, please enter the
-            digits you hear.{% endblocktrans %}
-        {% elif device.method == 'sms' %}
-          {% blocktrans %}We sent you a text message, please enter the tokens we
-            sent.{% endblocktrans %}
-        {% else %}
-          {% blocktrans %}Please enter the tokens generated by your token
-            generator.{% endblocktrans %}
+        {% if wizard.steps.current == 'auth' %}
+          {% blocktrans %}Please login to see this page.{% endblocktrans %}
+        {% elif wizard.steps.current == 'token' %}
+          {% if device.method == 'call' %}
+            {% blocktrans %}We are calling your phone right now, please enter the
+              digits you hear.{% endblocktrans %}
+          {% elif device.method == 'sms' %}
+            {% blocktrans %}We sent you a text message, please enter the tokens we
+              sent.{% endblocktrans %}
+          {% else %}
+            {% blocktrans %}Please enter the tokens generated by your token
+              generator.{% endblocktrans %}
+          {% endif %}
+        {% elif wizard.steps.current == 'backup' %}
+          {% blocktrans %}Use this form for entering backup tokens for logging in.
+            These tokens have been generated for you to print and keep safe. Please
+            enter one of these backup tokens to login to your account.{% endblocktrans %}
         {% endif %}
-      {% elif wizard.steps.current == 'backup' %}
-        {% blocktrans %}Use this form for entering backup tokens for logging in.
-          These tokens have been generated for you to print and keep safe. Please
-          enter one of these backup tokens to login to your account.{% endblocktrans %}
-      {% endif %}
-  </div>
+    </div>
+  {% endif %}
 
 
   <form action="" method="post">
diff --git a/aleksis/core/templates/two_factor/core/setup.html b/aleksis/core/templates/two_factor/core/setup.html
index 304a3eb7f9b6702ccc0a6886367ccf81ca82444a..2eb4ecb2828ac60104f284b2d75857e9b6be303b 100644
--- a/aleksis/core/templates/two_factor/core/setup.html
+++ b/aleksis/core/templates/two_factor/core/setup.html
@@ -84,7 +84,9 @@
     {% include "two_factor/_wizard_forms.html" %}
 
     {# hidden submit button to enable [enter] key #}
-    <div style="margin-left: -9999px"><input type="submit" value=""/></div>
+    <div style="margin-left: -9999px">
+      <input type="submit" value=""/>
+    </div>
 
     {% include "two_factor/_wizard_actions.html" %}
   </form>
diff --git a/aleksis/core/templatetags/apps.py b/aleksis/core/templatetags/apps.py
index 6f48c994917130290f2c4400fca753c4df11b06f..c25203daf2e256a436b42ef223e7976db0de8de6 100644
--- a/aleksis/core/templatetags/apps.py
+++ b/aleksis/core/templatetags/apps.py
@@ -2,4 +2,4 @@ from django.apps import AppConfig
 
 
 class TemplatetagsConfig(AppConfig):
-    name = 'templatetags'
+    name = "templatetags"
diff --git a/aleksis/core/templatetags/dashboard.py b/aleksis/core/templatetags/dashboard.py
index b051084f88008a1b30758d46f1d91c9695a408e1..b2c1541e8fedf08162d795f0224dce9bc7bfbbd6 100644
--- a/aleksis/core/templatetags/dashboard.py
+++ b/aleksis/core/templatetags/dashboard.py
@@ -5,8 +5,7 @@ register = Library()
 
 @register.simple_tag
 def include_widget(widget) -> dict:
-    """ Render a template with context from a defined widget """
-
+    """Render a template with context from a defined widget."""
     template = loader.get_template(widget.get_template())
     context = widget.get_context()
 
diff --git a/aleksis/core/templatetags/data_helpers.py b/aleksis/core/templatetags/data_helpers.py
index 0b195d136d86587f892635d6d0e41be1aebada68..f7393c73fd86eba6f861f87e321bf8dc5cbce6fd 100644
--- a/aleksis/core/templatetags/data_helpers.py
+++ b/aleksis/core/templatetags/data_helpers.py
@@ -1,14 +1,15 @@
-from typing import Any
+import json
+from typing import Any, Optional, Union
 
 from django import template
+from django.contrib.contenttypes.models import ContentType
 
 register = template.Library()
 
 
 @register.filter
 def get_dict(value: Any, arg: Any) -> Any:
-    """Gets an attribute of an object dynamically from a string name"""
-
+    """Get an attribute of an object dynamically from a string name."""
     if hasattr(value, str(arg)):
         return getattr(value, arg)
     elif hasattr(value, "keys") and arg in value.keys():
@@ -17,3 +18,24 @@ def get_dict(value: Any, arg: Any) -> Any:
         return value[int(arg)]
     else:
         return None
+
+
+@register.simple_tag
+def verbose_name(app_label: str, model: str, field: Optional[str] = None) -> str:
+    """Get a verbose name of a model or a field by app label and model name."""
+    ct = ContentType.objects.get(app_label=app_label, model=model).model_class()
+
+    if field:
+        # Field
+        return ct._meta.get_field(field).verbose_name.title()
+    else:
+        # Whole model
+        return ct._meta.verbose_name.title()
+
+
+@register.simple_tag
+def parse_json(value: Optional[str] = None) -> Union[dict, None]:
+    """Template tag for parsing JSON from a string."""
+    if not value:
+        return None
+    return json.loads(value)
diff --git a/aleksis/core/templatetags/html_helpers.py b/aleksis/core/templatetags/html_helpers.py
new file mode 100644
index 0000000000000000000000000000000000000000..a30a230ce4f9aebdf93499ef8ff511befe2a8871
--- /dev/null
+++ b/aleksis/core/templatetags/html_helpers.py
@@ -0,0 +1,20 @@
+from django import template
+
+from bs4 import BeautifulSoup
+
+register = template.Library()
+
+
+@register.filter
+def add_class_to_el(value: str, arg: str) -> str:
+    """Add a CSS class to every occurence of an element type.
+
+    Example: {{ mymodel.myhtmlfield|add_class_to_el:"ul,browser-default"
+    """
+    el, cls = arg.split(",")
+    soup = BeautifulSoup(value, "html.parser")
+
+    for el in soup.find_all(el):
+        el["class"] = el.get("class", "") + f" {cls}"
+
+    return str(soup)
diff --git a/aleksis/core/tests/models/test_notification.py b/aleksis/core/tests/models/test_notification.py
new file mode 100644
index 0000000000000000000000000000000000000000..1b1a6df054fe24f06bd363e825df3f1259af53e8
--- /dev/null
+++ b/aleksis/core/tests/models/test_notification.py
@@ -0,0 +1,34 @@
+import pytest
+
+from aleksis.core.models import Notification, Person
+
+pytestmark = pytest.mark.django_db
+
+
+def test_email_notification(mailoutbox):
+    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
+    )
+    notification.save()
+
+    assert notification.sent
+
+    assert len(mailoutbox) == 1
+
+    mail = mailoutbox[0]
+
+    assert email in mail.to
+    assert title in mail.body
+    assert description in mail.body
+    assert link in mail.body
+    assert sender in mail.body
+    assert recipient.addressing_name in mail.subject
+    assert recipient.addressing_name in mail.body
diff --git a/aleksis/core/tests/templatetags/test_data_helpers.py b/aleksis/core/tests/templatetags/test_data_helpers.py
index ce43e578dbd84f95ed96a6cf1426d615f7e9c816..176e08b22ecb252d5b0113583ab534f46e978072 100644
--- a/aleksis/core/tests/templatetags/test_data_helpers.py
+++ b/aleksis/core/tests/templatetags/test_data_helpers.py
@@ -1,4 +1,10 @@
-from aleksis.core.templatetags.data_helpers import get_dict
+import json
+
+import pytest
+
+from aleksis.core.templatetags.data_helpers import get_dict, parse_json, verbose_name
+
+pytestmark = pytest.mark.django_db
 
 
 def test_get_dict_object():
@@ -24,3 +30,24 @@ def test_get_dict_invalid():
     _foo = 12
 
     assert get_dict(_foo, "bar") is None
+
+
+def test_verbose_name_model():
+    assert verbose_name("core", "person") == "Person"
+
+
+def test_verbose_name_field():
+    assert verbose_name("core", "person", "first_name") == "First Name"
+
+
+def test_parse_json_json():
+    foo = {"foo": 12, "bar": "12", "baz": []}
+    foo_json = json.dumps(foo)
+
+    assert parse_json(foo_json) == foo
+    assert parse_json("{}") == {}
+
+
+def test_parse_json_empty():
+    assert parse_json(None) is None
+    assert parse_json("") is None
diff --git a/aleksis/core/tests/views/test_account.py b/aleksis/core/tests/views/test_account.py
index ecc15f59920ddb493ae64323a7aa4fbeebdafec4..28686eabf8290a7a72444d8fe5f4b71bb74f4626 100644
--- a/aleksis/core/tests/views/test_account.py
+++ b/aleksis/core/tests/views/test_account.py
@@ -10,7 +10,7 @@ def test_index_not_logged_in(client):
     response = client.get("/")
 
     assert response.status_code == 302
-    assert response['Location'].startswith(reverse(settings.LOGIN_URL))
+    assert response["Location"].startswith(reverse(settings.LOGIN_URL))
 
 
 def test_login_without_person(client, django_user_model):
@@ -39,4 +39,4 @@ def test_logout(client, django_user_model):
     response = client.get(reverse("logout"), follow=True)
 
     assert response.status_code == 200
-    assert "Enter your credentials." in response.content.decode("utf-8")
+    assert "Please login to see this page." in response.content.decode("utf-8")
diff --git a/aleksis/core/urls.py b/aleksis/core/urls.py
index 5de76472c91da9243edfa13c57c5ce29a7782e08..51048e5c83bdb6c540254a36754f32cdc3059591 100644
--- a/aleksis/core/urls.py
+++ b/aleksis/core/urls.py
@@ -9,46 +9,149 @@ from django.views.i18n import JavaScriptCatalog
 import calendarweek.django
 import debug_toolbar
 from django_js_reverse.views import urls_js
+from health_check.urls import urlpatterns as health_urls
 from two_factor.urls import urlpatterns as tf_urls
 
 from . import views
+from .util.core_helpers import is_celery_enabled
 
 urlpatterns = [
     path("", include("pwa.urls"), name="pwa"),
-    path("offline/", views.offline, name="offline"),
     path("about/", views.about, name="about_aleksis"),
     path("admin/", admin.site.urls),
     path("data_management/", views.data_management, name="data_management"),
-    path("status/", views.system_status, name="system_status"),
-    path("school_management", views.school_management, name="school_management"),
-    path("school/information/edit", views.edit_school, name="edit_school_information"),
-    path("school/term/edit", views.edit_schoolterm, name="edit_school_term"),
+    path("status/", views.SystemStatus.as_view(), name="system_status"),
     path("", include(tf_urls)),
     path("accounts/logout/", auth_views.LogoutView.as_view(), name="logout"),
+    path("school_terms/", views.SchoolTermListView.as_view(), name="school_terms"),
+    path("school_terms/create/", views.SchoolTermCreateView.as_view(), name="create_school_term"),
+    path("school_terms/<int:pk>/", views.SchoolTermEditView.as_view(), name="edit_school_term"),
     path("persons", views.persons, name="persons"),
     path("persons/accounts", views.persons_accounts, name="persons_accounts"),
     path("person", views.person, name="person"),
+    path("person/create", views.edit_person, name="create_person"),
     path("person/<int:id_>", views.person, name="person_by_id"),
     path("person/<int:id_>/edit", views.edit_person, name="edit_person_by_id"),
+    path("person/<int:id_>/delete", views.delete_person, name="delete_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"),
+    path(
+        "groups/additional_field/<int:id_>/edit",
+        views.edit_additional_field,
+        name="edit_additional_field_by_id",
+    ),
+    path(
+        "groups/additional_field/create",
+        views.edit_additional_field,
+        name="create_additional_field",
+    ),
+    path(
+        "groups/additional_field/<int:id_>/delete",
+        views.delete_additional_field,
+        name="delete_additional_field_by_id",
+    ),
     path("group/create", views.edit_group, name="create_group"),
     path("group/<int:id_>", views.group, name="group_by_id"),
     path("group/<int:id_>/edit", views.edit_group, name="edit_group_by_id"),
+    path("group/<int:id_>/delete", views.delete_group, name="delete_group_by_id"),
     path("", views.index, name="index"),
-    path("notifications/mark-read/<int:id_>", views.notification_mark_read, name="notification_mark_read"),
+    path(
+        "notifications/mark-read/<int:id_>",
+        views.notification_mark_read,
+        name="notification_mark_read",
+    ),
+    path("groups/group_type/create", views.edit_group_type, name="create_group_type"),
+    path(
+        "groups/group_type/<int:id_>/delete",
+        views.delete_group_type,
+        name="delete_group_type_by_id",
+    ),
+    path("groups/group_type/<int:id_>/edit", views.edit_group_type, name="edit_group_type_by_id"),
+    path("groups/group_types", views.group_types, name="group_types"),
     path("announcements/", views.announcements, name="announcements"),
     path("announcement/create/", views.announcement_form, name="add_announcement"),
-    path("announcement/edit/<int:pk>/", views.announcement_form, name="edit_announcement"),
-    path("announcement/delete/<int:pk>/", views.delete_announcement, name="delete_announcement"),
+    path("announcement/edit/<int:id_>/", views.announcement_form, name="edit_announcement"),
+    path("announcement/delete/<int:id_>/", views.delete_announcement, name="delete_announcement"),
     path("search/searchbar/", views.searchbar_snippets, name="searchbar_snippets"),
-    path("search/", include("haystack.urls")),
+    path("search/", views.PermissionSearchView(), name="haystack_search"),
     path("maintenance-mode/", include("maintenance_mode.urls")),
     path("impersonate/", include("impersonate.urls")),
     path("__i18n__/", include("django.conf.urls.i18n")),
     path("select2/", include("django_select2.urls")),
-    path("jsreverse.js", urls_js, name='js_reverse'),
+    path("jsreverse.js", urls_js, name="js_reverse"),
     path("calendarweek_i18n.js", calendarweek.django.i18n_js, name="calendarweek_i18n_js"),
-    path('gettext.js', JavaScriptCatalog.as_view(), name='javascript-catalog'),
+    path("gettext.js", JavaScriptCatalog.as_view(), name="javascript-catalog"),
+    path(
+        "preferences/site/", views.preferences, {"registry_name": "site"}, name="preferences_site"
+    ),
+    path(
+        "preferences/person/",
+        views.preferences,
+        {"registry_name": "person"},
+        name="preferences_person",
+    ),
+    path(
+        "preferences/group/",
+        views.preferences,
+        {"registry_name": "group"},
+        name="preferences_group",
+    ),
+    path(
+        "preferences/site/<int:pk>/",
+        views.preferences,
+        {"registry_name": "site"},
+        name="preferences_site",
+    ),
+    path(
+        "preferences/person/<int:pk>/",
+        views.preferences,
+        {"registry_name": "person"},
+        name="preferences_person",
+    ),
+    path(
+        "preferences/group/<int:pk>/",
+        views.preferences,
+        {"registry_name": "group"},
+        name="preferences_group",
+    ),
+    path(
+        "preferences/site/<int:pk>/<str:section>/",
+        views.preferences,
+        {"registry_name": "site"},
+        name="preferences_site",
+    ),
+    path(
+        "preferences/person/<int:pk>/<str:section>/",
+        views.preferences,
+        {"registry_name": "person"},
+        name="preferences_person",
+    ),
+    path(
+        "preferences/group/<int:pk>/<str:section>/",
+        views.preferences,
+        {"registry_name": "group"},
+        name="preferences_group",
+    ),
+    path(
+        "preferences/site/<str:section>/",
+        views.preferences,
+        {"registry_name": "site"},
+        name="preferences_site",
+    ),
+    path(
+        "preferences/person/<str:section>/",
+        views.preferences,
+        {"registry_name": "person"},
+        name="preferences_person",
+    ),
+    path(
+        "preferences/group/<str:section>/",
+        views.preferences,
+        {"registry_name": "group"},
+        name="preferences_group",
+    ),
+    path("health/", include(health_urls)),
 ]
 
 # Serve static files from STATIC_ROOT to make it work with runserver
@@ -64,6 +167,9 @@ if hasattr(settings, "TWILIO_ACCOUNT_SID"):
 
     urlpatterns += [path("", include(tf_twilio_urls))]
 
+if is_celery_enabled():
+    urlpatterns.append(path("celery_progress/", include("celery_progress.urls")))
+
 # Serve javascript-common if in development
 if settings.DEBUG:
     urlpatterns.append(path("__debug__/", include(debug_toolbar.urls)))
@@ -74,8 +180,7 @@ for app_config in apps.app_configs.values():
         continue
 
     try:
-        urlpatterns.append(path("app/%s/" % app_config.label, include("%s.urls" % app_config.name)))
+        urlpatterns.append(path(f"app/{app_config.label}/", include(f"{app_config.name}.urls")))
     except ModuleNotFoundError:
         # Ignore exception as app just has no URLs
         pass  # noqa
-
diff --git a/aleksis/core/util/apps.py b/aleksis/core/util/apps.py
index dbf6159252c0b20aee9fcc422fb21360c7fbc1e0..72e0a2fcd0c46c7ca8bae47dd79e79b70ec9d432 100644
--- a/aleksis/core/util/apps.py
+++ b/aleksis/core/util/apps.py
@@ -1,57 +1,42 @@
-from importlib import import_module
-from typing import Any, List, Optional, Tuple, Sequence
+from typing import Any, List, Optional, Sequence, Tuple
 
 import django.apps
 from django.contrib.auth.signals import user_logged_in, user_logged_out
 from django.db.models.signals import post_migrate, pre_migrate
 from django.http import HttpRequest
 
-from constance.signals import config_updated
-from license_expression import Licensing, LicenseSymbol
+from dynamic_preferences.signals import preference_updated
+from license_expression import Licensing
 from spdx_license_list import LICENSES
 
 from .core_helpers import copyright_years
 
 
 class AppConfig(django.apps.AppConfig):
-    """ An extended version of DJango's AppConfig container. """
+    """An extended version of DJango's AppConfig container."""
 
     def ready(self):
         super().ready()
 
-        # Run model extension code
-        try:
-            import_module(
-                ".".join(self.__class__.__module__.split(".")[:-1] + ["model_extensions"])
-            )
-        except ImportError:
-            # ImportErrors are non-fatal because model extensions are optional.
-            pass
-
         # Register default listeners
         pre_migrate.connect(self.pre_migrate, sender=self)
         post_migrate.connect(self.post_migrate, sender=self)
-        config_updated.connect(self.config_updated)
+        preference_updated.connect(self.preference_updated)
         user_logged_in.connect(self.user_logged_in)
         user_logged_out.connect(self.user_logged_out)
 
         # Getting an app ready means it should look at its config once
-        self.config_updated()
-
-        # Register system checks of this app
-        try:
-            import_module(".".join(self.__class__.__module__.split(".")[:-1] + ["checks"]))
-        except ImportError:
-            # ImportErrors are non-fatal because checks are optional.
-            pass
+        self.preference_updated(self)
 
     @classmethod
     def get_name(cls):
+        """Get name of application package."""
         return getattr(cls, "verbose_name", cls.name)
         # TODO Try getting from distribution if not set
 
     @classmethod
     def get_version(cls):
+        """Get version of application package."""
         try:
             from .. import __version__  # noqa
         except ImportError:
@@ -61,38 +46,46 @@ class AppConfig(django.apps.AppConfig):
 
     @classmethod
     def get_licence(cls) -> Tuple:
+        """Get tuple of licence information of application package."""
+        # Get string representation of licence in SPDX format
         licence = getattr(cls, "licence", None)
 
         default_dict = {
-            'isDeprecatedLicenseId': False,
-            'isFsfLibre': False,
-            'isOsiApproved': False,
-            'licenseId': 'unknown',
-            'name': 'Unknown Licence',
-            'referenceNumber': -1,
-            'url': '',
+            "isDeprecatedLicenseId": False,
+            "isFsfLibre": False,
+            "isOsiApproved": False,
+            "licenseId": "unknown",
+            "name": "Unknown Licence",
+            "referenceNumber": -1,
+            "url": "",
         }
-
         if licence:
+            # Parse licence string into object format
             licensing = Licensing(LICENSES.keys())
             parsed = licensing.parse(licence).simplify()
             readable = parsed.render_as_readable()
 
+            # Collect flags about licence combination (drop to False if any licence is False)
             flags = {
                 "isFsfLibre": True,
                 "isOsiApproved": True,
             }
 
+            # Fill information dictionaries with missing data
             licence_dicts = []
-
             for symbol in parsed.symbols:
+                # Get licence base information, stripping the "or later" mark
                 licence_dict = LICENSES.get(symbol.key.rstrip("+"), None)
 
                 if licence_dict is None:
+                    # Fall back to the default dict
                     licence_dict = default_dict
                 else:
-                    licence_dict["url"] = "https://spdx.org/licenses/{}.html".format(licence_dict["licenseId"])
+                    # Add missing licence link to SPDX data
+                    licence_id = licence_dict["licenseId"]
+                    licence_dict["url"] = f"https://spdx.org/licenses/{licence_id}.html"
 
+                # Drop summed up flags to False if this licence is False
                 flags["isFsfLibre"] = flags["isFsfLibre"] and licence_dict["isFsfLibre"]
                 flags["isOsiApproved"] = flags["isOsiApproved"] and licence_dict["isOsiApproved"]
 
@@ -100,40 +93,46 @@ class AppConfig(django.apps.AppConfig):
 
             return (readable, flags, licence_dicts)
         else:
+            # We could not find a valid licence
             return ("Unknown", [default_dict])
 
     @classmethod
     def get_urls(cls):
+        """Get list of URLs for this application package."""
         return getattr(cls, "urls", {})
         # TODO Try getting from distribution if not set
 
     @classmethod
     def get_copyright(cls) -> Sequence[Tuple[str, str, str]]:
-        copyrights = getattr(cls, "copyright", tuple())
+        """Get copyright information tuples for application package."""
+        copyrights = getattr(cls, "copyright_info", tuple())
 
         copyrights_processed = []
-
-        for copyright in copyrights:
+        for copyright_info in copyrights:
             copyrights_processed.append(
                 (
-                    copyright[0] if isinstance(copyright[0], str) else copyright_years(copyright[0]),
-                    copyright[1],
-                    copyright[2],
+                    # Sort copyright years and combine year ranges for display
+                    copyright_info[0]
+                    if isinstance(copyright_info[0], str)
+                    else copyright_years(copyright_info[0]),
+                    copyright_info[1],
+                    copyright_info[2],
                 )
             )
 
         return copyrights_processed
-
         # TODO Try getting from distribution if not set
 
-    def config_updated(
+    def preference_updated(
         self,
-        key: Optional[str] = "",
+        sender: Any,
+        section: Optional[str] = None,
+        name: Optional[str] = None,
         old_value: Optional[Any] = None,
         new_value: Optional[Any] = None,
         **kwargs,
     ) -> None:
-        """ Called on every app instance if a Constance config chagnes, and once on startup
+        """Call on every app instance if a dynamic preference changes, and once on startup.
 
         By default, it does nothing.
         """
@@ -149,7 +148,7 @@ class AppConfig(django.apps.AppConfig):
         apps: django.apps.registry.Apps,
         **kwargs,
     ) -> None:
-        """ Called on every app instance before its models are migrated
+        """Call on every app instance before its models are migrated.
 
         By default, it does nothing.
         """
@@ -165,7 +164,7 @@ class AppConfig(django.apps.AppConfig):
         apps: django.apps.registry.Apps,
         **kwargs,
     ) -> None:
-        """ Called on every app instance after its models have been migrated
+        """Call on every app instance after its models have been migrated.
 
         By default, asks all models to do maintenance on their default data.
         """
@@ -174,7 +173,7 @@ class AppConfig(django.apps.AppConfig):
     def user_logged_in(
         self, sender: type, request: Optional[HttpRequest], user: "User", **kwargs
     ) -> None:
-        """ Called after a user logged in
+        """Call after a user logged in.
 
         By default, it does nothing.
         """
@@ -183,13 +182,16 @@ class AppConfig(django.apps.AppConfig):
     def user_logged_out(
         self, sender: type, request: Optional[HttpRequest], user: "User", **kwargs
     ) -> None:
-        """ Called after a user logged out
+        """Call after a user logged out.
 
         By default, it does nothing.
         """
         pass
 
     def _maintain_default_data(self):
+        from django.contrib.auth.models import Permission
+        from django.contrib.contenttypes.models import ContentType
+
         if not self.models_module:
             # This app does not have any models, so bail out early
             return
@@ -198,3 +200,11 @@ class AppConfig(django.apps.AppConfig):
             if hasattr(model, "maintain_default_data"):
                 # Method implemented by each model object; can be left out
                 model.maintain_default_data()
+            if hasattr(model, "extra_permissions"):
+                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},
+                    )
diff --git a/aleksis/core/util/celery_progress.py b/aleksis/core/util/celery_progress.py
new file mode 100644
index 0000000000000000000000000000000000000000..9de760eeb56b8ce535a0cd8e88f7380c5411f78d
--- /dev/null
+++ b/aleksis/core/util/celery_progress.py
@@ -0,0 +1,34 @@
+from decimal import Decimal
+from typing import Union
+
+from celery_progress.backend import PROGRESS_STATE, AbstractProgressRecorder
+
+
+class ProgressRecorder(AbstractProgressRecorder):
+    def __init__(self, task):
+        self.task = task
+        self.messages = []
+        self.total = 100
+        self.current = 0
+
+    def set_progress(self, current: Union[int, float], **kwargs):
+        self.current = current
+
+        percent = 0
+        if self.total > 0:
+            percent = (Decimal(current) / Decimal(self.total)) * Decimal(100)
+            percent = float(round(percent, 2))
+
+        self.task.update_state(
+            state=PROGRESS_STATE,
+            meta={
+                "current": current,
+                "total": self.total,
+                "percent": percent,
+                "messages": self.messages,
+            },
+        )
+
+    def add_message(self, level: int, message: str, **kwargs):
+        self.messages.append((level, message))
+        self.set_progress(self.current)
diff --git a/aleksis/core/util/core_helpers.py b/aleksis/core/util/core_helpers.py
index 13558233068626c8f288b70624b2646840826819..ba28f66292217b77a8d15eb1988774bbe09776fb 100644
--- a/aleksis/core/util/core_helpers.py
+++ b/aleksis/core/util/core_helpers.py
@@ -1,32 +1,49 @@
-from datetime import datetime, timedelta
-from itertools import groupby
-from operator import itemgetter
 import os
 import pkgutil
+import time
+from datetime import datetime, timedelta
 from importlib import import_module
-from typing import Any, Callable, Sequence, Union, List
+from itertools import groupby
+from operator import itemgetter
+from typing import Any, Callable, Optional, Sequence, Union
 from uuid import uuid4
 
 from django.conf import settings
 from django.db.models import Model
 from django.http import HttpRequest
+from django.shortcuts import get_object_or_404
 from django.utils import timezone
 from django.utils.functional import lazy
 
+from django_global_request.middleware import get_request
+
+from aleksis.core.util import messages
+
 
 def copyright_years(years: Sequence[int], seperator: str = ", ", joiner: str = "–") -> str:
-    """ Takes a sequence of integegers and produces a string with ranges
+    """Take a sequence of integegers and produces a string with ranges.
 
     >>> copyright_years([1999, 2000, 2001, 2005, 2007, 2008, 2009])
     '1999–2001, 2005, 2007–2009'
     """
-
-    ranges = [list(map(itemgetter(1), group)) for _, group in groupby(enumerate(years), lambda e: e[1]-e[0])]
-    years_strs = [str(range_[0]) if len(range_) == 1 else joiner.join([str(range_[0]), str(range_[-1])]) for range_ in ranges]
+    ranges = [
+        list(map(itemgetter(1), group))
+        for _, group in groupby(enumerate(years), lambda e: e[1] - e[0])
+    ]
+    years_strs = [
+        str(range_[0]) if len(range_) == 1 else joiner.join([str(range_[0]), str(range_[-1])])
+        for range_ in ranges
+    ]
 
     return seperator.join(years_strs)
 
+
 def dt_show_toolbar(request: HttpRequest) -> bool:
+    """Add a helper to determin if Django debug toolbar should be displayed.
+
+    Extends the default behaviour by enabling DJDT for superusers independent
+    of source IP.
+    """
     from debug_toolbar.middleware import show_toolbar  # noqa
 
     if not settings.DEBUG:
@@ -41,26 +58,28 @@ def dt_show_toolbar(request: HttpRequest) -> bool:
 
 
 def get_app_packages() -> Sequence[str]:
-    """ Find all packages within the aleksis.apps namespace. """
-
+    """Find all packages within the aleksis.apps namespace."""
     # Import error are non-fatal here because probably simply no app is installed.
     try:
         import aleksis.apps
     except ImportError:
         return []
 
-    return ["aleksis.apps.%s" % pkg[1] for pkg in pkgutil.iter_modules(aleksis.apps.__path__)]
+    return [f"aleksis.apps.{pkg[1]}" for pkg in pkgutil.iter_modules(aleksis.apps.__path__)]
+
 
+def merge_app_settings(
+    setting: str, original: Union[dict, list], deduplicate: bool = False
+) -> Union[dict, list]:
+    """Merge app settings.
 
-def merge_app_settings(setting: str, original: Union[dict, list], deduplicate: bool = False) -> Union[dict, list]:
-    """ Get a named settings constant from all apps and merge it into the original.
+    Get a named settings constant from all apps and merge it into the original.
     To use this, add a settings.py file to the app, in the same format as Django's
     main settings.py.
 
     Note: Only selected names will be imported frm it to minimise impact of
     potentially malicious apps!
     """
-
     for pkg in get_app_packages():
         try:
             mod_settings = import_module(pkg + ".settings")
@@ -76,7 +95,7 @@ def merge_app_settings(setting: str, original: Union[dict, list], deduplicate: b
         for entry in app_setting:
             if entry in original:
                 if not deduplicate:
-                    raise AttributeError("%s already set in original." % entry)
+                    raise AttributeError(f"{entry} already set in original.")
             else:
                 if isinstance(original, list):
                     original.append(entry)
@@ -86,21 +105,49 @@ def merge_app_settings(setting: str, original: Union[dict, list], deduplicate: b
                     raise TypeError("Only dict and list settings can be merged.")
 
 
-def lazy_config(key: str) -> Callable[[str], Any]:
-    """ Lazily get a config value from constance. Useful to bind constance
-    configs to other global settings to make them available to third-party
-    apps that are not aware of constance.
+def get_site_preferences():
+    """Get the preferences manager of the current site."""
+    from django.contrib.sites.models import Site  # noqa
+
+    return Site.objects.get_current().preferences
+
+
+def lazy_preference(section: str, name: str) -> Callable[[str, str], Any]:
+    """Lazily get a config value from dynamic preferences.
+
+    Useful to bind preferences
+    to other global settings to make them available to third-party apps that are not
+    aware of dynamic preferences.
     """
 
-    def _get_config(key: str) -> Any:
-        from constance import config  # noqa
-        return getattr(config, key)
+    def _get_preference(section: str, name: str) -> Any:
+        return get_site_preferences()[f"{section}__{name}"]
 
     # The type is guessed from the default value to improve lazy()'s behaviour
-    return lazy(_get_config, type(settings.CONSTANCE_CONFIG[key][0]))(key)
+    # FIXME Reintroduce the behaviour described above
+    return lazy(_get_preference, str)(section, name)
+
+
+def lazy_get_favicon_url(
+    title: str, size: int, rel: str, default: Optional[str] = None
+) -> Callable[[str, str], Any]:
+    """Lazily get the URL to a favicon image."""
+
+    def _get_favicon_url(size: int, rel: str) -> Any:
+        from favicon.models import Favicon  # noqa
+
+        try:
+            favicon = Favicon.on_site.get(title=title)
+        except Favicon.DoesNotExist:
+            return default
+        else:
+            return favicon.get_favicon(size, rel).faviconImage.url
+
+    return lazy(_get_favicon_url, str)(size, rel)
 
 
 def is_impersonate(request: HttpRequest) -> bool:
+    """Check whether the user was impersonated by an admin."""
     if hasattr(request, "user"):
         return getattr(request.user, "is_impersonate", False)
     else:
@@ -108,34 +155,45 @@ def is_impersonate(request: HttpRequest) -> bool:
 
 
 def has_person(obj: Union[HttpRequest, Model]) -> bool:
-    """ Check wehether a model object has a person attribute linking it to a Person
-    object. The passed object can also be a HttpRequest object, in which case its
+    """Check wehether a model object has a person attribute linking it to a Person object.
+
+    The passed object can also be a HttpRequest object, in which case its
     associated User object is unwrapped and tested.
     """
-
     if isinstance(obj, HttpRequest):
         if hasattr(obj, "user"):
             obj = obj.user
         else:
             return False
 
-    return getattr(obj, "person", None) is not None
+    person = getattr(obj, "person", None)
+    if person is None:
+        return False
+    elif getattr(person, "is_dummy", False):
+        return False
+    else:
+        return True
+
+
+def is_celery_enabled():
+    """Check whether celery support is enabled."""
+    return hasattr(settings, "CELERY_RESULT_BACKEND")
 
 
 def celery_optional(orig: Callable) -> Callable:
-    """ Decorator that makes Celery optional for a function.
+    """Add a decorator that makes Celery optional for a function.
 
     If Celery is configured and available, it wraps the function in a Task
     and calls its delay method when invoked; if not, it leaves it untouched
     and it is executed synchronously.
     """
-
-    if hasattr(settings, "CELERY_RESULT_BACKEND"):
+    if is_celery_enabled():
         from ..celery import app  # noqa
+
         task = app.task(orig)
 
     def wrapped(*args, **kwargs):
-        if hasattr(settings, "CELERY_RESULT_BACKEND"):
+        if is_celery_enabled():
             task.delay(*args, **kwargs)
         else:
             orig(*args, **kwargs)
@@ -143,13 +201,114 @@ def celery_optional(orig: Callable) -> Callable:
     return wrapped
 
 
-def path_and_rename(instance, filename: str, upload_to: str = "files") -> str:
-    """ Updates path of an uploaded file and renames it to a random UUID in Django FileField """
+class DummyRecorder:
+    def set_progress(self, *args, **kwargs):
+        pass
+
+    def add_message(self, level: int, message: str, **kwargs) -> Optional[Any]:
+        request = get_request()
+        return messages.add_message(request, level, message, **kwargs)
+
+
+def celery_optional_progress(orig: Callable) -> Callable:
+    """Add a decorator that makes Celery with progress bar support optional for a function.
+
+    If Celery is configured and available, it wraps the function in a Task
+    and calls its delay method when invoked; if not, it leaves it untouched
+    and it is executed synchronously.
+
+    Additionally, it adds a recorder class as first argument
+    (`ProgressRecorder` if Celery is enabled, else `DummyRecoder`).
+
+    This recorder provides the functions `set_progress` and `add_message`
+    which can be used to track the status of the task.
+    For further information, see the respective recorder classes.
+
+    How to use
+    ----------
+    1. Write a function and include tracking methods
+
+    ::
+
+        from django.contrib import messages
+
+        from aleksis.core.util.core_helpers import celery_optional_progress
+
+        @celery_optional_progress
+        def do_something(recorder: Union[ProgressRecorder, DummyRecorder], foo, bar, baz=None):
+            # ...
+            recorder.total = len(list_with_data)
+
+            for i, item in list_with_data:
+                # ...
+                recorder.set_progress(i + 1)
+                # ...
 
+            recorder.add_message(messages.SUCCESS, "All data were imported successfully.")
+
+    2. Track process in view:
+
+    ::
+
+        def my_view(request):
+            context = {}
+            # ...
+            result = do_something(foo, bar, baz=baz)
+
+            if result:
+                context = {
+                    "title": _("Progress: Import data"),
+                    "back_url": reverse("index"),
+                    "progress": {
+                        "task_id": result.task_id,
+                        "title": _("Import objects …"),
+                        "success": _("The import was done successfully."),
+                        "error": _("There was a problem while importing data."),
+                    },
+                }
+
+                # Render progress view
+                return render(request, "core/progress.html", context)
+
+            # Render other view if Celery isn't enabled
+            return render(request, "my-app/other-view.html", context)
+    """
+
+    def recorder_func(self, *args, **kwargs):
+        if is_celery_enabled():
+            from .celery_progress import ProgressRecorder  # noqa
+
+            recorder = ProgressRecorder(self)
+        else:
+            recorder = DummyRecorder()
+        orig(recorder, *args, **kwargs)
+
+        # Needed to ensure that all messages are displayed by frontend
+        time.sleep(0.7)
+
+    var_name = f"{orig.__module__}.{orig.__name__}"
+
+    if is_celery_enabled():
+        from ..celery import app  # noqa
+
+        task = app.task(recorder_func, bind=True, name=var_name)
+
+    def wrapped(*args, **kwargs):
+        if is_celery_enabled():
+            return task.delay(*args, **kwargs)
+        else:
+            recorder_func(None, *args, **kwargs)
+            return None
+
+    return wrapped
+
+
+def path_and_rename(instance, filename: str, upload_to: str = "files") -> str:
+    """Update path of an uploaded file and renames it to a random UUID in Django FileField."""
     _, ext = os.path.splitext(filename)
 
     # set filename as random string
-    new_filename = '{}.{}'.format(uuid4().hex, ext)
+    new_filename = f"{uuid4().hex}.{ext}"
 
     # Create upload directory if necessary
     os.makedirs(os.path.join(settings.MEDIA_ROOT, upload_to), exist_ok=True)
@@ -159,15 +318,34 @@ def path_and_rename(instance, filename: str, upload_to: str = "files") -> str:
 
 
 def custom_information_processor(request: HttpRequest) -> dict:
-    """ Provides custom information in all templates """
+    """Provide custom information in all templates."""
+    from ..models import CustomMenu
 
-    from ..models import School, CustomMenu
     return {
-        "SCHOOL": School.get_default,
         "FOOTER_MENU": CustomMenu.get_default("footer"),
     }
 
 
 def now_tomorrow() -> datetime:
-    """ Return current time tomorrow """
+    """Return current time tomorrow."""
     return timezone.now() + timedelta(days=1)
+
+
+def objectgetter_optional(
+    model: Model, default: Optional[Any] = None, default_eval: bool = False
+) -> Callable[[HttpRequest, Optional[int]], Model]:
+    """Get an object by pk, defaulting to None."""
+
+    def get_object(request: HttpRequest, id_: Optional[int] = None, **kwargs) -> Model:
+        if id_ is not None:
+            return get_object_or_404(model, pk=id_)
+        else:
+            return eval(default) if default_eval else default  # noqa:S307
+
+    return get_object
+
+
+def handle_uploaded_file(f, filename: str):
+    with open(filename, "wb+") as destination:
+        for chunk in f.chunks():
+            destination.write(chunk)
diff --git a/aleksis/core/util/messages.py b/aleksis/core/util/messages.py
index e3c93dbb0301ea3eff1c2c7f4b18556d33c09789..5efc04c12befb65825164f8f954eedbd4dc4500c 100644
--- a/aleksis/core/util/messages.py
+++ b/aleksis/core/util/messages.py
@@ -8,6 +8,13 @@ from django.http import HttpRequest
 def add_message(
     request: Optional[HttpRequest], level: int, message: str, **kwargs
 ) -> Optional[Any]:
+    """Add a message.
+
+    Add a message to either Django's message framework, if called from a web request,
+    or to the default logger.
+
+    Default to DEBUG level.
+    """
     if request:
         return messages.add_message(request, level, message, **kwargs)
     else:
@@ -15,20 +22,55 @@ def add_message(
 
 
 def debug(request: Optional[HttpRequest], message: str, **kwargs) -> Optional[Any]:
+    """Add a debug message.
+
+    Add a message to either Django's message framework, if called from a web request,
+    or to the default logger.
+
+    Default to DEBUG level.
+    """
     return add_message(request, messages.DEBUG, message, **kwargs)
 
 
 def info(request: Optional[HttpRequest], message: str, **kwargs) -> Optional[Any]:
+    """Add a info message.
+
+    Add a message to either Django's message framework, if called from a web request,
+    or to the default logger.
+
+    Default to INFO level.
+    """
     return add_message(request, messages.INFO, message, **kwargs)
 
 
 def success(request: Optional[HttpRequest], message: str, **kwargs) -> Optional[Any]:
+    """Add a success message.
+
+    Add a message to either Django's message framework, if called from a web request,
+    or to the default logger.
+
+    Default to SUCCESS level.
+    """
     return add_message(request, messages.SUCCESS, message, **kwargs)
 
 
 def warning(request: Optional[HttpRequest], message: str, **kwargs) -> Optional[Any]:
+    """Add a warning message.
+
+    Add a message to either Django's message framework, if called from a web request,
+    or to the default logger.
+
+    Default to WARNING level.
+    """
     return add_message(request, messages.WARNING, message, **kwargs)
 
 
 def error(request: Optional[HttpRequest], message: str, **kwargs) -> Optional[Any]:
+    """Add an error message.
+
+    Add a message to either Django's message framework, if called from a web request,
+    or to the default logger.
+
+    Default to ERROR level.
+    """
     return add_message(request, messages.ERROR, message, **kwargs)
diff --git a/aleksis/core/util/middlewares.py b/aleksis/core/util/middlewares.py
new file mode 100644
index 0000000000000000000000000000000000000000..7ac5e5f2cbb710d3c6bb8c1bdda83ca170c96165
--- /dev/null
+++ b/aleksis/core/util/middlewares.py
@@ -0,0 +1,30 @@
+from typing import Callable
+
+from django.http import HttpRequest, HttpResponse
+
+from ..models import DummyPerson
+from .core_helpers import has_person
+
+
+class EnsurePersonMiddleware:
+    """Middleware that ensures that the logged-in user is linked to a person.
+
+    It is needed to inject a dummy person to a superuser that would otherwise
+    not have an associated person, in order they can get their account set up
+    without external help.
+    """
+
+    def __init__(self, get_response: Callable):
+        self.get_response = get_response
+
+    def __call__(self, request: HttpRequest) -> HttpResponse:
+        if not has_person(request):
+            if request.user.is_superuser:
+                # Super-users get a dummy person linked
+                dummy_person = DummyPerson(
+                    first_name=request.user.first_name, last_name=request.user.last_name
+                )
+                request.user.person = dummy_person
+
+        response = self.get_response(request)
+        return response
diff --git a/aleksis/core/util/notifications.py b/aleksis/core/util/notifications.py
index 7f309d1cc2e0a0a046334683f5e2100f28fdae2c..38d27057033752d6ae7cdefce9c53b4728447b80 100644
--- a/aleksis/core/util/notifications.py
+++ b/aleksis/core/util/notifications.py
@@ -1,4 +1,4 @@
-""" Utility code for notification system """
+"""Utility code for notification system."""
 
 from typing import Sequence, Union
 
@@ -10,19 +10,18 @@ from django.utils.translation import gettext_lazy as _
 
 from templated_email import send_templated_mail
 
+from .core_helpers import lazy_preference
+
 try:
     from twilio.rest import Client as TwilioClient
 except ImportError:
     TwilioClient = None
 
-from .core_helpers import celery_optional, lazy_config
-
 
 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 plan-text template and send via SMS to all recipients."""
     template = get_template(template_name)
     text = template.render(context)
 
@@ -34,11 +33,11 @@ def send_templated_sms(
 def _send_notification_email(notification: "Notification", template: str = "notification") -> None:
     context = {
         "notification": notification,
-        "notification_user": notification.recipient.adressing_name,
+        "notification_user": notification.recipient.addressing_name,
     }
     send_templated_mail(
         template_name=template,
-        from_email=lazy_config("MAIL_OUT"),
+        from_email=lazy_preference("mail", "address"),
         recipient_list=[notification.recipient.email],
         context=context,
     )
@@ -49,7 +48,7 @@ def _send_notification_sms(
 ) -> None:
     context = {
         "notification": notification,
-        "notification_user": notification.recipient.adressing_name,
+        "notification_user": notification.recipient.addressing_name,
     }
     send_templated_sms(
         template_name=template,
@@ -63,24 +62,23 @@ def _send_notification_sms(
 # - Check for availability
 # - Send notification through it
 _CHANNELS_MAP = {
-    "email": (_("E-Mail"), lambda: lazy_config("MAIL_OUT"), _send_notification_email),
+    "email": (_("E-Mail"), lambda: lazy_preference("mail", "address"), _send_notification_email),
     "sms": (_("SMS"), lambda: getattr(settings, "TWILIO_SID", None), _send_notification_sms),
 }
 
 
 def send_notification(notification: Union[int, "Notification"], resend: bool = False) -> None:
-    """ Send a notification through enabled channels.
+    """Send a notification through enabled channels.
 
     If resend is passed as True, the notification is sent even if it was
     previously marked as sent.
     """
-
-    channels = lazy_config("NOTIFICATION_CHANNELS")
-
     if isinstance(notification, int):
         Notification = apps.get_model("core", "Notification")
         notification = Notification.objects.get(pk=notification)
 
+    channels = [notification.recipient.preferences["notification__channels"]]
+
     if resend or not notification.sent:
         for channel in channels:
             name, check, send = _CHANNELS_MAP[channel]
@@ -89,13 +87,12 @@ def send_notification(notification: Union[int, "Notification"], resend: bool = F
 
 
 def get_notification_choices() -> list:
-    """ Return all available channels for notifications.
+    """Return all available channels for notifications.
 
     This gathers the channels that are technically available as per the
     system configuration. Which ones are available to users is defined
     by the administrator (by selecting a subset of these choices).
     """
-
     choices = []
     for channel, (name, check, send) in _CHANNELS_MAP.items():
         if check():
diff --git a/aleksis/core/util/predicates.py b/aleksis/core/util/predicates.py
new file mode 100644
index 0000000000000000000000000000000000000000..975273d7d934ae41df8ecefb494b121ed69b2a94
--- /dev/null
+++ b/aleksis/core/util/predicates.py
@@ -0,0 +1,114 @@
+from django.contrib.auth.backends import ModelBackend
+from django.contrib.auth.models import User
+from django.db.models import Model
+from django.http import HttpRequest
+
+from guardian.backends import ObjectPermissionBackend
+from guardian.shortcuts import get_objects_for_user
+from rules import predicate
+
+from ..models import Group
+from .core_helpers import get_site_preferences
+from .core_helpers import has_person as has_person_helper
+
+
+def permission_validator(request: HttpRequest, perm: str) -> bool:
+    """Check whether the request user has a permission."""
+    if request.user:
+        return request.user.has_perm(perm)
+    return False
+
+
+def check_global_permission(user: User, perm: str) -> bool:
+    """Check whether a user has a global permission."""
+    return ModelBackend().has_perm(user, perm)
+
+
+def check_object_permission(user: User, perm: str, obj: Model) -> bool:
+    """Check whether a user has a permission on a object."""
+    return ObjectPermissionBackend().has_perm(user, perm, obj)
+
+
+def has_global_perm(perm: str):
+    """Build predicate which checks whether a user has a global permission."""
+    name = f"has_global_perm:{perm}"
+
+    @predicate(name)
+    def fn(user: User) -> bool:
+        return check_global_permission(user, perm)
+
+    return fn
+
+
+def has_object_perm(perm: str):
+    """Build predicate which checks whether a user has a permission on a object."""
+    name = f"has_global_perm:{perm}"
+
+    @predicate(name)
+    def fn(user: User, obj: Model) -> bool:
+        if not obj:
+            return False
+        return check_object_permission(user, perm, obj)
+
+    return fn
+
+
+def has_any_object(perm: str, klass):
+    """Check if has any object.
+
+    Build predicate which checks whether a user has access
+    to objects with the provided permission.
+    """
+    name = f"has_any_object:{perm}"
+
+    @predicate(name)
+    def fn(user: User) -> bool:
+        objs = get_objects_for_user(user, perm, klass)
+        return len(objs) > 0
+
+    return fn
+
+
+def is_site_preference_set(section: str, pref: str):
+    """Check the boolean value of a given site preference."""
+    name = f"check_site_preference:{section}__{pref}"
+
+    @predicate(name)
+    def fn() -> bool:
+        return bool(get_site_preferences()[f"{section}__{pref}"])
+
+    return fn
+
+
+@predicate
+def has_person(user: User) -> bool:
+    """Predicate which checks whether a user has a linked person."""
+    return has_person_helper(user)
+
+
+@predicate
+def is_current_person(user: User, obj: Model) -> bool:
+    """Predicate which checks if the provided object is the person linked to the user object."""
+    return user.person == obj
+
+
+@predicate
+def is_group_owner(user: User, group: Group) -> bool:
+    """Predicate which checks if the user is a owner of the provided group."""
+    return group.owners.filter(owners=user.person).exists()
+
+
+@predicate
+def is_group_member(user: User, group: Group) -> bool:
+    """Predicate which checks if the user is a member of the provided group."""
+    return user.person in group.members.all()
+
+
+@predicate
+def is_notification_recipient(user: User, obj: Model) -> bool:
+    """Check if is a notification recipient.
+
+    Predicate which checks whether the recipient of the
+    notification a user wants to mark read is this user.
+    """
+    return user == obj.recipient.user
diff --git a/aleksis/core/util/sass_helpers.py b/aleksis/core/util/sass_helpers.py
index a439a22f93446bad6762ec0a2b34a717bdd19cac..cff75a936f81801eeeae1839bdc8d59eaef739cd 100644
--- a/aleksis/core/util/sass_helpers.py
+++ b/aleksis/core/util/sass_helpers.py
@@ -1,16 +1,34 @@
+"""Helpers for SASS/SCSS compilation."""
+
+import os
+from glob import glob
+
 from django.conf import settings
 
 from colour import web2hex
-from constance import config
 from sass import SassColor
 
+from .core_helpers import get_site_preferences
+
 
 def get_colour(html_colour: str) -> SassColor:
+    """Get a SASS colour object from an HTML colour string."""
     rgb = web2hex(html_colour, force_long=True)[1:]
     r, g, b = int(rgb[0:2], 16), int(rgb[2:4], 16), int(rgb[4:6], 16)
 
     return SassColor(r, g, b, 255)
 
 
-def get_config(setting: str) -> str:
-    return getattr(config, setting, "") or getattr(settings, setting, "")
+def get_preference(section: str, name: str) -> str:
+    """Get a preference from dynamic-preferences."""
+    return get_site_preferences()[f"{section}__{name}"]
+
+
+def clean_scss(*args, **kwargs) -> None:
+    """Unlink compiled CSS (i.e. cache invalidation)."""
+    for source_map in glob(os.path.join(settings.STATIC_ROOT, "*.css.map")):
+        try:
+            os.unlink(source_map)
+        except OSError:
+            # Ignore because old is better than nothing
+            pass  # noqa
diff --git a/aleksis/core/util/search.py b/aleksis/core/util/search.py
index 6720fb6b4236f10d3bfb1932969483d4d4db6d8f..5c8af9f860df94649d46f1fc0dd0741e35c6ad07 100644
--- a/aleksis/core/util/search.py
+++ b/aleksis/core/util/search.py
@@ -5,13 +5,14 @@ from haystack import indexes
 # Not used here, but simplifies imports for apps
 Indexable = indexes.Indexable  # noqa
 
-if settings.HAYSTACK_SIGNAL_PROCESSOR == 'celery_haystack.signals.CelerySignalProcessor':
-    from haystack.indexes import SearchIndex as BaseSearchIndex
-else:
+if settings.HAYSTACK_SIGNAL_PROCESSOR == "celery_haystack.signals.CelerySignalProcessor":
     from celery_haystack.indexes import CelerySearchIndex as BaseSearchIndex
+else:
+    from haystack.indexes import SearchIndex as BaseSearchIndex
+
 
 class SearchIndex(BaseSearchIndex):
-    """ Base class for search indexes on AlekSIS models
+    """Base class for search indexes on AlekSIS models.
 
     It provides a default document field caleld text and exects
     the related model in the model attribute.
diff --git a/aleksis/core/views.py b/aleksis/core/views.py
index c6aa5f5c1e4efd7d71c1c3cfa55996381647f8f8..e854fd0f8b60c549e03022414fa0686342b2b193 100644
--- a/aleksis/core/views.py
+++ b/aleksis/core/views.py
@@ -1,34 +1,69 @@
-from importlib import import_module
 from typing import Optional
 
 from django.apps import apps
-from django.contrib.auth.decorators import login_required
+from django.conf import settings
 from django.core.exceptions import PermissionDenied
-from django.http import Http404, HttpRequest, HttpResponse
+from django.core.paginator import Paginator
+from django.http import HttpRequest, HttpResponse, HttpResponseNotFound
 from django.shortcuts import get_object_or_404, redirect, render
+from django.urls import reverse_lazy
 from django.utils.translation import gettext_lazy as _
 
-from django_tables2 import RequestConfig
+import reversion
+from django_tables2 import RequestConfig, SingleTableView
+from dynamic_preferences.forms import preference_form_builder
+from guardian.shortcuts import get_objects_for_user
 from haystack.inputs import AutoQuery
 from haystack.query import SearchQuerySet
+from haystack.views import SearchView
+from health_check.views import MainView
+from rules.contrib.views import PermissionRequiredMixin, permission_required
 
-from .decorators import admin_required, person_required
+from .filters import GroupFilter, PersonFilter
 from .forms import (
+    AnnouncementForm,
+    ChildGroupsForm,
+    EditAdditionalFieldForm,
     EditGroupForm,
+    EditGroupTypeForm,
     EditPersonForm,
-    EditSchoolForm,
-    EditTermForm,
+    GroupPreferenceForm,
+    PersonPreferenceForm,
     PersonsAccountsFormSet,
-    AnnouncementForm,
+    SchoolTermForm,
+    SitePreferenceForm,
+)
+from .mixins import AdvancedCreateView, AdvancedEditView
+from .models import (
+    AdditionalField,
+    Announcement,
+    DashboardWidget,
+    Group,
+    GroupType,
+    Notification,
+    Person,
+    SchoolTerm,
+)
+from .registries import (
+    group_preferences_registry,
+    person_preferences_registry,
+    site_preferences_registry,
+)
+from .tables import (
+    AdditionalFieldsTable,
+    GroupsTable,
+    GroupTypesTable,
+    PersonsTable,
+    SchoolTermTable,
 )
-from .models import Activity, Group, Notification, Person, School, DashboardWidget, Announcement
-from .tables import GroupsTable, PersonsTable
 from .util import messages
 from .util.apps import AppConfig
+from .util.core_helpers import objectgetter_optional
 
 
-@person_required
+@permission_required("core.view_dashboard")
 def index(request: HttpRequest) -> HttpResponse:
+    """View for dashboard."""
     context = {}
 
     activities = request.user.person.activities.all()[:5]
@@ -51,71 +86,106 @@ def index(request: HttpRequest) -> HttpResponse:
     return render(request, "core/index.html", context)
 
 
-def offline(request):
-    return render(request, "core/offline.html")
+def offline(request: HttpRequest) -> HttpResponse:
+    """Offline message for PWA."""
+    return render(request, "core/pages/offline.html")
 
 
-def about(request):
+def about(request: HttpRequest) -> HttpResponse:
+    """About page listing all apps."""
     context = {}
 
-    context["app_configs"] = list(filter(lambda a: isinstance(a, AppConfig), apps.get_app_configs()))
+    context["app_configs"] = list(
+        filter(lambda a: isinstance(a, AppConfig), apps.get_app_configs())
+    )
+
+    return render(request, "core/pages/about.html", context)
+
+
+class SchoolTermListView(SingleTableView, PermissionRequiredMixin):
+    """Table of all school terms."""
+
+    model = SchoolTerm
+    table_class = SchoolTermTable
+    permission_required = "core.view_schoolterm"
+    template_name = "core/school_term/list.html"
+
 
-    return render(request, "core/about.html", context)
+class SchoolTermCreateView(AdvancedCreateView, PermissionRequiredMixin):
+    """Create view for school terms."""
 
+    model = SchoolTerm
+    form_class = SchoolTermForm
+    permission_required = "core.add_schoolterm"
+    template_name = "core/school_term/create.html"
+    success_url = reverse_lazy("school_terms")
+    success_message = _("The school term has been created.")
 
-@login_required
+
+class SchoolTermEditView(AdvancedEditView, PermissionRequiredMixin):
+    """Edit view for school terms."""
+
+    model = SchoolTerm
+    form_class = SchoolTermForm
+    permission_required = "core.edit_schoolterm"
+    template_name = "core/school_term/edit.html"
+    success_url = reverse_lazy("school_terms")
+    success_message = _("The school term has been saved.")
+
+
+@permission_required("core.view_persons")
 def persons(request: HttpRequest) -> HttpResponse:
+    """List view listing all persons."""
     context = {}
 
     # Get all persons
-    persons = Person.objects.filter(is_active=True)
+    persons = get_objects_for_user(
+        request.user, "core.view_person", Person.objects.filter(is_active=True)
+    )
+
+    # Get filter
+    persons_filter = PersonFilter(request.GET, queryset=persons)
+    context["persons_filter"] = persons_filter
 
     # Build table
-    persons_table = PersonsTable(persons)
+    persons_table = PersonsTable(persons_filter.qs)
     RequestConfig(request).configure(persons_table)
     context["persons_table"] = persons_table
 
-    return render(request, "core/persons.html", context)
+    return render(request, "core/person/list.html", context)
 
 
-@person_required
+@permission_required(
+    "core.view_person", fn=objectgetter_optional(Person, "request.user.person", True)
+)
 def person(request: HttpRequest, id_: Optional[int] = None) -> HttpResponse:
+    """Detail view for one person; defaulting to logged-in person."""
     context = {}
 
-    # Get person and check access
-    try:
-        if id_ is None:
-            person = request.user.person
-        else:
-            person = Person.objects.get(pk=id_)
-    except Person.DoesNotExist as e:
-        # Turn not-found object into a 404 error
-        raise Http404 from e
-
+    person = objectgetter_optional(Person, "request.user.person", True)(request, id_)
     context["person"] = person
 
     # Get groups where person is member of
     groups = Group.objects.filter(members=person)
 
+    # Get filter
+    groups_filter = GroupFilter(request.GET, queryset=groups)
+    context["groups_filter"] = groups_filter
+
     # Build table
-    groups_table = GroupsTable(groups)
+    groups_table = GroupsTable(groups_filter.qs)
     RequestConfig(request).configure(groups_table)
     context["groups_table"] = groups_table
 
-    return render(request, "core/person_full.html", context)
+    return render(request, "core/person/full.html", context)
 
 
-@login_required
+@permission_required("core.view_group", fn=objectgetter_optional(Group, None, False))
 def group(request: HttpRequest, id_: int) -> HttpResponse:
+    """Detail view for one group."""
     context = {}
 
-    # Get group and check if it exist
-    try:
-        group = Group.objects.get(pk=id_)
-    except Group.DoesNotExist as e:
-        # Turn not-found object into a 404 error
-        raise Http404 from e
-
+    group = objectgetter_optional(Group, None, False)(request, id_)
     context["group"] = group
 
     # Get group
@@ -124,45 +194,62 @@ def group(request: HttpRequest, id_: int) -> HttpResponse:
     # Get members
     members = group.members.filter(is_active=True)
 
+    # Get filter
+    members_filter = PersonFilter(request.GET, queryset=members)
+    context["members_filter"] = members_filter
+
     # Build table
-    members_table = PersonsTable(members)
+    members_table = PersonsTable(members_filter.qs)
     RequestConfig(request).configure(members_table)
     context["members_table"] = members_table
 
     # Get owners
     owners = group.owners.filter(is_active=True)
 
+    # Get filter
+    owners_filter = PersonFilter(request.GET, queryset=owners)
+    context["owners_filter"] = owners_filter
+
     # Build table
-    owners_table = PersonsTable(owners)
+    owners_table = PersonsTable(owners_filter.qs)
     RequestConfig(request).configure(owners_table)
     context["owners_table"] = owners_table
 
     # Get statistics
     context["stats"] = group.get_group_stats
 
-    return render(request, "core/group_full.html", context)
+    return render(request, "core/group/full.html", context)
 
 
-@login_required
+@permission_required("core.view_groups")
 def groups(request: HttpRequest) -> HttpResponse:
+    """List view for listing all groups."""
     context = {}
 
     # Get all groups
-    groups = Group.objects.all()
+    groups = get_objects_for_user(request.user, "core.view_group", Group)
+
+    # Get filter
+    groups_filter = GroupFilter(request.GET, queryset=groups)
+    context["groups_filter"] = groups_filter
 
     # Build table
-    groups_table = GroupsTable(groups)
+    groups_table = GroupsTable(group_filter.qs)
     RequestConfig(request).configure(groups_table)
     context["groups_table"] = groups_table
 
-    return render(request, "core/groups.html", context)
+    return render(request, "core/group/list.html", context)
 
 
-@admin_required
+@permission_required("core.link_persons_accounts")
 def persons_accounts(request: HttpRequest) -> HttpResponse:
+    """View allowing to batch-process linking of users to persons."""
     context = {}
 
+    # Get all persons
     persons_qs = Person.objects.all()
+
+    # Form set with one form per known person
     persons_accounts_formset = PersonsAccountsFormSet(request.POST or None, queryset=persons_qs)
 
     if request.method == "POST":
@@ -171,151 +258,189 @@ def persons_accounts(request: HttpRequest) -> HttpResponse:
 
     context["persons_accounts_formset"] = persons_accounts_formset
 
-    return render(request, "core/persons_accounts.html", context)
+    return render(request, "core/person/accounts.html", context)
 
 
-@admin_required
-def edit_person(request: HttpRequest, id_: int) -> HttpResponse:
+@permission_required("core.assign_child_groups_to_groups")
+def groups_child_groups(request: HttpRequest) -> HttpResponse:
+    """View for batch-processing assignment from child groups to groups."""
     context = {}
 
-    person = get_object_or_404(Person, id=id_)
+    # Apply filter
+    filter_ = GroupFilter(request.GET, queryset=Group.objects.all())
+    context["filter"] = filter_
 
-    edit_person_form = EditPersonForm(request.POST or None, request.FILES or None, instance=person)
+    # Paginate
+    paginator = Paginator(filter_.qs, 1)
+    page_number = request.POST.get("page", request.POST.get("old_page"))
 
-    context["person"] = person
+    if page_number:
+        page = paginator.get_page(page_number)
+        group = page[0]
 
-    if request.method == "POST":
-        if edit_person_form.is_valid():
-            edit_person_form.save(commit=True)
+        if "save" in request.POST:
+            form = ChildGroupsForm(request.POST)
+            form.is_valid()
 
-            messages.success(request, _("The person has been saved."))
-            return redirect("edit_person_by_id", id_=person.id)
+            if "child_groups" in form.cleaned_data:
+                group.child_groups.set(form.cleaned_data["child_groups"])
+                group.save()
+                messages.success(request, _("The child groups were successfully saved."))
+        else:
+            # Init form
+            form = ChildGroupsForm(initial={"child_groups": group.child_groups.all()})
 
-    context["edit_person_form"] = edit_person_form
+        context["paginator"] = paginator
+        context["page"] = page
+        context["group"] = group
+        context["form"] = form
 
-    return render(request, "core/edit_person.html", context)
+    return render(request, "core/group/child_groups.html", context)
 
 
-@admin_required
-def edit_group(request: HttpRequest, id_: Optional[int] = None) -> HttpResponse:
+@permission_required("core.edit_person", fn=objectgetter_optional(Person))
+def edit_person(request: HttpRequest, id_: Optional[int] = None) -> HttpResponse:
+    """Edit view for a single person, defaulting to logged-in person."""
     context = {}
 
+    person = objectgetter_optional(Person)(request, id_)
+    context["person"] = person
+
     if id_:
-        group = get_object_or_404(Group, id=id_)
-        edit_group_form = EditGroupForm(request.POST or None, instance=group)
+        # Edit form for existing group
+        edit_person_form = EditPersonForm(request.POST or None, instance=person)
     else:
-        group = None
-        edit_group_form = EditGroupForm(request.POST or None)
+        # Empty form to create a new group
+        if request.user.has_perm("core.create_person"):
+            edit_person_form = EditPersonForm(request.POST or None)
+        else:
+            raise PermissionDenied()
 
     if request.method == "POST":
-        if edit_group_form.is_valid():
-            edit_group_form.save(commit=True)
+        if edit_person_form.is_valid():
+            with reversion.create_revision():
+                edit_person_form.save(commit=True)
+            messages.success(request, _("The person has been saved."))
 
-            messages.success(request, _("The group has been saved."))
-            return redirect("groups")
+            # Redirect to self to ensure post-processed data is displayed
+            return redirect("edit_person_by_id", id_=person.id)
 
-    context["group"] = group
-    context["edit_group_form"] = edit_group_form
+    context["edit_person_form"] = edit_person_form
 
-    return render(request, "core/edit_group.html", context)
+    return render(request, "core/person/edit.html", context)
 
 
-@admin_required
-def data_management(request: HttpRequest) -> HttpResponse:
-    context = {}
-    return render(request, "core/data_management.html", context)
+def get_group_by_id(request: HttpRequest, id_: Optional[int] = None):
+    if id_:
+        return get_object_or_404(Group, id=id_)
+    else:
+        return None
 
 
-@admin_required
-def system_status(request: HttpRequest) -> HttpResponse:
+@permission_required("core.edit_group", fn=objectgetter_optional(Group, None, False))
+def edit_group(request: HttpRequest, id_: Optional[int] = None) -> HttpResponse:
+    """View to edit or create a group."""
     context = {}
 
-    return render(request, "core/system_status.html", context)
-
+    group = objectgetter_optional(Group, None, False)(request, id_)
+    context["group"] = group
 
-@admin_required
-def school_management(request: HttpRequest) -> HttpResponse:
-    context = {}
-    return render(request, "core/school_management.html", context)
+    if id_:
+        # Edit form for existing group
+        edit_group_form = EditGroupForm(request.POST or None, instance=group)
+    else:
+        # Empty form to create a new group
+        if request.user.has_perm("core.create_group"):
+            edit_group_form = EditGroupForm(request.POST or None)
+        else:
+            raise PermissionDenied()
 
+    if request.method == "POST":
+        if edit_group_form.is_valid():
+            with reversion.create_revision():
+                group = edit_group_form.save(commit=True)
 
-@admin_required
-def edit_school(request: HttpRequest) -> HttpResponse:
-    context = {}
+            messages.success(request, _("The group has been saved."))
 
-    school = School.objects.first()
-    edit_school_form = EditSchoolForm(request.POST or None, request.FILES or None, instance=school)
+            return redirect("group_by_id", group.pk)
 
-    context["school"] = school
+    context["edit_group_form"] = edit_group_form
 
-    if request.method == "POST":
-        if edit_school_form.is_valid():
-            edit_school_form.save(commit=True)
+    return render(request, "core/group/edit.html", context)
 
-            messages.success(request, _("The school has been saved."))
-            return redirect("index")
 
-    context["edit_school_form"] = edit_school_form
+@permission_required("core.manage_data")
+def data_management(request: HttpRequest) -> HttpResponse:
+    """View with special menu for data management."""
+    context = {}
+    return render(request, "core/management/data_management.html", context)
 
-    return render(request, "core/edit_school.html", context)
 
+class SystemStatus(MainView, PermissionRequiredMixin):
+    """View giving information about the system status."""
 
-@admin_required
-def edit_schoolterm(request: HttpRequest) -> HttpResponse:
+    template_name = "core/pages/system_status.html"
+    permission_required = "core.view_system_status"
     context = {}
 
-    term = School.objects.first().current_term
-    edit_term_form = EditTermForm(request.POST or None, instance=term)
-
-    if request.method == "POST":
-        if edit_term_form.is_valid():
-            edit_term_form.save(commit=True)
+    def get(self, request, *args, **kwargs):
+        status_code = 500 if self.errors else 200
 
-            messages.success(request, _("The term has been saved."))
-            return redirect("index")
+        if "django_celery_results" in settings.INSTALLED_APPS:
+            from django_celery_results.models import TaskResult  # noqa
+            from celery.task.control import inspect  # noqa
 
-    context["edit_term_form"] = edit_term_form
+            if inspect().registered_tasks():
+                job_list = list(inspect().registered_tasks().values())[0]
+                results = []
+                for job in job_list:
+                    results.append(TaskResult.objects.filter(task_name=job).last())
 
-    return render(request, "core/edit_schoolterm.html", context)
+        context = {"plugins": self.plugins, "status_code": status_code}
+        return self.render_to_response(context, status=status_code)
 
 
+@permission_required(
+    "core.mark_notification_as_read", fn=objectgetter_optional(Notification, None, False)
+)
 def notification_mark_read(request: HttpRequest, id_: int) -> HttpResponse:
-    context = {}
+    """Mark a notification read."""
+    notification = objectgetter_optional(Notification, None, False)(request, id_)
 
-    notification = get_object_or_404(Notification, pk=id_)
-
-    if notification.recipient.user == request.user:
-        notification.read = True
-        notification.save()
-    else:
-        raise PermissionDenied(_("You are not allowed to mark notifications from other users as read!"))
+    notification.read = True
+    notification.save()
 
+    # Redirect to dashboard as this is only used from there if JavaScript is unavailable
     return redirect("index")
 
 
-@admin_required
+@permission_required("core.view_announcements")
 def announcements(request: HttpRequest) -> HttpResponse:
+    """List view of announcements."""
     context = {}
 
-    # Get all persons
+    # Get all announcements
     announcements = Announcement.objects.all()
     context["announcements"] = announcements
 
     return render(request, "core/announcement/list.html", context)
 
 
-@admin_required
-def announcement_form(request: HttpRequest, pk: Optional[int] = None) -> HttpResponse:
+@permission_required(
+    "core.create_or_edit_announcement", fn=objectgetter_optional(Announcement, None, False)
+)
+def announcement_form(request: HttpRequest, id_: Optional[int] = None) -> HttpResponse:
+    """View to create or edit an announcement."""
     context = {}
 
-    if pk:
-        announcement = get_object_or_404(Announcement, pk=pk)
-        form = AnnouncementForm(
-            request.POST or None,
-            instance=announcement
-        )
+    announcement = objectgetter_optional(Announcement, None, False)(request, id_)
+
+    if announcement:
+        # Edit form for existing announcement
+        form = AnnouncementForm(request.POST or None, instance=announcement)
         context["mode"] = "edit"
     else:
+        # Empty form to create new announcement
         form = AnnouncementForm(request.POST or None)
         context["mode"] = "add"
 
@@ -331,22 +456,240 @@ def announcement_form(request: HttpRequest, pk: Optional[int] = None) -> HttpRes
     return render(request, "core/announcement/form.html", context)
 
 
-@admin_required
-def delete_announcement(request: HttpRequest, pk: int) -> HttpResponse:
+@permission_required(
+    "core.delete_announcement", fn=objectgetter_optional(Announcement, None, False)
+)
+def delete_announcement(request: HttpRequest, id_: int) -> HttpResponse:
+    """View to delete an announcement."""
     if request.method == "POST":
-        announcement = get_object_or_404(Announcement, pk=pk)
+        announcement = objectgetter_optional(Announcement, None, False)(request, id_)
         announcement.delete()
         messages.success(request, _("The announcement has been deleted."))
 
     return redirect("announcements")
 
 
-@login_required
+@permission_required("core.search")
 def searchbar_snippets(request: HttpRequest) -> HttpResponse:
-    query = request.GET.get('q', '')
-    limit = int(request.GET.get('limit', '5'))
+    """View to return HTML snippet with searchbar autocompletion results."""
+    query = request.GET.get("q", "")
+    limit = int(request.GET.get("limit", "5"))
 
     results = SearchQuerySet().filter(text=AutoQuery(query))[:limit]
     context = {"results": results}
 
     return render(request, "search/searchbar_snippets.html", context)
+
+
+class PermissionSearchView(PermissionRequiredMixin, SearchView):
+    """Wrapper to apply permission to haystack's search view."""
+
+    permission_required = "core.search"
+
+    def create_response(self):
+        context = self.get_context()
+        if not self.has_permission():
+            return self.handle_no_permission()
+        return render(self.request, self.template, context)
+
+
+def preferences(
+    request: HttpRequest,
+    registry_name: str = "person",
+    pk: Optional[int] = None,
+    section: Optional[str] = None,
+) -> HttpResponse:
+    """View for changing preferences."""
+    context = {}
+
+    # Decide which registry to use and check preferences
+    if registry_name == "site":
+        registry = site_preferences_registry
+        instance = request.site
+        form_class = SitePreferenceForm
+
+        if not request.user.has_perm("core.change_site_preferences", instance):
+            raise PermissionDenied()
+    elif registry_name == "person":
+        registry = person_preferences_registry
+        instance = objectgetter_optional(Person, "request.user.person", True)(request, pk)
+        form_class = PersonPreferenceForm
+
+        if not request.user.has_perm("core.change_person_preferences", instance):
+            raise PermissionDenied()
+    elif registry_name == "group":
+        registry = group_preferences_registry
+        instance = objectgetter_optional(Group, None, False)(request, pk)
+        form_class = GroupPreferenceForm
+
+        if not request.user.has_perm("core.change_group_preferences", instance):
+            raise PermissionDenied()
+    else:
+        # Invalid registry name passed from URL
+        return HttpResponseNotFound()
+
+    # Build final form from dynamic-preferences
+    form_class = preference_form_builder(form_class, instance=instance, section=section)
+
+    if request.method == "POST":
+        form = form_class(request.POST, request.FILES or None)
+        if form.is_valid():
+            form.update_preferences()
+            messages.success(request, _("The preferences have been saved successfully."))
+    else:
+        form = form_class()
+
+    context["registry"] = registry
+    context["registry_name"] = registry_name
+    context["section"] = section
+    context["registry_url"] = "preferences_" + registry_name
+    context["form"] = form
+    context["instance"] = instance
+
+    return render(request, "dynamic_preferences/form.html", context)
+
+
+@permission_required("core.delete_person", fn=objectgetter_optional(Person))
+def delete_person(request: HttpRequest, id_: int) -> HttpResponse:
+    """View to delete an person."""
+    person = objectgetter_optional(Person)(request, id_)
+
+    with reversion.create_revision():
+        person.save()
+
+    person.delete()
+    messages.success(request, _("The person has been deleted."))
+
+    return redirect("persons")
+
+
+@permission_required("core.delete_group", fn=objectgetter_optional(Group))
+def delete_group(request: HttpRequest, id_: int) -> HttpResponse:
+    """View to delete an group."""
+    group = objectgetter_optional(Group)(request, id_)
+    with reversion.create_revision():
+        group.save()
+
+    group.delete()
+    messages.success(request, _("The group has been deleted."))
+
+    return redirect("groups")
+
+
+@permission_required(
+    "core.change_additionalfield", fn=objectgetter_optional(AdditionalField, None, False)
+)
+def edit_additional_field(request: HttpRequest, id_: Optional[int] = None) -> HttpResponse:
+    """View to edit or create a additional_field."""
+    context = {}
+
+    additional_field = objectgetter_optional(AdditionalField, None, False)(request, id_)
+    context["additional_field"] = additional_field
+
+    if id_:
+        # Edit form for existing additional_field
+        edit_additional_field_form = EditAdditionalFieldForm(
+            request.POST or None, instance=additional_field
+        )
+    else:
+        if request.user.has_perm("core.create_additionalfield"):
+            # Empty form to create a new additional_field
+            edit_additional_field_form = EditAdditionalFieldForm(request.POST or None)
+        else:
+            raise PermissionDenied()
+
+    if request.method == "POST":
+        if edit_additional_field_form.is_valid():
+            edit_additional_field_form.save(commit=True)
+
+            messages.success(request, _("The additional_field has been saved."))
+
+            return redirect("additional_fields")
+
+    context["edit_additional_field_form"] = edit_additional_field_form
+
+    return render(request, "core/additional_field/edit.html", context)
+
+
+@permission_required("core.view_additionalfield")
+def additional_fields(request: HttpRequest) -> HttpResponse:
+    """List view for listing all additional fields."""
+    context = {}
+
+    # Get all additional fields
+    additional_fields = get_objects_for_user(
+        request.user, "core.view_additionalfield", AdditionalField
+    )
+
+    # Build table
+    additional_fields_table = AdditionalFieldsTable(additional_fields)
+    RequestConfig(request).configure(additional_fields_table)
+    context["additional_fields_table"] = additional_fields_table
+
+    return render(request, "core/additional_field/list.html", context)
+
+
+@permission_required(
+    "core.delete_additionalfield", fn=objectgetter_optional(AdditionalField, None, False)
+)
+def delete_additional_field(request: HttpRequest, id_: int) -> HttpResponse:
+    """View to delete an additional field."""
+    additional_field = objectgetter_optional(AdditionalField, None, False)(request, id_)
+    additional_field.delete()
+    messages.success(request, _("The additional field has been deleted."))
+
+    return redirect("additional_fields")
+
+
+@permission_required("core.change_grouptype", fn=objectgetter_optional(GroupType, None, False))
+def edit_group_type(request: HttpRequest, id_: Optional[int] = None) -> HttpResponse:
+    """View to edit or create a group_type."""
+    context = {}
+
+    group_type = objectgetter_optional(GroupType, None, False)(request, id_)
+    context["group_type"] = group_type
+
+    if id_:
+        # Edit form for existing group_type
+        edit_group_type_form = EditGroupTypeForm(request.POST or None, instance=group_type)
+    else:
+        # Empty form to create a new group_type
+        edit_group_type_form = EditGroupTypeForm(request.POST or None)
+
+    if request.method == "POST":
+        if edit_group_type_form.is_valid():
+            edit_group_type_form.save(commit=True)
+
+            messages.success(request, _("The group type has been saved."))
+
+            return redirect("group_types")
+
+    context["edit_group_type_form"] = edit_group_type_form
+
+    return render(request, "core/group_type/edit.html", context)
+
+
+@permission_required("core.view_grouptype")
+def group_types(request: HttpRequest) -> HttpResponse:
+    """List view for listing all group types."""
+    context = {}
+
+    # Get all group types
+    group_types = get_objects_for_user(request.user, "core.view_grouptype", GroupType)
+
+    # Build table
+    group_types_table = GroupTypesTable(group_types)
+    RequestConfig(request).configure(group_types_table)
+    context["group_types_table"] = group_types_table
+
+    return render(request, "core/group_type/list.html", context)
+
+
+@permission_required("core.delete_grouptype", fn=objectgetter_optional(GroupType, None, False))
+def delete_group_type(request: HttpRequest, id_: int) -> HttpResponse:
+    """View to delete an group_type."""
+    group_type = objectgetter_optional(GroupType, None, False)(request, id_)
+    group_type.delete()
+    messages.success(request, _("The group type has been deleted."))
+
+    return redirect("group_types")
diff --git a/apps/official/AlekSIS-App-Chronos b/apps/official/AlekSIS-App-Chronos
index 6833571a02b8df133ea0f3b99f75e93da0c50fc9..b081e4d32922d38e4f6229f33e7cecf5a1b408e0 160000
--- a/apps/official/AlekSIS-App-Chronos
+++ b/apps/official/AlekSIS-App-Chronos
@@ -1 +1 @@
-Subproject commit 6833571a02b8df133ea0f3b99f75e93da0c50fc9
+Subproject commit b081e4d32922d38e4f6229f33e7cecf5a1b408e0
diff --git a/apps/official/AlekSIS-App-DashboardFeeds b/apps/official/AlekSIS-App-DashboardFeeds
index bbf946c3c91d94d425889a1bcc6436acb7f1819d..85814a4f6c4fc1d94a853b46be8544c11b5767ee 160000
--- a/apps/official/AlekSIS-App-DashboardFeeds
+++ b/apps/official/AlekSIS-App-DashboardFeeds
@@ -1 +1 @@
-Subproject commit bbf946c3c91d94d425889a1bcc6436acb7f1819d
+Subproject commit 85814a4f6c4fc1d94a853b46be8544c11b5767ee
diff --git a/apps/official/AlekSIS-App-Exlibris b/apps/official/AlekSIS-App-Exlibris
deleted file mode 160000
index ab4e3f2d03bba8b4c2b6ad90d08106f423ae32e4..0000000000000000000000000000000000000000
--- a/apps/official/AlekSIS-App-Exlibris
+++ /dev/null
@@ -1 +0,0 @@
-Subproject commit ab4e3f2d03bba8b4c2b6ad90d08106f423ae32e4
diff --git a/apps/official/AlekSIS-App-Hjelp b/apps/official/AlekSIS-App-Hjelp
new file mode 160000
index 0000000000000000000000000000000000000000..31500a0580b6839122df35f114e471334903d26e
--- /dev/null
+++ b/apps/official/AlekSIS-App-Hjelp
@@ -0,0 +1 @@
+Subproject commit 31500a0580b6839122df35f114e471334903d26e
diff --git a/apps/official/AlekSIS-App-LDAP b/apps/official/AlekSIS-App-LDAP
index f99aa641eb5cdeaf5421e5297813cbcb3c55eb6c..487a3423abc8b8bc8a4d89e474336c76b5cafa61 160000
--- a/apps/official/AlekSIS-App-LDAP
+++ b/apps/official/AlekSIS-App-LDAP
@@ -1 +1 @@
-Subproject commit f99aa641eb5cdeaf5421e5297813cbcb3c55eb6c
+Subproject commit 487a3423abc8b8bc8a4d89e474336c76b5cafa61
diff --git a/apps/official/AlekSIS-App-Untis b/apps/official/AlekSIS-App-Untis
index a567021ee5e66657b3374a76865bab1ee9bdbc33..0b417dfef095fb7a52f03301abb66066a841b467 160000
--- a/apps/official/AlekSIS-App-Untis
+++ b/apps/official/AlekSIS-App-Untis
@@ -1 +1 @@
-Subproject commit a567021ee5e66657b3374a76865bab1ee9bdbc33
+Subproject commit 0b417dfef095fb7a52f03301abb66066a841b467
diff --git a/ci/build_dist.yml b/ci/build_dist.yml
new file mode 100644
index 0000000000000000000000000000000000000000..939cdf326decd7642edff67a62f84ff028f30d56
--- /dev/null
+++ b/ci/build_dist.yml
@@ -0,0 +1,7 @@
+build_dist:
+  stage: build
+  script:
+    - tox -e build
+  artifacts:
+    paths:
+      - dist/
diff --git a/ci/build_docker.yml b/ci/build_docker.yml
new file mode 100644
index 0000000000000000000000000000000000000000..92a1572c6ac4d1878110500f458262a6a7c5bdf4
--- /dev/null
+++ b/ci/build_docker.yml
@@ -0,0 +1,22 @@
+build_docker:
+  stage: build
+  image:
+    name: gcr.io/kaniko-project/executor:debug
+    entrypoint: [""]
+  script:
+    - echo "{\"auths\":{\"$CI_REGISTRY\":{\"username\":\"$CI_REGISTRY_USER\",\"password\":\"$CI_REGISTRY_PASSWORD\"}}}" >/kaniko/.docker/config.json
+    - /kaniko/executor
+       --context $CI_PROJECT_DIR
+       --dockerfile $CI_PROJECT_DIR/Dockerfile
+       --destination $CI_REGISTRY_IMAGE:$CI_COMMIT_REF_NAME
+       --cache=true
+       --cleanup
+    - /kaniko/executor
+       --context $CI_PROJECT_DIR/docker/nginx
+       --dockerfile $CI_PROJECT_DIR/docker/nginx/Dockerfile
+       --destination $CI_REGISTRY_IMAGE/nginx:$CI_COMMIT_REF_NAME
+       --cache=true
+       --cleanup
+  only:
+    - master
+    - tags
diff --git a/ci/deploy.yml b/ci/deploy.yml
new file mode 100644
index 0000000000000000000000000000000000000000..d01e23e4f3436bc8dd4849d283038591157e9668
--- /dev/null
+++ b/ci/deploy.yml
@@ -0,0 +1,43 @@
+pages:
+  stage: deploy
+  before_script:
+    - cp -r .tox/screenshots/firefox docs/screenshots
+  script:
+    - export LC_ALL=en_GB.utf8
+    - tox -e docs -- BUILDDIR=../public/docs
+  artifacts:
+    paths:
+    - public/
+  only:
+    - master
+
+deploy_demo-master:
+  stage: deploy
+  environment:
+    name: demo/master
+    url: http://demo-master.aleksis.org
+  before_script:
+    - 'which ssh-agent || ( apt-get update -y && apt-get install openssh-client -y )'
+    - eval $(ssh-agent -s)
+    - echo "$SSH_PRIVATE_KEY" | tr -d '\r' | ssh-add -
+    - mkdir -p ~/.ssh
+    - chmod 700 ~/.ssh
+    - echo "$SSH_KNOWN_HOSTS" >~/.ssh/known_hosts
+    - chmod 644 ~/.ssh/known_hosts
+  script:
+    - grep -v "build:" docker-compose.yml | ssh root@demo-master.aleksis.org
+       env ALEKSIS_IMAGE_TAG=${CI_COMMIT_REF_NAME}
+       docker-compose
+        -p aleksis-${CI_ENVIRONMENT_SLUG}
+        -f /dev/stdin
+        pull
+    - grep -v "build:" docker-compose.yml | ssh root@demo-master.aleksis.org
+       env ALEKSIS_IMAGE_TAG=${CI_COMMIT_REF_NAME}
+           NGINX_HTTP_PORT=80
+           ALEKSIS_maintenance__debug=true
+       docker-compose
+        -p aleksis-${CI_ENVIRONMENT_SLUG}
+        -f /dev/stdin
+        up -d
+  only:
+    - master
diff --git a/ci/general.yml b/ci/general.yml
new file mode 100644
index 0000000000000000000000000000000000000000..58891983dbeda6b9c8f49dc07e490a021a4c31dd
--- /dev/null
+++ b/ci/general.yml
@@ -0,0 +1,20 @@
+image: registry.edugit.org/teckids/team-sysadmin/docker-images/python-pimped:latest
+
+stages:
+  - test
+  - build
+  - deploy
+
+variables:
+  GIT_SUBMODULE_STRATEGY: recursive
+  PIP_CACHE_DIR: "$CI_PROJECT_DIR/.cache/pip"
+  FF_NETWORK_PER_BUILD: "true"
+
+cache:
+  key:
+    files:
+      - poetry.lock
+      - pyproject.toml
+  paths:
+    - .cache/pip
+    - .tox
diff --git a/ci/test.yml b/ci/test.yml
new file mode 100644
index 0000000000000000000000000000000000000000..2ab64bc324536c8716c5cb8641d09d9c43dbf844
--- /dev/null
+++ b/ci/test.yml
@@ -0,0 +1,26 @@
+test:
+  stage: test
+  services:
+    - name: selenium/standalone-firefox
+      alias: selenium
+  before_script:
+    - adduser --disabled-password --gecos "Test User" testuser
+    - chown -R testuser .
+  script:
+    - sudo apt update
+    - sudo apt install python3-ldap libldap2-dev libssl-dev libsasl2-dev python3.7-dev -y
+    - sudo -u testuser
+      env TEST_SELENIUM_HUB=http://selenium:4444/wd/hub
+          TEST_SELENIUM_BROWSERS=firefox
+          TEST_HOST=build
+      tox -e selenium -- --junitxml=.tox/junit.xml
+  artifacts:
+    paths:
+      - .tox/screenshots
+    reports:
+      junit: .tox/junit.xml
+
+lint:
+  stage: test
+  script:
+    - tox -e lint,security
diff --git a/dev.sh b/dev.sh
index ffe8aee433c1e1bcd93ba229bedea74b8e6dc86d..33c36f4748de2c5ceb2b3c3eb812e12d11013c67 100755
--- a/dev.sh
+++ b/dev.sh
@@ -53,7 +53,7 @@ case "$1" in
     "gource")
 	for d in . apps/official/*; do
 		gource --output-custom-log - "$d"
-	done | sort -n | gource --log-format custom --background-image aleksis/core/static/img/aleksis-icon.png -
+	done | sort -n | gource --log-format custom --background-image aleksis/core/static/img/aleksis-icon.png "$@" -
 	exit
 	;;
 
diff --git a/docker/entrypoint.sh b/docker/entrypoint.sh
index 8c9c64e55abb848ae6d67d42f99e5434f9916a94..4b68fa884420b1a40ed34d4bd4410aabd2ac1e3f 100755
--- a/docker/entrypoint.sh
+++ b/docker/entrypoint.sh
@@ -20,6 +20,7 @@ done
 python manage.py compilescss
 python manage.py collectstatic --no-input --clear
 python manage.py migrate
+python manage.py createinitialrevisions
 
 ARG=${$1:-"gunicorn"}
 
diff --git a/docs/admin/02_ldap.rst b/docs/admin/02_ldap.rst
index e9c4745fcbf5767c6e6edc2c21f9b4ba0f64742e..1239d43e1b45d255100b249c417b44bd5de67b51 100644
--- a/docs/admin/02_ldap.rst
+++ b/docs/admin/02_ldap.rst
@@ -28,5 +28,7 @@ existing file or add a new one)::
   [default.ldap]
   uri = "ldaps://ldap.myschool.edu"
   bind = { dn = "cn=reader,dc=myschool,dc=edu", password = "secret" }
-  users = { base = "ou=people,dc=myschool,dc=edu", filter = "(uid=%(user)s)" }
+
+  [default.ldap.users]
+  search = { base = "ou=people,dc=myschool,dc=edu", filter = "(uid=%(user)s)" }
   map = { first_name = "givenName", last_name = "sn", email = "mail" }
diff --git a/docs/conf.py b/docs/conf.py
index 24652c53867fe91709e0616105cb54f2fd88f59e..5817b0ad2b1195aeb704d7561fa57aec5a6d880a 100644
--- a/docs/conf.py
+++ b/docs/conf.py
@@ -8,29 +8,30 @@
 
 # -- Path setup --------------------------------------------------------------
 
+import os
+import sys
+
 # If extensions (or modules to document with autodoc) are in another directory,
 # add these directories to sys.path here. If the directory is relative to the
 # documentation root, use os.path.abspath to make it absolute, like shown here.
 #
 import django
-import os
-import sys
-sys.path.insert(0, os.path.abspath('..'))
-os.environ['DJANGO_SETTINGS_MODULE'] = 'aleksis.core.settings'
-os.environ['LOCAL_SETTINGS_FILE'] = os.path.abspath(
-    os.path.join('..', 'local.cfg'))
+
+sys.path.insert(0, os.path.abspath(".."))
+os.environ["DJANGO_SETTINGS_MODULE"] = "aleksis.core.settings"
+os.environ["LOCAL_SETTINGS_FILE"] = os.path.abspath(os.path.join("..", "local.cfg"))
 django.setup()
 
 # -- Project information -----------------------------------------------------
 
-project = 'AlekSIS'
-copyright = '2019, 2020, AlekSIS team'
-author = 'AlekSIS team'
+project = "AlekSIS"
+copyright = "2019, 2020, AlekSIS team"
+author = "AlekSIS team"
 
 # The short X.Y version
-version = '2.0'
+version = "2.0"
 # The full version, including alpha/beta/rc tags
-release = '2.0a1'
+release = "2.0a1"
 
 
 # -- General configuration ---------------------------------------------------
@@ -43,24 +44,24 @@ release = '2.0a1'
 # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
 # ones.
 extensions = [
-    'sphinx.ext.autodoc',
-    'sphinxcontrib_django',
-    'sphinx_autodoc_typehints',
-    'sphinx.ext.intersphinx',
-    'sphinx.ext.viewcode',
+    "sphinx.ext.autodoc",
+    "sphinxcontrib_django",
+    "sphinx_autodoc_typehints",
+    "sphinx.ext.intersphinx",
+    "sphinx.ext.viewcode",
 ]
 
 # Add any paths that contain templates here, relative to this directory.
-templates_path = ['_templates']
+templates_path = ["_templates"]
 
 # The suffix(es) of source filenames.
 # You can specify multiple suffix as a list of string:
 #
 # source_suffix = ['.rst', '.md']
-source_suffix = '.rst'
+source_suffix = ".rst"
 
 # The master toctree document.
-master_doc = 'index'
+master_doc = "index"
 
 # The language for content autogenerated by Sphinx. Refer to documentation
 # for a list of supported languages.
@@ -72,7 +73,7 @@ language = None
 # List of patterns, relative to source directory, that match files and
 # directories to ignore when looking for source files.
 # This pattern also affects html_static_path and html_extra_path.
-exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store']
+exclude_patterns = ["_build", "Thumbs.db", ".DS_Store"]
 
 # The name of the Pygments (syntax highlighting) style to use.
 pygments_style = None
@@ -83,7 +84,7 @@ pygments_style = None
 # The theme to use for HTML and HTML Help pages.  See the documentation for
 # a list of builtin themes.
 #
-html_theme = 'alabaster'
+html_theme = "alabaster"
 
 # 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
@@ -94,7 +95,7 @@ html_theme = 'alabaster'
 # 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,
 # so a file named "default.css" will overwrite the builtin "default.css".
-html_static_path = ['_static']
+html_static_path = ["_static"]
 
 # Custom sidebar templates, must be a dictionary that maps document names
 # to template names.
@@ -110,7 +111,7 @@ html_static_path = ['_static']
 # -- Options for HTMLHelp output ---------------------------------------------
 
 # Output file base name for HTML help builder.
-htmlhelp_basename = 'AlekSISdoc'
+htmlhelp_basename = "AlekSISdoc"
 
 
 # -- Options for LaTeX output ------------------------------------------------
@@ -119,15 +120,12 @@ latex_elements = {
     # The paper size ('letterpaper' or 'a4paper').
     #
     # 'papersize': 'letterpaper',
-
     # The font size ('10pt', '11pt' or '12pt').
     #
     # 'pointsize': '10pt',
-
     # Additional stuff for the LaTeX preamble.
     #
     # 'preamble': '',
-
     # Latex figure (float) alignment
     #
     # 'figure_align': 'htbp',
@@ -137,8 +135,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, "AlekSIS.tex", "AlekSIS Documentation", "AlekSIS team", "manual"),
 ]
 
 
@@ -146,10 +143,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", "AlekSIS Documentation", [author], 1)]
 
 
 # -- Options for Texinfo output ----------------------------------------------
@@ -158,9 +152,15 @@ man_pages = [
 # (source start file, target name, title, author,
 #  dir menu entry, description, category)
 texinfo_documents = [
-    (master_doc, 'AlekSIS', 'AlekSIS Documentation',
-     author, 'AlekSIS', 'One line description of project.',
-     'Miscellaneous'),
+    (
+        master_doc,
+        "AlekSIS",
+        "AlekSIS Documentation",
+        author,
+        "AlekSIS",
+        "One line description of project.",
+        "Miscellaneous",
+    ),
 ]
 
 
@@ -179,7 +179,7 @@ epub_title = project
 # epub_uid = ''
 
 # A list of files that should not be packed into the epub file.
-epub_exclude_files = ['search.html']
+epub_exclude_files = ["search.html"]
 
 
 # -- Extension configuration -------------------------------------------------
@@ -187,5 +187,7 @@ epub_exclude_files = ['search.html']
 # -- Options for intersphinx extension ---------------------------------------
 
 # Example configuration for intersphinx: refer to the Python standard library.
-intersphinx_mapping = {'https://docs.python.org/': None,
-                       'https://docs.djangoproject.com/en/stable': 'https://docs.djangoproject.com/en/stable/_objects'}
+intersphinx_mapping = {
+    "https://docs.python.org/": None,
+    "https://docs.djangoproject.com/en/stable": "https://docs.djangoproject.com/en/stable/_objects",
+}
diff --git a/docs/dev/01_setup.rst b/docs/dev/01_setup.rst
index 880737d3087915f4e9cc8c9c76485b923e77243a..d61c21e9a074310ec599f8bcffb6824dabe64bac 100644
--- a/docs/dev/01_setup.rst
+++ b/docs/dev/01_setup.rst
@@ -28,7 +28,7 @@ Install native dependencies
 
 Some system libraries are required to install AlekSIS::
 
-  sudo apt install build-essential libpq-dev libpq5 libssl-dev python3-dev python3-pip python3-venv yarnpkg
+  sudo apt install build-essential libpq-dev libpq5 libssl-dev python3-dev python3-pip python3-venv yarnpkg gettext
 
 
 Get Poetry
@@ -70,6 +70,7 @@ All three steps can be done with the ``poetry run`` command and
   poetry run ./manage.py collectstatic
   poetry run ./manage.py compilemessages
   poetry run ./manage.py migrate
+  poetry run ./manage.py createinitialrevisions
 
 (You might need database settings for the `migrate` command; see below.)
 
diff --git a/manage.py b/manage.py
index 80c8e811820451fffa585a63b57791a6fe1f4ce2..023e9fd47cbc4c0308997db1061aace8bd8afb61 100755
--- a/manage.py
+++ b/manage.py
@@ -2,8 +2,8 @@
 import os
 import sys
 
-if __name__ == '__main__':
-    os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'aleksis.core.settings')
+if __name__ == "__main__":
+    os.environ.setdefault("DJANGO_SETTINGS_MODULE", "aleksis.core.settings")
     try:
         from django.core.management import execute_from_command_line
     except ImportError as exc:
diff --git a/poetry.lock b/poetry.lock
index 6893b47254efa4705507f421317319d5a99d8e70..cc0f45102b25299110e3d77f897bc120b8c8b49a 100644
--- a/poetry.lock
+++ b/poetry.lock
@@ -12,7 +12,7 @@ description = "Low-level AMQP client for Python (fork of amqplib)."
 name = "amqp"
 optional = true
 python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
-version = "2.5.2"
+version = "2.6.0"
 
 [package.dependencies]
 vine = ">=1.1.3,<5.0.0a1"
@@ -23,7 +23,7 @@ description = "A small Python module for determining appropriate platform-specif
 name = "appdirs"
 optional = false
 python-versions = "*"
-version = "1.4.3"
+version = "1.4.4"
 
 [[package]]
 category = "main"
@@ -31,10 +31,10 @@ description = "ASGI specs, helper code, and adapters"
 name = "asgiref"
 optional = false
 python-versions = ">=3.5"
-version = "3.2.7"
+version = "3.2.10"
 
 [package.extras]
-tests = ["pytest (>=4.3.0,<4.4.0)", "pytest-asyncio (>=0.10.0,<0.11.0)"]
+tests = ["pytest", "pytest-asyncio"]
 
 [[package]]
 category = "dev"
@@ -43,7 +43,7 @@ marker = "sys_platform == \"win32\""
 name = "atomicwrites"
 optional = false
 python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
-version = "1.3.0"
+version = "1.4.0"
 
 [[package]]
 category = "dev"
@@ -91,7 +91,7 @@ description = "Screen-scraping library"
 name = "beautifulsoup4"
 optional = false
 python-versions = "*"
-version = "4.9.0"
+version = "4.9.1"
 
 [package.dependencies]
 soupsieve = [">1.2", "<2.0"]
@@ -134,9 +134,10 @@ description = "An easy safelist-based HTML-sanitizing tool."
 name = "bleach"
 optional = false
 python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
-version = "3.1.4"
+version = "3.1.5"
 
 [package.dependencies]
+packaging = "*"
 six = ">=1.9.0"
 webencodings = "*"
 
@@ -146,7 +147,7 @@ description = "Define boolean algebras, create and parse boolean expressions and
 name = "boolean.py"
 optional = false
 python-versions = "*"
-version = "3.7"
+version = "3.8"
 
 [[package]]
 category = "main"
@@ -164,12 +165,13 @@ category = "main"
 description = "Distributed Task Queue."
 name = "celery"
 optional = true
-python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*,"
-version = "4.4.2"
+python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
+version = "4.4.6"
 
 [package.dependencies]
 billiard = ">=3.6.3.0,<4.0"
-kombu = ">=4.6.8,<4.7"
+future = ">=0.18.0"
+kombu = ">=4.6.10,<4.7"
 pytz = ">0.0-dev"
 vine = "1.3.0"
 
@@ -186,10 +188,10 @@ arangodb = ["pyArango (>=1.3.2)"]
 auth = ["cryptography"]
 azureblockblob = ["azure-storage (0.36.0)", "azure-common (1.1.5)", "azure-storage-common (1.1.0)"]
 brotli = ["brotli (>=1.0.0)", "brotlipy (>=0.7.0)"]
-cassandra = ["cassandra-driver"]
+cassandra = ["cassandra-driver (<3.21.0)"]
 consul = ["python-consul"]
 cosmosdbsql = ["pydocumentdb (2.3.2)"]
-couchbase = ["couchbase", "couchbase-cffi"]
+couchbase = ["couchbase-cffi (<3.0.0)", "couchbase (<3.0.0)"]
 couchdb = ["pycouchdb"]
 django = ["Django (>=1.11)"]
 dynamodb = ["boto3 (>=1.9.178)"]
@@ -209,7 +211,7 @@ s3 = ["boto3 (>=1.9.125)"]
 slmq = ["softlayer-messaging (>=1.0.3)"]
 solar = ["ephem"]
 sqlalchemy = ["sqlalchemy"]
-sqs = ["boto3 (>=1.9.125)", "pycurl (7.43.0.2)"]
+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)"]
@@ -221,10 +223,23 @@ description = "An app for integrating Celery with Haystack."
 name = "celery-haystack"
 optional = true
 python-versions = "*"
-version = "0.3.1"
+version = "0.10"
 
 [package.dependencies]
-django-appconf = ">=0.2.1"
+django-appconf = ">=0.4.1"
+
+[[package]]
+category = "main"
+description = "Drop in, configurable, dependency-free progress bars for your Django/Celery applications."
+name = "celery-progress"
+optional = false
+python-versions = "*"
+version = "0.0.10"
+
+[package.extras]
+rabbitmq = ["channels-rabbitmq"]
+redis = ["channels-redis"]
+websockets = ["channels"]
 
 [[package]]
 category = "main"
@@ -232,7 +247,7 @@ description = "Python package for providing Mozilla's CA Bundle."
 name = "certifi"
 optional = false
 python-versions = "*"
-version = "2020.4.5.1"
+version = "2020.6.20"
 
 [[package]]
 category = "main"
@@ -248,7 +263,7 @@ description = "Composable command line interface toolkit"
 name = "click"
 optional = false
 python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
-version = "7.1.1"
+version = "7.1.2"
 
 [[package]]
 category = "main"
@@ -292,14 +307,6 @@ version = "5.1"
 [package.extras]
 toml = ["toml"]
 
-[[package]]
-category = "dev"
-description = "Distribution utilities"
-name = "distlib"
-optional = false
-python-versions = "*"
-version = "0.3.0"
-
 [[package]]
 category = "dev"
 description = "Use Database URLs in your Django Application."
@@ -314,7 +321,7 @@ description = "A high-level Python Web framework that encourages rapid developme
 name = "django"
 optional = false
 python-versions = ">=3.6"
-version = "3.0.5"
+version = "3.0.7"
 
 [package.dependencies]
 asgiref = ">=3.2,<4.0"
@@ -353,7 +360,7 @@ description = "Django LDAP authentication backend."
 name = "django-auth-ldap"
 optional = true
 python-versions = ">=3.5"
-version = "2.1.1"
+version = "2.2.0"
 
 [package.dependencies]
 Django = ">=1.11"
@@ -382,6 +389,17 @@ version = "2.2.0"
 [package.dependencies]
 Django = ">=1.8"
 
+[[package]]
+category = "main"
+description = "Django utility for a memoization decorator that uses the Django cache framework."
+name = "django-cache-memoize"
+optional = false
+python-versions = ">=3.4"
+version = "0.1.7"
+
+[package.extras]
+dev = ["flake8", "tox", "twine", "therapist", "black"]
+
 [[package]]
 category = "main"
 description = "Database-backed Periodic Tasks."
@@ -437,24 +455,7 @@ description = "simple color field for your models with a nice color-picker in th
 name = "django-colorfield"
 optional = false
 python-versions = "*"
-version = "0.2.2"
-
-[[package]]
-category = "main"
-description = "Django live settings with pluggable backends, including Redis."
-name = "django-constance"
-optional = false
-python-versions = "*"
-version = "2.6.0"
-
-[package.dependencies]
-[package.dependencies.django-picklefield]
-optional = true
-version = "*"
-
-[package.extras]
-database = ["django-picklefield"]
-redis = ["redis"]
+version = "0.3.1"
 
 [[package]]
 category = "main"
@@ -481,27 +482,52 @@ version = "2.2"
 Django = ">=1.11"
 sqlparse = ">=0.2.0"
 
+[[package]]
+category = "main"
+description = "Dynamic global and instance settings for your django project"
+name = "django-dynamic-preferences"
+optional = false
+python-versions = "*"
+version = "1.9"
+
+[package.dependencies]
+django = ">=1.11"
+persisting-theory = ">=0.2.1"
+six = "*"
+
 [[package]]
 category = "main"
 description = "Yet another Django audit log app, hopefully the simplest one."
 name = "django-easy-audit"
 optional = false
 python-versions = "*"
-version = "1.2.2b4"
+version = "1.2.3a5"
 
 [package.dependencies]
 beautifulsoup4 = "*"
 
+[[package]]
+category = "main"
+description = "simple Django app which allows you to upload a image and it renders a wide variety for html link tags to display the favicon"
+name = "django-favicon-plus-reloaded"
+optional = false
+python-versions = "*"
+version = "1.0.4"
+
+[package.dependencies]
+django = "*"
+pillow = "*"
+
 [[package]]
 category = "main"
 description = "Django-filter is a reusable Django application for allowing users to filter querysets dynamically."
 name = "django-filter"
 optional = false
-python-versions = ">=3.4"
-version = "2.2.0"
+python-versions = ">=3.5"
+version = "2.3.0"
 
 [package.dependencies]
-Django = ">=1.11"
+Django = ">=2.2"
 
 [[package]]
 category = "main"
@@ -514,6 +540,17 @@ version = "2.2"
 [package.dependencies]
 Django = ">=1.11"
 
+[[package]]
+category = "main"
+description = "Implementation of per object permissions for Django."
+name = "django-guardian"
+optional = false
+python-versions = ">=3.5"
+version = "2.3.0"
+
+[package.dependencies]
+Django = ">=2.2"
+
 [[package]]
 category = "main"
 description = "Command to anonymize sensitive data."
@@ -540,6 +577,17 @@ version = "3.0b1"
 [package.dependencies]
 Django = ">=2.2"
 
+[[package]]
+category = "main"
+description = "Run checks on services like databases, queue servers, celery processes, etc."
+name = "django-health-check"
+optional = false
+python-versions = "*"
+version = "3.12.1"
+
+[package.dependencies]
+django = ">=1.11"
+
 [[package]]
 category = "main"
 description = "A reusable app for cropping images easily and non-destructively in Django"
@@ -557,7 +605,7 @@ description = "Django app to allow superusers to impersonate other users."
 name = "django-impersonate"
 optional = false
 python-versions = "*"
-version = "1.5"
+version = "1.5.1"
 
 [[package]]
 category = "main"
@@ -612,22 +660,11 @@ description = "Material design for django forms and admin"
 name = "django-material"
 optional = false
 python-versions = "*"
-version = "1.6.3"
+version = "1.6.7"
 
 [package.dependencies]
 six = "*"
 
-[[package]]
-category = "main"
-description = "An implementation of memoization technique for Django."
-name = "django-memoize"
-optional = false
-python-versions = "*"
-version = "2.3.0"
-
-[package.dependencies]
-django = "*"
-
 [[package]]
 category = "main"
 description = "A straightforward menu generator for Django"
@@ -653,15 +690,27 @@ description = "A pluggable framework for adding two-factor authentication to Dja
 name = "django-otp"
 optional = false
 python-versions = "*"
-version = "0.7.5"
+version = "0.9.3"
 
 [package.dependencies]
 django = ">=1.11"
-six = ">=1.10.0"
 
 [package.extras]
 qrcode = ["qrcode"]
 
+[[package]]
+category = "main"
+description = "A django-otp plugin that verifies YubiKey OTP tokens."
+name = "django-otp-yubikey"
+optional = false
+python-versions = "*"
+version = "0.5.2"
+
+[package.dependencies]
+YubiOTP = ">=0.2.2"
+django-otp = ">=0.5.0"
+six = ">=1.10.0"
+
 [[package]]
 category = "main"
 description = "An international phone number field for django models."
@@ -678,20 +727,6 @@ babel = "*"
 phonenumbers = ["phonenumbers (>=7.0.2)"]
 phonenumberslite = ["phonenumberslite (>=7.0.2)"]
 
-[[package]]
-category = "main"
-description = "Pickled object field for Django"
-name = "django-picklefield"
-optional = false
-python-versions = "*"
-version = "2.1.1"
-
-[package.dependencies]
-Django = ">=1.11"
-
-[package.extras]
-tests = ["tox"]
-
 [[package]]
 category = "main"
 description = "Seamless polymorphic inheritance for Django models"
@@ -709,7 +744,7 @@ description = "A Django app to include a manifest.json and Service Worker instan
 name = "django-pwa"
 optional = false
 python-versions = "*"
-version = "1.0.8"
+version = "1.0.9"
 
 [package.dependencies]
 django = ">=1.8"
@@ -725,6 +760,17 @@ version = "0.6"
 [package.dependencies]
 django = ">=1.11"
 
+[[package]]
+category = "main"
+description = "An extension to the Django web framework that provides version control for model instances."
+name = "django-reversion"
+optional = false
+python-versions = ">=3.6"
+version = "3.0.7"
+
+[package.dependencies]
+django = ">=1.11"
+
 [[package]]
 category = "main"
 description = "SASS processor to compile SCSS files into *.css, while rendering, or offline."
@@ -743,12 +789,15 @@ description = "Select2 option fields for Django"
 name = "django-select2"
 optional = false
 python-versions = "*"
-version = "7.2.3"
+version = "7.4.2"
 
 [package.dependencies]
 django = ">=2.2"
 django-appconf = ">=0.6.0"
 
+[package.extras]
+test = ["pytest", "pytest-cov", "pytest-django", "selenium"]
+
 [[package]]
 category = "main"
 description = "Makes specified django settings visible in template rendering context."
@@ -823,10 +872,18 @@ django-otp = ">=0.6.0,<0.99"
 django-phonenumber-field = ">=1.1.0,<3.99"
 qrcode = ">=4.0.0,<6.99"
 
+[package.dependencies.django-otp-yubikey]
+optional = true
+version = "*"
+
 [package.dependencies.phonenumbers]
 optional = true
 version = ">=7.0.9,<8.99"
 
+[package.dependencies.twilio]
+optional = true
+version = ">=6.0"
+
 [package.extras]
 call = ["twilio (>=6.0)"]
 phonenumbers = ["phonenumbers (>=7.0.9,<8.99)"]
@@ -867,12 +924,11 @@ category = "dev"
 description = "A parser for Python dependency files"
 name = "dparse"
 optional = false
-python-versions = "*"
-version = "0.5.0"
+python-versions = ">=3.5"
+version = "0.5.1"
 
 [package.dependencies]
 packaging = "*"
-pipenv = "*"
 pyyaml = "*"
 toml = "*"
 
@@ -922,21 +978,13 @@ version = "2.7"
 django = ">=1.11,<4.0"
 pillow = "*"
 
-[[package]]
-category = "dev"
-description = "Discover and load entry points from installed packages."
-name = "entrypoints"
-optional = false
-python-versions = ">=2.7"
-version = "0.3"
-
 [[package]]
 category = "main"
 description = "Faker is a Python package that generates fake data for you."
 name = "faker"
 optional = false
 python-versions = ">=3.4"
-version = "4.0.3"
+version = "4.1.1"
 
 [package.dependencies]
 python-dateutil = ">=2.4"
@@ -944,25 +992,20 @@ text-unidecode = "1.3"
 
 [[package]]
 category = "dev"
-description = "A platform independent file lock."
-name = "filelock"
-optional = false
-python-versions = "*"
-version = "3.0.12"
-
-[[package]]
-category = "dev"
-description = "the modular source code checker: pep8, pyflakes and co"
+description = "the modular source code checker: pep8 pyflakes and co"
 name = "flake8"
 optional = false
-python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
-version = "3.7.9"
+python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,>=2.7"
+version = "3.8.3"
 
 [package.dependencies]
-entrypoints = ">=0.3.0,<0.4.0"
 mccabe = ">=0.6.0,<0.7.0"
-pycodestyle = ">=2.5.0,<2.6.0"
-pyflakes = ">=2.1.0,<2.2.0"
+pycodestyle = ">=2.6.0a1,<2.7.0"
+pyflakes = ">=2.2.0,<2.3.0"
+
+[package.dependencies.importlib-metadata]
+python = "<3.8"
+version = "*"
 
 [[package]]
 category = "dev"
@@ -984,10 +1027,10 @@ description = "flake8 plugin to call black as a code style validator"
 name = "flake8-black"
 optional = false
 python-versions = "*"
-version = "0.1.1"
+version = "0.2.0"
 
 [package.dependencies]
-black = ">=19.3b0"
+black = "*"
 flake8 = ">=3.0.0"
 
 [[package]]
@@ -996,7 +1039,7 @@ description = "Check for python builtins being used as variables or parameters."
 name = "flake8-builtins"
 optional = false
 python-versions = "*"
-version = "1.5.2"
+version = "1.5.3"
 
 [package.dependencies]
 flake8 = "*"
@@ -1010,7 +1053,7 @@ description = "Plugin to catch bad style specific to Django Projects"
 name = "flake8-django"
 optional = false
 python-versions = "*"
-version = "0.0.4"
+version = "1.1.1"
 
 [package.dependencies]
 flake8 = "*"
@@ -1041,7 +1084,7 @@ description = "flake8 plugin that integrates isort ."
 name = "flake8-isort"
 optional = false
 python-versions = "*"
-version = "2.9.1"
+version = "3.0.0"
 
 [package.dependencies]
 flake8 = ">=3.2.1"
@@ -1090,13 +1133,21 @@ version = "0.0.13"
 flake8 = ">=3.0.0"
 restructuredtext_lint = "*"
 
+[[package]]
+category = "main"
+description = "Clean single-source support for Python 3 and 2"
+name = "future"
+optional = true
+python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*"
+version = "0.18.2"
+
 [[package]]
 category = "dev"
 description = "Git Object Database"
 name = "gitdb"
 optional = false
 python-versions = ">=3.4"
-version = "4.0.4"
+version = "4.0.5"
 
 [package.dependencies]
 smmap = ">=3.0.1,<4"
@@ -1107,7 +1158,7 @@ description = "Python Git Library"
 name = "gitpython"
 optional = false
 python-versions = ">=3.4"
-version = "3.1.1"
+version = "3.1.3"
 
 [package.dependencies]
 gitdb = ">=4.0.1,<5"
@@ -1143,14 +1194,14 @@ marker = "python_version < \"3.8\""
 name = "importlib-metadata"
 optional = false
 python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7"
-version = "1.6.0"
+version = "1.7.0"
 
 [package.dependencies]
 zipp = ">=0.5"
 
 [package.extras]
 docs = ["sphinx", "rst.linker"]
-testing = ["packaging", "importlib-resources"]
+testing = ["packaging", "pep517", "importlib-resources (>=1.3)"]
 
 [[package]]
 category = "dev"
@@ -1186,10 +1237,10 @@ description = "Messaging library for Python."
 name = "kombu"
 optional = true
 python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
-version = "4.6.8"
+version = "4.6.11"
 
 [package.dependencies]
-amqp = ">=2.5.2,<2.6"
+amqp = ">=2.6.0,<2.7"
 
 [package.dependencies.importlib-metadata]
 python = "<3.8"
@@ -1217,7 +1268,7 @@ description = "Sass for Python: A straightforward binding of libsass for Python.
 name = "libsass"
 optional = false
 python-versions = "*"
-version = "0.19.4"
+version = "0.20.0"
 
 [package.dependencies]
 six = "*"
@@ -1255,7 +1306,7 @@ description = "More routines for operating on iterables, beyond itertools"
 name = "more-itertools"
 optional = false
 python-versions = ">=3.5"
-version = "8.2.0"
+version = "8.4.0"
 
 [[package]]
 category = "dev"
@@ -1282,12 +1333,12 @@ python-versions = "*"
 version = "0.4.3"
 
 [[package]]
-category = "dev"
+category = "main"
 description = "Core utilities for Python packages"
 name = "packaging"
 optional = false
 python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
-version = "20.3"
+version = "20.4"
 
 [package.dependencies]
 pyparsing = ">=2.0.2"
@@ -1309,16 +1360,24 @@ optional = false
 python-versions = "*"
 version = "5.4.5"
 
+[[package]]
+category = "main"
+description = "Registries that can autodiscover values accross your project apps"
+name = "persisting-theory"
+optional = false
+python-versions = "*"
+version = "0.2.1"
+
 [[package]]
 category = "dev"
 description = "PostgreSQL interface library"
 name = "pg8000"
 optional = false
 python-versions = ">=3.5"
-version = "1.15.2"
+version = "1.15.3"
 
 [package.dependencies]
-scramp = "1.1.1"
+scramp = "1.2.0"
 
 [[package]]
 category = "main"
@@ -1326,7 +1385,7 @@ description = "Python version of Google's common library for parsing, formatting
 name = "phonenumbers"
 optional = false
 python-versions = "*"
-version = "8.12.1"
+version = "8.12.6"
 
 [[package]]
 category = "main"
@@ -1334,22 +1393,7 @@ description = "Python Imaging Library (Fork)"
 name = "pillow"
 optional = false
 python-versions = ">=3.5"
-version = "7.1.1"
-
-[[package]]
-category = "dev"
-description = "Python Development Workflow for Humans."
-name = "pipenv"
-optional = false
-python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*"
-version = "2018.11.26"
-
-[package.dependencies]
-certifi = "*"
-pip = ">=9.0.1"
-setuptools = ">=36.2.1"
-virtualenv = "*"
-virtualenv-clone = ">=0.2.5"
+version = "7.1.2"
 
 [[package]]
 category = "dev"
@@ -1367,6 +1411,17 @@ version = ">=0.12"
 [package.extras]
 dev = ["pre-commit", "tox"]
 
+[[package]]
+category = "main"
+description = "Cross-platform lib for process and system monitoring in Python."
+name = "psutil"
+optional = false
+python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
+version = "5.7.0"
+
+[package.extras]
+enum = ["enum34"]
+
 [[package]]
 category = "main"
 description = "psycopg2 - Python-PostgreSQL Database Adapter"
@@ -1381,7 +1436,7 @@ description = "library with cross-python path, ini-parsing, io, code, log facili
 name = "py"
 optional = false
 python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
-version = "1.8.1"
+version = "1.9.0"
 
 [[package]]
 category = "main"
@@ -1408,7 +1463,15 @@ description = "Python style guide checker"
 name = "pycodestyle"
 optional = false
 python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
-version = "2.5.0"
+version = "2.6.0"
+
+[[package]]
+category = "main"
+description = "Cryptographic library for Python"
+name = "pycryptodome"
+optional = false
+python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
+version = "3.9.8"
 
 [[package]]
 category = "dev"
@@ -1427,7 +1490,7 @@ description = "passive checker of Python programs"
 name = "pyflakes"
 optional = false
 python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
-version = "2.1.1"
+version = "2.2.0"
 
 [[package]]
 category = "dev"
@@ -1438,7 +1501,20 @@ python-versions = ">=3.5"
 version = "2.6.1"
 
 [[package]]
-category = "dev"
+category = "main"
+description = "JSON Web Token implementation in Python"
+name = "pyjwt"
+optional = false
+python-versions = "*"
+version = "1.7.1"
+
+[package.extras]
+crypto = ["cryptography (>=1.4)"]
+flake8 = ["flake8", "flake8-import-order", "pep8-naming"]
+test = ["pytest (>=4.0.1,<5.0.0)", "pytest-cov (>=2.6.0,<3.0.0)", "pytest-runner (>=4.2,<5.0.0)"]
+
+[[package]]
+category = "main"
 description = "Python parsing module"
 name = "pyparsing"
 optional = false
@@ -1451,7 +1527,7 @@ description = "pytest: simple powerful testing with Python"
 name = "pytest"
 optional = false
 python-versions = ">=3.5"
-version = "5.4.1"
+version = "5.4.3"
 
 [package.dependencies]
 atomicwrites = ">=1.0"
@@ -1476,15 +1552,15 @@ category = "dev"
 description = "Pytest plugin for measuring coverage."
 name = "pytest-cov"
 optional = false
-python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
-version = "2.8.1"
+python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
+version = "2.10.0"
 
 [package.dependencies]
 coverage = ">=4.4"
-pytest = ">=3.6"
+pytest = ">=4.6"
 
 [package.extras]
-testing = ["fields", "hunter", "process-tests (2.0.2)", "six", "virtualenv"]
+testing = ["fields", "hunter", "process-tests (2.0.2)", "six", "pytest-xdist", "virtualenv"]
 
 [[package]]
 category = "dev"
@@ -1519,7 +1595,7 @@ description = "pytest-sugar is a plugin for pytest that changes the default look
 name = "pytest-sugar"
 optional = false
 python-versions = "*"
-version = "0.9.2"
+version = "0.9.3"
 
 [package.dependencies]
 packaging = ">=14.1"
@@ -1543,7 +1619,7 @@ description = "Python Crontab API"
 name = "python-crontab"
 optional = true
 python-versions = "*"
-version = "2.4.1"
+version = "2.5.1"
 
 [package.dependencies]
 python-dateutil = "*"
@@ -1580,7 +1656,7 @@ description = "Python modules for implementing LDAP clients"
 name = "python-ldap"
 optional = true
 python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*"
-version = "3.2.0"
+version = "3.3.0"
 
 [package.dependencies]
 pyasn1 = ">=0.3.7"
@@ -1603,7 +1679,7 @@ description = "World timezone definitions, modern and historical"
 name = "pytz"
 optional = false
 python-versions = "*"
-version = "2019.3"
+version = "2020.1"
 
 [[package]]
 category = "main"
@@ -1637,7 +1713,7 @@ description = "Python client for Redis key-value store"
 name = "redis"
 optional = true
 python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
-version = "3.4.1"
+version = "3.5.3"
 
 [package.extras]
 hiredis = ["hiredis (>=0.1.3)"]
@@ -1648,7 +1724,7 @@ description = "Alternative regular expression module, to replace re."
 name = "regex"
 optional = false
 python-versions = "*"
-version = "2020.4.4"
+version = "2020.6.8"
 
 [[package]]
 category = "main"
@@ -1656,7 +1732,7 @@ description = "Python HTTP for Humans."
 name = "requests"
 optional = false
 python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
-version = "2.23.0"
+version = "2.24.0"
 
 [package.dependencies]
 certifi = ">=2017.4.17"
@@ -1674,22 +1750,30 @@ description = "reStructuredText linter"
 name = "restructuredtext-lint"
 optional = false
 python-versions = "*"
-version = "1.3.0"
+version = "1.3.1"
 
 [package.dependencies]
 docutils = ">=0.11,<1.0"
 
+[[package]]
+category = "main"
+description = "Awesome Django authorization, without the database"
+name = "rules"
+optional = false
+python-versions = "*"
+version = "2.2"
+
 [[package]]
 category = "dev"
-description = "Safety checks your installed dependencies for known security vulnerabilities."
+description = "Checks installed dependencies for known vulnerabilities."
 name = "safety"
 optional = false
-python-versions = "*"
-version = "1.8.7"
+python-versions = ">=3.5"
+version = "1.9.0"
 
 [package.dependencies]
 Click = ">=6.0"
-dparse = ">=0.4.1"
+dparse = ">=0.5.1"
 packaging = "*"
 requests = "*"
 setuptools = "*"
@@ -1700,7 +1784,7 @@ description = "An implementation of the SCRAM protocol."
 name = "scramp"
 optional = false
 python-versions = ">=3.5"
-version = "1.1.1"
+version = "1.2.0"
 
 [[package]]
 category = "dev"
@@ -1719,7 +1803,7 @@ description = "Python 2 and 3 compatibility utilities"
 name = "six"
 optional = false
 python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*"
-version = "1.14.0"
+version = "1.15.0"
 
 [[package]]
 category = "dev"
@@ -1727,7 +1811,7 @@ description = "A pure Python implementation of a sliding window memory map manag
 name = "smmap"
 optional = false
 python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
-version = "3.0.2"
+version = "3.0.4"
 
 [[package]]
 category = "dev"
@@ -1743,7 +1827,7 @@ description = "A modern CSS selector implementation for Beautiful Soup."
 name = "soupsieve"
 optional = false
 python-versions = "*"
-version = "1.9.5"
+version = "1.9.6"
 
 [[package]]
 category = "main"
@@ -1751,7 +1835,7 @@ description = "A simple tool/library for working with SPDX license definitions."
 name = "spdx-license-list"
 optional = false
 python-versions = "*"
-version = "0.4.0"
+version = "0.5.0"
 
 [[package]]
 category = "dev"
@@ -1759,13 +1843,13 @@ description = "Python documentation generator"
 name = "sphinx"
 optional = false
 python-versions = ">=3.5"
-version = "2.4.4"
+version = "3.1.1"
 
 [package.dependencies]
 Jinja2 = ">=2.3"
 Pygments = ">=2.0"
 alabaster = ">=0.7,<0.8"
-babel = ">=1.3,<2.0 || >2.0"
+babel = ">=1.3"
 colorama = ">=0.3.5"
 docutils = ">=0.12"
 imagesize = "*"
@@ -1782,7 +1866,8 @@ sphinxcontrib-serializinghtml = "*"
 
 [package.extras]
 docs = ["sphinxcontrib-websupport"]
-test = ["pytest (<5.3.3)", "pytest-cov", "html5lib", "flake8 (>=3.5.0)", "flake8-import-order", "mypy (>=0.761)", "docutils-stubs"]
+lint = ["flake8 (>=3.5.0)", "flake8-import-order", "mypy (>=0.780)", "docutils-stubs"]
+test = ["pytest", "pytest-cov", "html5lib", "typed-ast", "cython"]
 
 [[package]]
 category = "dev"
@@ -1790,10 +1875,10 @@ description = "Type hints (PEP 484) support for the Sphinx autodoc extension"
 name = "sphinx-autodoc-typehints"
 optional = false
 python-versions = ">=3.5.2"
-version = "1.10.3"
+version = "1.11.0"
 
 [package.dependencies]
-Sphinx = ">=2.1"
+Sphinx = ">=3.0"
 
 [package.extras]
 test = ["pytest (>=3.1.0)", "typing-extensions (>=3.5)", "sphobjinv (>=2.0)", "dataclasses"]
@@ -1891,12 +1976,11 @@ category = "dev"
 description = "Manage dynamic plugins for Python applications"
 name = "stevedore"
 optional = false
-python-versions = "*"
-version = "1.32.0"
+python-versions = ">=3.6"
+version = "2.0.1"
 
 [package.dependencies]
 pbr = ">=2.0.0,<2.1.0 || >2.1.0"
-six = ">=1.10.0"
 
 [[package]]
 category = "dev"
@@ -1912,7 +1996,7 @@ description = "A collection of helpers and mock objects for unit tests and doc t
 name = "testfixtures"
 optional = false
 python-versions = "*"
-version = "6.14.0"
+version = "6.14.1"
 
 [package.extras]
 build = ["setuptools-git", "wheel", "twine"]
@@ -1959,7 +2043,7 @@ description = "Python Library for Tom's Obvious, Minimal Language"
 name = "toml"
 optional = false
 python-versions = "*"
-version = "0.10.0"
+version = "0.10.1"
 
 [[package]]
 category = "main"
@@ -1967,11 +2051,28 @@ description = "Fast, Extensible Progress Meter"
 name = "tqdm"
 optional = false
 python-versions = ">=2.6, !=3.0.*, !=3.1.*"
-version = "4.45.0"
+version = "4.46.1"
 
 [package.extras]
 dev = ["py-make (>=0.1.0)", "twine", "argopt", "pydoc-markdown"]
 
+[[package]]
+category = "main"
+description = "Twilio API client and TwiML generator"
+name = "twilio"
+optional = false
+python-versions = "*"
+version = "6.43.0"
+
+[package.dependencies]
+PyJWT = ">=1.4.2"
+pytz = "*"
+six = "*"
+
+[package.dependencies.requests]
+python = ">=3.0"
+version = ">=2.0.0"
+
 [[package]]
 category = "dev"
 description = "a fork of Python 2 and 3 ast modules with type comment support"
@@ -2011,41 +2112,11 @@ version = "1.3.0"
 
 [[package]]
 category = "dev"
-description = "Virtual Python Environment builder"
-name = "virtualenv"
-optional = false
-python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,>=2.7"
-version = "20.0.18"
-
-[package.dependencies]
-appdirs = ">=1.4.3,<2"
-distlib = ">=0.3.0,<1"
-filelock = ">=3.0.0,<4"
-six = ">=1.9.0,<2"
-
-[package.dependencies.importlib-metadata]
-python = "<3.8"
-version = ">=0.12,<2"
-
-[package.extras]
-docs = ["sphinx (>=2.0.0,<3)", "sphinx-argparse (>=0.2.5,<1)", "sphinx-rtd-theme (>=0.4.3,<1)", "towncrier (>=19.9.0rc1)", "proselint (>=0.10.2,<1)"]
-testing = ["pytest (>=4.0.0,<6)", "coverage (>=4.5.1,<6)", "pytest-mock (>=2.0.0,<3)", "pytest-env (>=0.6.2,<1)", "pytest-timeout (>=1.3.4,<2)", "packaging (>=20.0)", "xonsh (>=0.9.16,<1)"]
-
-[[package]]
-category = "dev"
-description = "script to clone virtualenvs."
-name = "virtualenv-clone"
-optional = false
-python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
-version = "0.5.4"
-
-[[package]]
-category = "dev"
-description = "Measures number of Terminal column cells of wide-character codes"
+description = "Measures the displayed width of unicode strings in a terminal"
 name = "wcwidth"
 optional = false
 python-versions = "*"
-version = "0.1.9"
+version = "0.2.5"
 
 [[package]]
 category = "main"
@@ -2055,6 +2126,18 @@ optional = false
 python-versions = "*"
 version = "0.5.1"
 
+[[package]]
+category = "main"
+description = "A library for verifying YubiKey OTP tokens, both locally and through a Yubico web service."
+name = "yubiotp"
+optional = false
+python-versions = "*"
+version = "0.2.2.post1"
+
+[package.dependencies]
+pycryptodome = "*"
+six = "*"
+
 [[package]]
 category = "main"
 description = "Backport of pathlib-compatible object wrapper for zip files"
@@ -2073,7 +2156,7 @@ celery = ["Celery", "django-celery-results", "django-celery-beat", "django-celer
 ldap = ["django-auth-ldap"]
 
 [metadata]
-content-hash = "afd2f4b69870b913a740e8247dbe3618d40e0b51294203c5e883b9f72427bd71"
+content-hash = "ab4755861d9926350d3e669b36d26ec7c31aa5b68004bb69ecb49eb13419b216"
 python-versions = "^3.7"
 
 [metadata.files]
@@ -2082,20 +2165,20 @@ alabaster = [
     {file = "alabaster-0.7.12.tar.gz", hash = "sha256:a661d72d58e6ea8a57f7a86e37d86716863ee5e92788398526d58b26a4e4dc02"},
 ]
 amqp = [
-    {file = "amqp-2.5.2-py2.py3-none-any.whl", hash = "sha256:6e649ca13a7df3faacdc8bbb280aa9a6602d22fd9d545336077e573a1f4ff3b8"},
-    {file = "amqp-2.5.2.tar.gz", hash = "sha256:77f1aef9410698d20eaeac5b73a87817365f457a507d82edf292e12cbb83b08d"},
+    {file = "amqp-2.6.0-py2.py3-none-any.whl", hash = "sha256:bb68f8d2bced8f93ccfd07d96c689b716b3227720add971be980accfc2952139"},
+    {file = "amqp-2.6.0.tar.gz", hash = "sha256:24dbaff8ce4f30566bb88976b398e8c4e77637171af3af6f1b9650f48890e60b"},
 ]
 appdirs = [
-    {file = "appdirs-1.4.3-py2.py3-none-any.whl", hash = "sha256:d8b24664561d0d34ddfaec54636d502d7cea6e29c3eaf68f3df6180863e2166e"},
-    {file = "appdirs-1.4.3.tar.gz", hash = "sha256:9e5896d1372858f8dd3344faf4e5014d21849c756c8d5701f78f8a103b372d92"},
+    {file = "appdirs-1.4.4-py2.py3-none-any.whl", hash = "sha256:a841dacd6b99318a741b166adb07e19ee71a274450e68237b4650ca1055ab128"},
+    {file = "appdirs-1.4.4.tar.gz", hash = "sha256:7d5d0167b2b1ba821647616af46a749d1c653740dd0d2415100fe26e27afdf41"},
 ]
 asgiref = [
-    {file = "asgiref-3.2.7-py2.py3-none-any.whl", hash = "sha256:9ca8b952a0a9afa61d30aa6d3d9b570bb3fd6bafcf7ec9e6bed43b936133db1c"},
-    {file = "asgiref-3.2.7.tar.gz", hash = "sha256:8036f90603c54e93521e5777b2b9a39ba1bad05773fcf2d208f0299d1df58ce5"},
+    {file = "asgiref-3.2.10-py3-none-any.whl", hash = "sha256:9fc6fb5d39b8af147ba40765234fa822b39818b12cc80b35ad9b0cef3a476aed"},
+    {file = "asgiref-3.2.10.tar.gz", hash = "sha256:7e51911ee147dd685c3c8b805c0ad0cb58d360987b56953878f8c06d2d1c6f1a"},
 ]
 atomicwrites = [
-    {file = "atomicwrites-1.3.0-py2.py3-none-any.whl", hash = "sha256:03472c30eb2c5d1ba9227e4c2ca66ab8287fbfbbda3888aa93dc2e28fc6811b4"},
-    {file = "atomicwrites-1.3.0.tar.gz", hash = "sha256:75a9445bac02d8d058d5e1fe689654ba5a6556a1dfd8ce6ec55a0ed79866cfa6"},
+    {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-19.3.0-py2.py3-none-any.whl", hash = "sha256:08a96c641c3a74e44eb59afb61a24f2cb9f4d7188748e76ba4bb5edfa3cb7d1c"},
@@ -2110,9 +2193,9 @@ bandit = [
     {file = "bandit-1.6.2.tar.gz", hash = "sha256:41e75315853507aa145d62a78a2a6c5e3240fe14ee7c601459d0df9418196065"},
 ]
 beautifulsoup4 = [
-    {file = "beautifulsoup4-4.9.0-py2-none-any.whl", hash = "sha256:a4bbe77fd30670455c5296242967a123ec28c37e9702a8a81bd2f20a4baf0368"},
-    {file = "beautifulsoup4-4.9.0-py3-none-any.whl", hash = "sha256:d4e96ac9b0c3a6d3f0caae2e4124e6055c5dcafde8e2f831ff194c104f0775a0"},
-    {file = "beautifulsoup4-4.9.0.tar.gz", hash = "sha256:594ca51a10d2b3443cbac41214e12dbb2a1cd57e1a7344659849e2e20ba6a8d8"},
+    {file = "beautifulsoup4-4.9.1-py2-none-any.whl", hash = "sha256:e718f2342e2e099b640a34ab782407b7b676f47ee272d6739e60b8ea23829f2c"},
+    {file = "beautifulsoup4-4.9.1-py3-none-any.whl", hash = "sha256:a6237df3c32ccfaee4fd201c8f5f9d9df619b93121d01353a64a73ce8c6ef9a8"},
+    {file = "beautifulsoup4-4.9.1.tar.gz", hash = "sha256:73cc4d115b96f79c7d77c1c7f7a0a8d4c57860d1041df407dd1aae7f07a77fd7"},
 ]
 billiard = [
     {file = "billiard-3.6.3.0-py3-none-any.whl", hash = "sha256:bff575450859a6e0fbc2f9877d9b715b0bbc07c3565bb7ed2280526a0cdf5ede"},
@@ -2123,35 +2206,40 @@ black = [
     {file = "black-19.10b0.tar.gz", hash = "sha256:c2edb73a08e9e0e6f65a0e6af18b059b8b1cdd5bef997d7a0b181df93dc81539"},
 ]
 bleach = [
-    {file = "bleach-3.1.4-py2.py3-none-any.whl", hash = "sha256:cc8da25076a1fe56c3ac63671e2194458e0c4d9c7becfd52ca251650d517903c"},
-    {file = "bleach-3.1.4.tar.gz", hash = "sha256:e78e426105ac07026ba098f04de8abe9b6e3e98b5befbf89b51a5ef0a4292b03"},
+    {file = "bleach-3.1.5-py2.py3-none-any.whl", hash = "sha256:2bce3d8fab545a6528c8fa5d9f9ae8ebc85a56da365c7f85180bfe96a35ef22f"},
+    {file = "bleach-3.1.5.tar.gz", hash = "sha256:3c4c520fdb9db59ef139915a5db79f8b51bc2a7257ea0389f30c846883430a4b"},
 ]
 "boolean.py" = [
-    {file = "boolean.py-3.7-py2.py3-none-any.whl", hash = "sha256:82ae181f9c85cb5c893a5a4daba9f24d60b538a7dd27fd0c6752a77eba4fbeff"},
-    {file = "boolean.py-3.7.tar.gz", hash = "sha256:bd19b412435611ecc712603d0fd7d0e280e24698e7a6e3d5f610473870c5dd1e"},
+    {file = "boolean.py-3.8-py2.py3-none-any.whl", hash = "sha256:d75da0fd0354425fa64f6bbc6cec6ae1485d0eec3447b73187ff8cbf9b572e26"},
+    {file = "boolean.py-3.8.tar.gz", hash = "sha256:cc24e20f985d60cd4a3a5a1c0956dd12611159d32a75081dabd0c9ab981acaa4"},
 ]
 calendarweek = [
     {file = "calendarweek-0.4.5-py3-none-any.whl", hash = "sha256:b35fcc087073969d017cede62a7295bcd714a1304bcb4c4e2b0f23acb0265fb1"},
     {file = "calendarweek-0.4.5.tar.gz", hash = "sha256:5b1788ca435022f9348fc81a718974e51dd85d080f9aa3dad717df70a1bc6e1f"},
 ]
 celery = [
-    {file = "celery-4.4.2-py2.py3-none-any.whl", hash = "sha256:5b4b37e276033fe47575107a2775469f0b721646a08c96ec2c61531e4fe45f2a"},
-    {file = "celery-4.4.2.tar.gz", hash = "sha256:108a0bf9018a871620936c33a3ee9f6336a89f8ef0a0f567a9001f4aa361415f"},
+    {file = "celery-4.4.6-py2.py3-none-any.whl", hash = "sha256:ef17d7dffde7fc73ecab3a3b6389d93d3213bac53fa7f28e68e33647ad50b916"},
+    {file = "celery-4.4.6.tar.gz", hash = "sha256:fd77e4248bb1b7af5f7922dd8e81156f540306e3a5c4b1c24167c1f5f06025da"},
 ]
 celery-haystack = [
-    {file = "celery-haystack-0.3.1.tar.gz", hash = "sha256:49992712e67b1f39afd294dca6ba2820f2d262b3137ad14cb0c57a05fa218725"},
+    {file = "celery-haystack-0.10.tar.gz", hash = "sha256:b6e2a3c70071bef0838ca1a7d9f14fae6c2ecf385704092e59b82147a1ee552e"},
+    {file = "celery_haystack-0.10-py2.py3-none-any.whl", hash = "sha256:ec1f39050661e033f554de99cb9393c2e94427667ff5401f16393b2a68f888fc"},
+]
+celery-progress = [
+    {file = "celery-progress-0.0.10.tar.gz", hash = "sha256:3f7b35e1e6c79eec38f5647b024aa74193d0a41d5b47ecbb85b66f9ca68d5261"},
+    {file = "celery_progress-0.0.10-py3-none-any.whl", hash = "sha256:90941bf3aaeac9333d554a2191fa6cd81ef323472329ace0dd77344ac6aab092"},
 ]
 certifi = [
-    {file = "certifi-2020.4.5.1-py2.py3-none-any.whl", hash = "sha256:1d987a998c75633c40847cc966fcf5904906c920a7f17ef374f5aa4282abd304"},
-    {file = "certifi-2020.4.5.1.tar.gz", hash = "sha256:51fcb31174be6e6664c5f69e3e1691a2d72a1a12e90f872cbdb1567eb47b6519"},
+    {file = "certifi-2020.6.20-py2.py3-none-any.whl", hash = "sha256:8fc0819f1f30ba15bdb34cceffb9ef04d99f420f68eb75d901e9560b8749fc41"},
+    {file = "certifi-2020.6.20.tar.gz", hash = "sha256:5930595817496dd21bb8dc35dad090f1c2cd0adfaf21204bf6732ca5d8ee34d3"},
 ]
 chardet = [
     {file = "chardet-3.0.4-py2.py3-none-any.whl", hash = "sha256:fc323ffcaeaed0e0a02bf4d117757b98aed530d9ed4531e3e15460124c106691"},
     {file = "chardet-3.0.4.tar.gz", hash = "sha256:84ab92ed1c4d4f16916e05906b6b75a6c0fb5db821cc65e70cbd64a3e2a5eaae"},
 ]
 click = [
-    {file = "click-7.1.1-py2.py3-none-any.whl", hash = "sha256:e345d143d80bf5ee7534056164e5e112ea5e22716bbb1ce727941f4c8b471b9a"},
-    {file = "click-7.1.1.tar.gz", hash = "sha256:8a18b4ea89d8820c5d0c7da8a64b2c324b4dabb695804dbfea19b9be9d88c0cc"},
+    {file = "click-7.1.2-py2.py3-none-any.whl", hash = "sha256:dacca89f4bfadd5de3d7489b7c8a566eee0d3676333fbb50030263894c38c0dc"},
+    {file = "click-7.1.2.tar.gz", hash = "sha256:d2b5255c7c6349bc1bd1e59e08cd12acbbd63ce649f2588755783aa94dfb6b1a"},
 ]
 colorama = [
     {file = "colorama-0.4.3-py2.py3-none-any.whl", hash = "sha256:7d73d2a99753107a36ac6b455ee49046802e59d9d076ef8e47b61499fa29afff"},
@@ -2197,16 +2285,13 @@ coverage = [
     {file = "coverage-5.1-cp39-cp39-win_amd64.whl", hash = "sha256:bb28a7245de68bf29f6fb199545d072d1036a1917dca17a1e75bbb919e14ee8e"},
     {file = "coverage-5.1.tar.gz", hash = "sha256:f90bfc4ad18450c80b024036eaf91e4a246ae287701aaa88eaebebf150868052"},
 ]
-distlib = [
-    {file = "distlib-0.3.0.zip", hash = "sha256:2e166e231a26b36d6dfe35a48c4464346620f8645ed0ace01ee31822b288de21"},
-]
 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.0.5-py3-none-any.whl", hash = "sha256:642d8eceab321ca743ae71e0f985ff8fdca59f07aab3a9fb362c617d23e33a76"},
-    {file = "Django-3.0.5.tar.gz", hash = "sha256:d4666c2edefa38c5ede0ec1655424c56dc47ceb04b6d8d62a7eac09db89545c1"},
+    {file = "Django-3.0.7-py3-none-any.whl", hash = "sha256:e1630333248c9b3d4e38f02093a26f1e07b271ca896d73097457996e0fae12e8"},
+    {file = "Django-3.0.7.tar.gz", hash = "sha256:5052b34b34b3425233c682e0e11d658fd6efd587d11335a0203d827224ada8f2"},
 ]
 django-any-js = [
     {file = "django-any-js-1.0.3.post0.tar.gz", hash = "sha256:1da88b44b861b0f54f6b8ea0eb4c7c4fa1a5772e9a4320532cd4e0871a4e23f7"},
@@ -2216,8 +2301,8 @@ django-appconf = [
     {file = "django_appconf-1.0.4-py2.py3-none-any.whl", hash = "sha256:1b1d0e1069c843ebe8ae5aa48ec52403b1440402b320c3e3a206a0907e97bb06"},
 ]
 django-auth-ldap = [
-    {file = "django-auth-ldap-2.1.1.tar.gz", hash = "sha256:fabbbc35a5d28ce6a12e3f6309229d82bfe6a410391914938593e4b96ce42ec8"},
-    {file = "django_auth_ldap-2.1.1-py3-none-any.whl", hash = "sha256:43c47c8eac1d0b1f1ee70d28534c7cef33deefddff996f8fae11dd937cc65e82"},
+    {file = "django-auth-ldap-2.2.0.tar.gz", hash = "sha256:11af1773b08613339d2c3a0cec1308a4d563518f17b1719c3759994d0b4d04bf"},
+    {file = "django_auth_ldap-2.2.0-py3-none-any.whl", hash = "sha256:0ed2d88d81c39be915a9ab53b97ec0a33a3d16055518ab4c9bcffe8236d40370"},
 ]
 django-bleach = [
     {file = "django-bleach-0.6.1.tar.gz", hash = "sha256:674709c26040618aff0741ce8261fd151e5ead405bd50568c2034662d69daac3"},
@@ -2227,6 +2312,10 @@ django-bulk-update = [
     {file = "django-bulk-update-2.2.0.tar.gz", hash = "sha256:5ab7ce8a65eac26d19143cc189c0f041d5c03b9d1b290ca240dc4f3d6aaeb337"},
     {file = "django_bulk_update-2.2.0-py2.py3-none-any.whl", hash = "sha256:49a403392ae05ea872494d74fb3dfa3515f8df5c07cc277c3dc94724c0ee6985"},
 ]
+django-cache-memoize = [
+    {file = "django-cache-memoize-0.1.7.tar.gz", hash = "sha256:5e96349b0159aec1eb79257199a1902ea3ed538231ce7b4fee12e563127ca657"},
+    {file = "django_cache_memoize-0.1.7-py2.py3-none-any.whl", hash = "sha256:bc7f53725558244af62197d0125732d7ec88ecc1281a3a2f37d77ae1a8c269d3"},
+]
 django-celery-beat = [
     {file = "django-celery-beat-2.0.0.tar.gz", hash = "sha256:fdf1255eecfbeb770c6521fe3e69989dfc6373cd5a7f0fe62038d37f80f47e48"},
     {file = "django_celery_beat-2.0.0-py2.py3-none-any.whl", hash = "sha256:fe0b2a1b31d4a6234fea4b31986ddfd4644a48fab216ce1843f3ed0ddd2e9097"},
@@ -2244,11 +2333,8 @@ django-ckeditor = [
     {file = "django_ckeditor-5.9.0-py2.py3-none-any.whl", hash = "sha256:71c3c7bb46b0cbfb9712ef64af0d2a406eab233f44ecd7c42c24bdfa39ae3bde"},
 ]
 django-colorfield = [
-    {file = "django-colorfield-0.2.2.tar.gz", hash = "sha256:49cfce71365de88130e65ced8f2c5c4826b31e9ab0c5f0e721ff13a830b5be76"},
-    {file = "django_colorfield-0.2.2-py2-none-any.whl", hash = "sha256:ecb8af68f35028e35f973ddb687c2dcae86d028c6da1b72580c0d3fae915d3b7"},
-]
-django-constance = [
-    {file = "django-constance-2.6.0.tar.gz", hash = "sha256:12d827f9d5552ee39884fb6fb356f231f32b1ab8958acc715e3d1a6ecf913653"},
+    {file = "django-colorfield-0.3.1.tar.gz", hash = "sha256:5e3a720fd8dd46878b62d8d06b9ec81927d5ba8d2bc828ebaba49e7775f8393f"},
+    {file = "django_colorfield-0.3.1-py2-none-any.whl", hash = "sha256:5830fcc4b1ab3111e2e6c8a2d75e25c09e404cea80d9f50002b09f068c8854df"},
 ]
 django-dbbackup = [
     {file = "django-dbbackup-3.3.0.tar.gz", hash = "sha256:bb109735cae98b64ad084e5b461b7aca2d7b39992f10c9ed9435e3ebb6fb76c8"},
@@ -2257,18 +2343,30 @@ django-debug-toolbar = [
     {file = "django-debug-toolbar-2.2.tar.gz", hash = "sha256:eabbefe89881bbe4ca7c980ff102e3c35c8e8ad6eb725041f538988f2f39a943"},
     {file = "django_debug_toolbar-2.2-py3-none-any.whl", hash = "sha256:ff94725e7aae74b133d0599b9bf89bd4eb8f5d2c964106e61d11750228c8774c"},
 ]
+django-dynamic-preferences = [
+    {file = "django-dynamic-preferences-1.9.tar.gz", hash = "sha256:407db27bf55d391c4c8a4944e0521f35eff82c2f2fd5a2fc843fb1b4cc1a31f4"},
+    {file = "django_dynamic_preferences-1.9-py2.py3-none-any.whl", hash = "sha256:a3c84696f0459d8d6d9c43374ff3db7daa59b46670b461bb954057d08af607e1"},
+]
 django-easy-audit = [
-    {file = "django-easy-audit-1.2.2b4.tar.gz", hash = "sha256:eac94b76882c6ad3fdb76d15f4f4ea281dc61e0897e92a457e058b87ed21ff68"},
-    {file = "django_easy_audit-1.2.2b4-py3-none-any.whl", hash = "sha256:49ef3beea7bf439b349daa66d5e3d7624a7c9005d3bfd51f54d15dd5dcfaa202"},
+    {file = "django-easy-audit-1.2.3a5.tar.gz", hash = "sha256:48fc3042760485eb9baae232c4ce21fb96476246e341ee7656a09db3ab3df047"},
+    {file = "django_easy_audit-1.2.3a5-py3-none-any.whl", hash = "sha256:32a60f1278f8b34aeda7c6f16a8f04bfab777584a38829e28e468ce0d5a45313"},
+]
+django-favicon-plus-reloaded = [
+    {file = "django-favicon-plus-reloaded-1.0.4.tar.gz", hash = "sha256:90c761c636a338e6e9fb1d086649d82095085f92cff816c9cf074607f28c85a5"},
+    {file = "django_favicon_plus_reloaded-1.0.4-py3-none-any.whl", hash = "sha256:26e4316d41328a61ced52c7fc0ead795f0eb194d6a30311c34a9833c6fe30a7c"},
 ]
 django-filter = [
-    {file = "django-filter-2.2.0.tar.gz", hash = "sha256:c3deb57f0dd7ff94d7dce52a047516822013e2b441bed472b722a317658cfd14"},
-    {file = "django_filter-2.2.0-py3-none-any.whl", hash = "sha256:558c727bce3ffa89c4a7a0b13bc8976745d63e5fd576b3a9a851650ef11c401b"},
+    {file = "django-filter-2.3.0.tar.gz", hash = "sha256:11e63dd759835d9ba7a763926ffb2662cf8a6dcb4c7971a95064de34dbc7e5af"},
+    {file = "django_filter-2.3.0-py3-none-any.whl", hash = "sha256:616848eab6fc50193a1b3730140c49b60c57a3eda1f7fc57fa8505ac156c6c75"},
 ]
 django-formtools = [
     {file = "django-formtools-2.2.tar.gz", hash = "sha256:c5272c03c1cd51b2375abf7397a199a3148a9fbbf2f100e186467a84025d13b2"},
     {file = "django_formtools-2.2-py2.py3-none-any.whl", hash = "sha256:304fa777b8ef9e0693ce7833f885cb89ba46b0e46fc23b01176900a93f46742f"},
 ]
+django-guardian = [
+    {file = "django-guardian-2.3.0.tar.gz", hash = "sha256:ed2de26e4defb800919c5749fb1bbe370d72829fbd72895b6cf4f7f1a7607e1b"},
+    {file = "django_guardian-2.3.0-py3-none-any.whl", hash = "sha256:0e70706c6cda88ddaf8849bddb525b8df49de05ba0798d4b3506049f0d95cbc8"},
+]
 django-hattori = [
     {file = "django-hattori-0.2.1.tar.gz", hash = "sha256:6953d40881317252f19f62c4e7fe8058924b852c7498bc42beb7bc4d268c252c"},
     {file = "django_hattori-0.2.1-py2.py3-none-any.whl", hash = "sha256:e529ed7af8fc34a0169c797c477672b687a205a56f3f5206f90c260acb83b7ac"},
@@ -2277,12 +2375,16 @@ django-haystack = [
     {file = "django-haystack-3.0b1.tar.gz", hash = "sha256:9dba64f5c76cf147ac382d4a4a270f30d30a45a3a7a1738a9d05c96d18777c07"},
     {file = "django_haystack-3.0b1-py3-none-any.whl", hash = "sha256:b83705e1cf8141cd1755fc6683ac65fea4e1281f4b4306bc9224af96495b0df3"},
 ]
+django-health-check = [
+    {file = "django-health-check-3.12.1.tar.gz", hash = "sha256:0563827e003d25fd4d9ebbd7467dea5f390435628d645aaa63f8889deaded73a"},
+    {file = "django_health_check-3.12.1-py2.py3-none-any.whl", hash = "sha256:9e6b7d93d4902901474efd4e25d31b5aaea7563b570c0260adce52cd3c3a9e36"},
+]
 django-image-cropping = [
     {file = "django-image-cropping-1.4.0.tar.gz", hash = "sha256:6cc4a6bd8901e69b710caceea29b942fdb202da26626313cd9271ae989a83a52"},
     {file = "django_image_cropping-1.4.0-py3-none-any.whl", hash = "sha256:fe6a139c6d5dfc480f2a1d4e7e3e928d5edaefc898e17be66bc5f73140762ad9"},
 ]
 django-impersonate = [
-    {file = "django-impersonate-1.5.tar.gz", hash = "sha256:2c10bcb1c42fe6495d915f4cc4cfd7c5f8375ba39a06b0f062ce6f1e2ff76585"},
+    {file = "django-impersonate-1.5.1.tar.gz", hash = "sha256:7c786ffaa7a5dd430f9277b53a64676c470b684eee5aa52c3b483298860d09b4"},
 ]
 django-ipware = [
     {file = "django-ipware-2.1.0.tar.gz", hash = "sha256:a7c7a8fd019dbdc9c357e6e582f65034e897572fc79a7e467674efa8aef9d00b"},
@@ -2303,11 +2405,8 @@ django-maintenance-mode = [
     {file = "django_maintenance_mode-0.14.0-py2-none-any.whl", hash = "sha256:b4cc24a469ed10897826a28f05d64e6166a58d130e4940ac124ce198cd4cc778"},
 ]
 django-material = [
-    {file = "django-material-1.6.3.tar.gz", hash = "sha256:f8758afe1beabc16a3c54f5437c7fea15946b7d068eedd89c97d57a363793950"},
-    {file = "django_material-1.6.3-py2.py3-none-any.whl", hash = "sha256:502dc88c2f61f190fdc401666e83b47da00cbda98477af6ed8b7d43944ce6407"},
-]
-django-memoize = [
-    {file = "django-memoize-2.3.0.tar.gz", hash = "sha256:85decffbef7d38ffc569dc96527f598e6677bbc01ce29adf722b051da7efd4be"},
+    {file = "django-material-1.6.7.tar.gz", hash = "sha256:3cc68b34348634f019bf529f3e0b99b1474ab36ec9b50040f5e557b5b65add1d"},
+    {file = "django_material-1.6.7-py2.py3-none-any.whl", hash = "sha256:9da268532c92c270b512d9610c9723a07dbfea06db98434dac8aa1dd2910778f"},
 ]
 django-menu-generator = [
     {file = "django-menu-generator-1.0.4.tar.gz", hash = "sha256:ce71a5055c16933c8aff64fb36c21e5cf8b6d505733aceed1252f8b99369a378"},
@@ -2316,34 +2415,38 @@ django-middleware-global-request = [
     {file = "django-middleware-global-request-0.1.2.tar.gz", hash = "sha256:f6490759bc9f7dbde4001709554e29ca715daf847f2222914b4e47117dca9313"},
 ]
 django-otp = [
-    {file = "django-otp-0.7.5.tar.gz", hash = "sha256:1f16c2b93fe484706ff16ac6f5e64ecc73dd240318c333e0560384ba548d3837"},
-    {file = "django_otp-0.7.5-py2.py3-none-any.whl", hash = "sha256:cd4975539be478417033561e9832a1a69a583189f680e92a649f412c661f90aa"},
+    {file = "django-otp-0.9.3.tar.gz", hash = "sha256:d2390e61794bc10dea2fd949cbcfb7946e9ae4fb248df5494ccc4ef9ac50427e"},
+    {file = "django_otp-0.9.3-py3-none-any.whl", hash = "sha256:97849f7bf1b50c4c36a5845ab4d2e11dd472fa8e6bcc34fe18b6d3af6e4aa449"},
+]
+django-otp-yubikey = [
+    {file = "django-otp-yubikey-0.5.2.tar.gz", hash = "sha256:f0b1881562fb42ee9f12c28d284cbdb90d1f0383f2d53a595373b080a19bc261"},
+    {file = "django_otp_yubikey-0.5.2-py2.py3-none-any.whl", hash = "sha256:26b12c763b37e99b95b8b8a54d06d8d54c3774eb26133a452f54558033de732b"},
 ]
 django-phonenumber-field = [
     {file = "django-phonenumber-field-3.0.1.tar.gz", hash = "sha256:794ebbc3068a7af75aa72a80cb0cec67e714ff8409a965968040f1fd210b2d97"},
     {file = "django_phonenumber_field-3.0.1-py3-none-any.whl", hash = "sha256:1ab19f723928582fed412bd9844221fa4ff466276d8526b8b4a9913ee1487c5e"},
 ]
-django-picklefield = [
-    {file = "django-picklefield-2.1.1.tar.gz", hash = "sha256:67a5e156343e3b032cac2f65565f0faa81635a99c7da74b0f07a0f5db467b646"},
-    {file = "django_picklefield-2.1.1-py2.py3-none-any.whl", hash = "sha256:e03cb181b7161af38ad6b573af127e4fe9b7cc2c455b42c1ec43eaad525ade0a"},
-]
 django-polymorphic = [
     {file = "django-polymorphic-2.1.2.tar.gz", hash = "sha256:6e08a76c91066635ccb7ef3ebbe9a0ad149febae6b30be2579716ec16d3c6461"},
     {file = "django_polymorphic-2.1.2-py2.py3-none-any.whl", hash = "sha256:0a25058e95e5e99fe0beeabb8f4734effe242d7b5b77dca416fba9fd3062da6a"},
 ]
 django-pwa = [
-    {file = "django-pwa-1.0.8.tar.gz", hash = "sha256:caf9d6e2a792def272e6cb496f594a9821c4d73cb5117d33560bc7b7b82d6132"},
-    {file = "django_pwa-1.0.8-py3-none-any.whl", hash = "sha256:88a844095ec3dc38ec8edc8d1f95247eccaebefeb41484fb94c10631881b0eb7"},
+    {file = "django-pwa-1.0.9.tar.gz", hash = "sha256:c11bcb40bbbb65f9037e4ae4d7233e6fa724c4410419b257cce4b6624a9542e9"},
+    {file = "django_pwa-1.0.9-py3-none-any.whl", hash = "sha256:8706cbd84489fb63d3523d5037d2cdfd8ff177417292bd7845b0f177d3c4ed3f"},
 ]
 django-render-block = [
     {file = "django_render_block-0.6-py2.py3-none-any.whl", hash = "sha256:95c7dc9610378a10e0c4a10d8364ec7307210889afccd6a67a6aaa0fd599bd4d"},
 ]
+django-reversion = [
+    {file = "django-reversion-3.0.7.tar.gz", hash = "sha256:72fc53580a6b538f0cfff10f27f42333f67d79c406399289c94ec5a193cfb3e1"},
+    {file = "django_reversion-3.0.7-py3-none-any.whl", hash = "sha256:ecab4703ecc0871dc325c3e100139def84eb153622df3413fbcd9de7d3503c78"},
+]
 django-sass-processor = [
     {file = "django-sass-processor-0.8.tar.gz", hash = "sha256:e039551994feaaba6fcf880412b25a772dd313162a34cbb4289814988cfae340"},
 ]
 django-select2 = [
-    {file = "django-select2-7.2.3.tar.gz", hash = "sha256:b4cd3e8c42bdbdc582c38c03a23bc115a90c2f86563282329d37c567d8c34c36"},
-    {file = "django_select2-7.2.3-py2.py3-none-any.whl", hash = "sha256:1482be84449c254ec944c4da0a236b00ec57445304377b783850fd95b269d1ad"},
+    {file = "django-select2-7.4.2.tar.gz", hash = "sha256:9d3330fa0083a03fb69fceb5dcd2e78065cfd08e45c89d4fd727fce4673d3e08"},
+    {file = "django_select2-7.4.2-py2.py3-none-any.whl", hash = "sha256:06531d563ce33c3133682ae2bb9e6d762103a863d0054ffef51bae8b4cfcca6c"},
 ]
 django-settings-context-processor = [
     {file = "django-settings-context-processor-0.2.tar.gz", hash = "sha256:d37c853d69a3069f5abbf94c7f4f6fc0fac38bbd0524190cd5a250ba800e496a"},
@@ -2379,8 +2482,8 @@ docutils = [
     {file = "docutils-0.16.tar.gz", hash = "sha256:c2de3a60e9e7d07be26b7f2b00ca0309c207e06c100f9cc2a94931fc75a478fc"},
 ]
 dparse = [
-    {file = "dparse-0.5.0-py3-none-any.whl", hash = "sha256:14fed5efc5e98c0a81dfe100c4c2ea0a4c189104e9a9d18b5cfd342a163f97be"},
-    {file = "dparse-0.5.0.tar.gz", hash = "sha256:db349e53f6d03c8ee80606c49b35f515ed2ab287a8e1579e2b4bdf52b12b1530"},
+    {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-2.2.3-py2.py3-none-any.whl", hash = "sha256:e803cdab2d7addd539c4ee8d121f15ab0b63a83a5b723150e1746aa7e8063adb"},
@@ -2389,34 +2492,27 @@ dynaconf = [
 easy-thumbnails = [
     {file = "easy-thumbnails-2.7.tar.gz", hash = "sha256:e4e7a0dd4001f56bfd4058428f2c91eafe27d33ef3b8b33ac4e013b159b9ff91"},
 ]
-entrypoints = [
-    {file = "entrypoints-0.3-py2.py3-none-any.whl", hash = "sha256:589f874b313739ad35be6e0cd7efde2a4e9b6fea91edcc34e58ecbb8dbe56d19"},
-    {file = "entrypoints-0.3.tar.gz", hash = "sha256:c70dd71abe5a8c85e55e12c19bd91ccfeec11a6e99044204511f9ed547d48451"},
-]
 faker = [
-    {file = "Faker-4.0.3-py3-none-any.whl", hash = "sha256:53bf2c8a2de8af271466e7b9cc2f08ecf83c4c947981680eb61080779a0adace"},
-    {file = "Faker-4.0.3.tar.gz", hash = "sha256:7292806948ed848f1bcea1e7b963bae6f398687d1da0ea096e156fea2787f454"},
-]
-filelock = [
-    {file = "filelock-3.0.12-py3-none-any.whl", hash = "sha256:929b7d63ec5b7d6b71b0fa5ac14e030b3f70b75747cef1b10da9b879fef15836"},
-    {file = "filelock-3.0.12.tar.gz", hash = "sha256:18d82244ee114f543149c66a6e0c14e9c4f8a1044b5cdaadd0f82159d6a6ff59"},
+    {file = "Faker-4.1.1-py3-none-any.whl", hash = "sha256:1290f589648bc470b8d98fff1fdff773fe3f46b4ca2cac73ac74668b12cf008e"},
+    {file = "Faker-4.1.1.tar.gz", hash = "sha256:c006b3664c270a2cfd4785c5e41ff263d48101c4e920b5961cf9c237131d8418"},
 ]
 flake8 = [
-    {file = "flake8-3.7.9-py2.py3-none-any.whl", hash = "sha256:49356e766643ad15072a789a20915d3c91dc89fd313ccd71802303fd67e4deca"},
-    {file = "flake8-3.7.9.tar.gz", hash = "sha256:45681a117ecc81e870cbf1262835ae4af5e7a8b08e40b944a8a6e6b895914cfb"},
+    {file = "flake8-3.8.3-py2.py3-none-any.whl", hash = "sha256:15e351d19611c887e482fb960eae4d44845013cc142d42896e9862f775d8cf5c"},
+    {file = "flake8-3.8.3.tar.gz", hash = "sha256:f04b9fcbac03b0a3e58c0ab3a0ecc462e023a9faf046d57794184028123aa208"},
 ]
 flake8-bandit = [
     {file = "flake8_bandit-2.1.2.tar.gz", hash = "sha256:687fc8da2e4a239b206af2e54a90093572a60d0954f3054e23690739b0b0de3b"},
 ]
 flake8-black = [
-    {file = "flake8-black-0.1.1.tar.gz", hash = "sha256:56f85aaa5a83f06a3f61e680e3b50f156b5e557ebdcb964d823d86f4c108b0c8"},
+    {file = "flake8-black-0.2.0.tar.gz", hash = "sha256:10e7ff9f81f637a9471684e5624d6a32b11cba362b38df4e20fc8f761184440b"},
 ]
 flake8-builtins = [
-    {file = "flake8-builtins-1.5.2.tar.gz", hash = "sha256:fe7be13fe51bfb06bdae6096c6488e328c822c3aa080e24b91b77116a4fbb8b0"},
-    {file = "flake8_builtins-1.5.2-py2.py3-none-any.whl", hash = "sha256:a0296d23da92a6f2494243b9f2039bfdb73f34aba20054c1b70b2a60c84745bb"},
+    {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-0.0.4.tar.gz", hash = "sha256:7329ec2e2b8b194e8109639c534359014c79df4d50b14f4b85b8395edc5d6760"},
+    {file = "flake8-django-1.1.1.tar.gz", hash = "sha256:fb4e8f669d3cf44297bb6e1c5d0a358ab0aba373cd4c69268cf2798de6bcbd9b"},
+    {file = "flake8_django-1.1.1-py3-none-any.whl", hash = "sha256:c71da0e61b6119dae91cbffdbdb00f1d6ebe3f5d0c43f5bf136929997ab0b72d"},
 ]
 flake8-docstrings = [
     {file = "flake8-docstrings-1.5.0.tar.gz", hash = "sha256:3d5a31c7ec6b7367ea6506a87ec293b94a0a46c0bce2bb4975b7f1d09b6f3717"},
@@ -2427,8 +2523,8 @@ flake8-fixme = [
     {file = "flake8_fixme-1.1.1-py2.py3-none-any.whl", hash = "sha256:226a6f2ef916730899f29ac140bed5d4a17e5aba79f00a0e3ae1eff1997cb1ac"},
 ]
 flake8-isort = [
-    {file = "flake8-isort-2.9.1.tar.gz", hash = "sha256:0d34b266080e1748412b203a1690792245011706b1858c203476b43460bf3652"},
-    {file = "flake8_isort-2.9.1-py2.py3-none-any.whl", hash = "sha256:a77df28778a1ac6ac4153339ebd9d252935f3ed4379872d4f8b84986296d8cc3"},
+    {file = "flake8-isort-3.0.0.tar.gz", hash = "sha256:3ce227b5c5342b6d63937d3863e8de8783ae21863cb035cf992cdb0ba5990aa3"},
+    {file = "flake8_isort-3.0.0-py2.py3-none-any.whl", hash = "sha256:f5322a85cea89998e0df954162fd35a1f1e5b5eb4fc0c79b5975aa2799106baa"},
 ]
 flake8-mypy = [
     {file = "flake8-mypy-17.8.0.tar.gz", hash = "sha256:47120db63aff631ee1f84bac6fe8e64731dc66da3efc1c51f85e15ade4a3ba18"},
@@ -2441,13 +2537,16 @@ flake8-polyfill = [
 flake8-rst-docstrings = [
     {file = "flake8-rst-docstrings-0.0.13.tar.gz", hash = "sha256:b1b619d81d879b874533973ac04ee5d823fdbe8c9f3701bfe802bb41813997b4"},
 ]
+future = [
+    {file = "future-0.18.2.tar.gz", hash = "sha256:b1bead90b70cf6ec3f0710ae53a525360fa360d306a86583adc6bf83a4db537d"},
+]
 gitdb = [
-    {file = "gitdb-4.0.4-py3-none-any.whl", hash = "sha256:ba1132c0912e8c917aa8aa990bee26315064c7b7f171ceaaac0afeb1dc656c6a"},
-    {file = "gitdb-4.0.4.tar.gz", hash = "sha256:6f0ecd46f99bb4874e5678d628c3a198e2b4ef38daea2756a2bfd8df7dd5c1a5"},
+    {file = "gitdb-4.0.5-py3-none-any.whl", hash = "sha256:91f36bfb1ab7949b3b40e23736db18231bf7593edada2ba5c3a174a7b23657ac"},
+    {file = "gitdb-4.0.5.tar.gz", hash = "sha256:c9e1f2d0db7ddb9a704c2a0217be31214e91a4fe1dea1efad19ae42ba0c285c9"},
 ]
 gitpython = [
-    {file = "GitPython-3.1.1-py3-none-any.whl", hash = "sha256:71b8dad7409efbdae4930f2b0b646aaeccce292484ffa0bc74f1195582578b3d"},
-    {file = "GitPython-3.1.1.tar.gz", hash = "sha256:6d4f10e2aaad1864bb0f17ec06a2c2831534140e5883c350d58b4e85189dab74"},
+    {file = "GitPython-3.1.3-py3-none-any.whl", hash = "sha256:ef1d60b01b5ce0040ad3ec20bc64f783362d41fa0822a2742d3586e1f49bb8ac"},
+    {file = "GitPython-3.1.3.tar.gz", hash = "sha256:e107af4d873daed64648b4f4beb89f89f0cfbe3ef558fc7821ed2331c2f8da1a"},
 ]
 html2text = [
     {file = "html2text-2020.1.16-py3-none-any.whl", hash = "sha256:c7c629882da0cf377d66f073329ccf34a12ed2adf0169b9285ae4e63ef54c82b"},
@@ -2462,8 +2561,8 @@ imagesize = [
     {file = "imagesize-1.2.0.tar.gz", hash = "sha256:b1f6b5a4eab1f73479a50fb79fcf729514a900c341d8503d62a62dbc4127a2b1"},
 ]
 importlib-metadata = [
-    {file = "importlib_metadata-1.6.0-py2.py3-none-any.whl", hash = "sha256:2a688cbaa90e0cc587f1df48bdc97a6eadccdcd9c35fb3f976a09e3b5016d90f"},
-    {file = "importlib_metadata-1.6.0.tar.gz", hash = "sha256:34513a8a0c4962bc66d35b359558fd8a5e10cd472d37aec5f66858addef32c1e"},
+    {file = "importlib_metadata-1.7.0-py2.py3-none-any.whl", hash = "sha256:dc15b2969b4ce36305c51eebe62d418ac7791e9a157911d58bfb1f9ccd8e2070"},
+    {file = "importlib_metadata-1.7.0.tar.gz", hash = "sha256:90bb658cdbbf6d1735b6341ce708fc7024a3e14e99ffdc5783edea9f9b077f83"},
 ]
 isort = [
     {file = "isort-4.3.21-py2.py3-none-any.whl", hash = "sha256:6e811fcb295968434526407adb8796944f1988c5b65e8139058f2014cbe100fd"},
@@ -2474,26 +2573,23 @@ jinja2 = [
     {file = "Jinja2-2.11.2.tar.gz", hash = "sha256:89aab215427ef59c34ad58735269eb58b1a5808103067f7bb9d5836c651b3bb0"},
 ]
 kombu = [
-    {file = "kombu-4.6.8-py2.py3-none-any.whl", hash = "sha256:598e7e749d6ab54f646b74b2d2df67755dee13894f73ab02a2a9feb8870c7cb2"},
-    {file = "kombu-4.6.8.tar.gz", hash = "sha256:2d1cda774126a044d91a7ff5fa6d09edf99f46924ab332a810760fe6740e9b76"},
+    {file = "kombu-4.6.11-py2.py3-none-any.whl", hash = "sha256:be48cdffb54a2194d93ad6533d73f69408486483d189fe9f5990ee24255b0e0a"},
+    {file = "kombu-4.6.11.tar.gz", hash = "sha256:ca1b45faac8c0b18493d02a8571792f3c40291cf2bcf1f55afed3d8f3aa7ba74"},
 ]
 libsass = [
-    {file = "libsass-0.19.4-cp27-cp27m-macosx_10_14_intel.whl", hash = "sha256:74acd9adf506142699dfa292f0e569fdccbd9e7cf619e8226f7117de73566e32"},
-    {file = "libsass-0.19.4-cp27-cp27m-win32.whl", hash = "sha256:50778d4be269a021ba2bf42b5b8f6ff3704ab96a82175a052680bddf3ba7cc9f"},
-    {file = "libsass-0.19.4-cp27-cp27m-win_amd64.whl", hash = "sha256:4dcfd561fb100250b89496e1362b96f2cc804f689a59731eb0f94f9a9e144f4a"},
-    {file = "libsass-0.19.4-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:845a9573b25c141164972d498855f4ad29367c09e6d76fad12955ad0e1c83013"},
-    {file = "libsass-0.19.4-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:81a013a4c2a614927fd1ef7a386eddabbba695cbb02defe8f31cf495106e974c"},
-    {file = "libsass-0.19.4-cp35-cp35m-win32.whl", hash = "sha256:fcb7ab4dc81889e5fc99cafbc2017bc76996f9992fc6b175f7a80edac61d71df"},
-    {file = "libsass-0.19.4-cp35-cp35m-win_amd64.whl", hash = "sha256:fc5f8336750f76f1bfae82f7e9e89ae71438d26fc4597e3ab4c05ca8fcd41d8a"},
-    {file = "libsass-0.19.4-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:9b59afa0d755089c4165516400a39a289b796b5612eeef5736ab7a1ebf96a67c"},
-    {file = "libsass-0.19.4-cp36-cp36m-win32.whl", hash = "sha256:c93df526eeef90b1ea4799c1d33b6cd5aea3e9f4633738fb95c1287c13e6b404"},
-    {file = "libsass-0.19.4-cp36-cp36m-win_amd64.whl", hash = "sha256:0fd8b4337b3b101c6e6afda9112cc0dc4bacb9133b59d75d65968c7317aa3272"},
-    {file = "libsass-0.19.4-cp37-cp37m-macosx_10_14_x86_64.whl", hash = "sha256:003a65b4facb4c5dbace53fb0f70f61c5aae056a04b4d112a198c3c9674b31f2"},
-    {file = "libsass-0.19.4-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:338e9ae066bf1fde874e335324d5355c52d2081d978b4f74fc59536564b35b08"},
-    {file = "libsass-0.19.4-cp37-cp37m-win32.whl", hash = "sha256:e318f06f06847ff49b1f8d086ac9ebce1e63404f7ea329adab92f4f16ba0e00e"},
-    {file = "libsass-0.19.4-cp37-cp37m-win_amd64.whl", hash = "sha256:a7e685466448c9b1bf98243339793978f654a1151eb5c975f09b83c7a226f4c1"},
-    {file = "libsass-0.19.4-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:6a51393d75f6e3c812785b0fa0b7d67c54258c28011921f204643b55f7355ec0"},
-    {file = "libsass-0.19.4.tar.gz", hash = "sha256:8b5b6d1a7c4ea1d954e0982b04474cc076286493f6af2d0a13c2e950fbe0be95"},
+    {file = "libsass-0.20.0-cp27-cp27m-macosx_10_14_intel.whl", hash = "sha256:107c409524c6a4ed14410fa9dafa9ee59c6bd3ecae75d73af749ab2b75685726"},
+    {file = "libsass-0.20.0-cp27-cp27m-win32.whl", hash = "sha256:98f6dee9850b29e62977a963e3beb3cfeb98b128a267d59d2c3d675e298c8d57"},
+    {file = "libsass-0.20.0-cp27-cp27m-win_amd64.whl", hash = "sha256:b077261a04ba1c213e932943208471972c5230222acb7fa97373e55a40872cbb"},
+    {file = "libsass-0.20.0-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:e6a547c0aa731dcb4ed71f198e814bee0400ce04d553f3f12a53bc3a17f2a481"},
+    {file = "libsass-0.20.0-cp36-abi3-manylinux1_x86_64.whl", hash = "sha256:74f6fb8da58179b5d86586bc045c16d93d55074bc7bb48b6354a4da7ac9f9dfd"},
+    {file = "libsass-0.20.0-cp36-cp36m-win32.whl", hash = "sha256:a43f3830d83ad9a7f5013c05ce239ca71744d0780dad906587302ac5257bce60"},
+    {file = "libsass-0.20.0-cp36-cp36m-win_amd64.whl", hash = "sha256:fd19c8f73f70ffc6cbcca8139da08ea9a71fc48e7dfc4bb236ad88ab2d6558f1"},
+    {file = "libsass-0.20.0-cp37-abi3-macosx_10_14_x86_64.whl", hash = "sha256:8cf72552b39e78a1852132e16b706406bc76029fe3001583284ece8d8752a60a"},
+    {file = "libsass-0.20.0-cp37-cp37m-win32.whl", hash = "sha256:7555d9b24e79943cfafac44dbb4ca7e62105c038de7c6b999838c9ff7b88645d"},
+    {file = "libsass-0.20.0-cp37-cp37m-win_amd64.whl", hash = "sha256:794f4f4661667263e7feafe5cc866e3746c7c8a9192b2aa9afffdadcbc91c687"},
+    {file = "libsass-0.20.0-cp38-cp38-win32.whl", hash = "sha256:3bc0d68778b30b5fa83199e18795314f64b26ca5871e026343e63934f616f7f7"},
+    {file = "libsass-0.20.0-cp38-cp38-win_amd64.whl", hash = "sha256:5c8ff562b233734fbc72b23bb862cc6a6f70b1e9bf85a58422aa75108b94783b"},
+    {file = "libsass-0.20.0.tar.gz", hash = "sha256:b7452f1df274b166dc22ee2e9154c4adca619bcbbdf8041a7aa05f372a1dacbc"},
 ]
 license-expression = [
     {file = "license-expression-1.2.tar.gz", hash = "sha256:7960e1dfdf20d127e75ead931476f2b5c7556df05b117a73880b22ade17d1abc"},
@@ -2539,8 +2635,8 @@ mccabe = [
     {file = "mccabe-0.6.1.tar.gz", hash = "sha256:dd8d182285a0fe56bace7f45b5e7d1a6ebcbf524e8f3bd87eb0f125271b8831f"},
 ]
 more-itertools = [
-    {file = "more-itertools-8.2.0.tar.gz", hash = "sha256:b1ddb932186d8a6ac451e1d95844b382f55e12686d51ca0c68b6f61f2ab7a507"},
-    {file = "more_itertools-8.2.0-py3-none-any.whl", hash = "sha256:5dd8bcf33e5f9513ffa06d5ad33d78f31e1931ac9a18f33d37e77a180d393a7c"},
+    {file = "more-itertools-8.4.0.tar.gz", hash = "sha256:68c70cc7167bdf5c7c9d8f6954a7837089c6a36bf565383919bb595efb8a17e5"},
+    {file = "more_itertools-8.4.0-py3-none-any.whl", hash = "sha256:b78134b2063dd214000685165d81c154522c3ee0a1c0d4d113c80361c234c5a2"},
 ]
 mypy = [
     {file = "mypy-0.770-cp35-cp35m-macosx_10_6_x86_64.whl", hash = "sha256:a34b577cdf6313bf24755f7a0e3f3c326d5c1f4fe7422d1d06498eb25ad0c600"},
@@ -2563,8 +2659,8 @@ mypy-extensions = [
     {file = "mypy_extensions-0.4.3.tar.gz", hash = "sha256:2d82818f5bb3e369420cb3c4060a7970edba416647068eb4c5343488a6c604a8"},
 ]
 packaging = [
-    {file = "packaging-20.3-py2.py3-none-any.whl", hash = "sha256:82f77b9bee21c1bafbf35a84905d604d5d1223801d639cf3ed140bd651c08752"},
-    {file = "packaging-20.3.tar.gz", hash = "sha256:3c292b474fda1671ec57d46d739d072bfd495a4f51ad01a055121d81e952b7a3"},
+    {file = "packaging-20.4-py2.py3-none-any.whl", hash = "sha256:998416ba6962ae7fbd6596850b80e17859a5753ba17c32284f67bfff33784181"},
+    {file = "packaging-20.4.tar.gz", hash = "sha256:4357f74f47b9c12db93624a82154e9b120fa8293699949152b22065d556079f8"},
 ]
 pathspec = [
     {file = "pathspec-0.8.0-py2.py3-none-any.whl", hash = "sha256:7d91249d21749788d07a2d0f94147accd8f845507400749ea19c1ec9054a12b0"},
@@ -2574,48 +2670,59 @@ pbr = [
     {file = "pbr-5.4.5-py2.py3-none-any.whl", hash = "sha256:579170e23f8e0c2f24b0de612f71f648eccb79fb1322c814ae6b3c07b5ba23e8"},
     {file = "pbr-5.4.5.tar.gz", hash = "sha256:07f558fece33b05caf857474a366dfcc00562bca13dd8b47b2b3e22d9f9bf55c"},
 ]
+persisting-theory = [
+    {file = "persisting-theory-0.2.1.tar.gz", hash = "sha256:00ff7dcc8f481ff75c770ca5797d968e8725b6df1f77fe0cf7d20fa1e5790c0a"},
+]
 pg8000 = [
-    {file = "pg8000-1.15.2-py3-none-any.whl", hash = "sha256:2bfdd03c2623302af655ef089a958ff329b2035c9d9aea406b5e4dac9c007524"},
-    {file = "pg8000-1.15.2.tar.gz", hash = "sha256:eb42ba62fbc048c91d5cf1ac729e0ea4ee329cc526bddafed4e7a8aa6b57fbbb"},
+    {file = "pg8000-1.15.3-py3-none-any.whl", hash = "sha256:79d2e761343e582dec6698cf7c06d49c33255cbafba29ff298dd4690fffb7d80"},
+    {file = "pg8000-1.15.3.tar.gz", hash = "sha256:af97353076b8e5d271d91c64c8ca806e2157d11b7862c90ff6f0e23be0fc217d"},
 ]
 phonenumbers = [
-    {file = "phonenumbers-8.12.1-py2.py3-none-any.whl", hash = "sha256:bebf881ef0e775b93062fbd107bf164b5baef877a7b8f702e93a9a5d24ae4065"},
-    {file = "phonenumbers-8.12.1.tar.gz", hash = "sha256:59ae9cb25fb03027c9f2bf5584098e699be7eca12c443838b83752956be15cda"},
+    {file = "phonenumbers-8.12.6-py2.py3-none-any.whl", hash = "sha256:e49b8e21c557f0dafee966ddd55fb2bd3d6db155451999b75fb1b012e8d2016c"},
+    {file = "phonenumbers-8.12.6.tar.gz", hash = "sha256:d332078fe71c6153b5a263ac87283618b2afe514a248a14f06a0d39ce1f5ce0b"},
 ]
 pillow = [
-    {file = "Pillow-7.1.1-cp35-cp35m-macosx_10_10_intel.whl", hash = "sha256:b7453750cf911785009423789d2e4e5393aae9cbb8b3f471dab854b85a26cb89"},
-    {file = "Pillow-7.1.1-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:4510c6b33277970b1af83c987277f9a08ec2b02cc20ac0f9234e4026136bb137"},
-    {file = "Pillow-7.1.1-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:b99b2607b6cd58396f363b448cbe71d3c35e28f03e442ab00806463439629c2c"},
-    {file = "Pillow-7.1.1-cp35-cp35m-win32.whl", hash = "sha256:cd47793f7bc9285a88c2b5551d3f16a2ddd005789614a34c5f4a598c2a162383"},
-    {file = "Pillow-7.1.1-cp35-cp35m-win_amd64.whl", hash = "sha256:04a10558320eba9137d6a78ca6fc8f4a5801f1b971152938851dc4629d903579"},
-    {file = "Pillow-7.1.1-cp36-cp36m-macosx_10_10_x86_64.whl", hash = "sha256:50a10b048f4dd81c092adad99fa5f7ba941edaf2f9590510109ac2a15e706695"},
-    {file = "Pillow-7.1.1-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:721c04d3c77c38086f1f95d1cd8df87f2f9a505a780acf8575912b3206479da1"},
-    {file = "Pillow-7.1.1-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:a5dc9f28c0239ec2742d4273bd85b2aa84655be2564db7ad1eb8f64b1efcdc4c"},
-    {file = "Pillow-7.1.1-cp36-cp36m-win32.whl", hash = "sha256:d6bf085f6f9ec6a1724c187083b37b58a8048f86036d42d21802ed5d1fae4853"},
-    {file = "Pillow-7.1.1-cp36-cp36m-win_amd64.whl", hash = "sha256:251e5618125ec12ac800265d7048f5857a8f8f1979db9ea3e11382e159d17f68"},
-    {file = "Pillow-7.1.1-cp37-cp37m-macosx_10_10_x86_64.whl", hash = "sha256:433bbc2469a2351bea53666d97bb1eb30f0d56461735be02ea6b27654569f80f"},
-    {file = "Pillow-7.1.1-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:eb84e7e5b07ff3725ab05977ac56d5eeb0c510795aeb48e8b691491be3c5745b"},
-    {file = "Pillow-7.1.1-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:3713386d1e9e79cea1c5e6aaac042841d7eef838cc577a3ca153c8bedf570287"},
-    {file = "Pillow-7.1.1-cp37-cp37m-win32.whl", hash = "sha256:291bad7097b06d648222b769bbfcd61e40d0abdfe10df686d20ede36eb8162b6"},
-    {file = "Pillow-7.1.1-cp37-cp37m-win_amd64.whl", hash = "sha256:6c1924ed7dbc6ad0636907693bbbdd3fdae1d73072963e71f5644b864bb10b4d"},
-    {file = "Pillow-7.1.1-cp38-cp38-macosx_10_10_x86_64.whl", hash = "sha256:670e58d3643971f4afd79191abd21623761c2ebe61db1c2cb4797d817c4ba1a7"},
-    {file = "Pillow-7.1.1-cp38-cp38-manylinux1_i686.whl", hash = "sha256:8d5799243050c2833c2662b824dfb16aa98e408d2092805edea4300a408490e7"},
-    {file = "Pillow-7.1.1-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:da737ab273f4d60ae552f82ad83f7cbd0e173ca30ca20b160f708c92742ee212"},
-    {file = "Pillow-7.1.1-cp38-cp38-win32.whl", hash = "sha256:b2f3e8cc52ecd259b94ca880fea0d15f4ebc6da2cd3db515389bb878d800270f"},
-    {file = "Pillow-7.1.1-cp38-cp38-win_amd64.whl", hash = "sha256:2f0b52a08d175f10c8ea36685115681a484c55d24d0933f9fd911e4111c04144"},
-    {file = "Pillow-7.1.1-pp373-pypy36_pp73-win32.whl", hash = "sha256:90cd441a1638ae176eab4d8b6b94ab4ec24b212ed4c3fbee2a6e74672481d4f8"},
-    {file = "Pillow-7.1.1-py3.8-macosx-10.9-x86_64.egg", hash = "sha256:5eef904c82b5f8e4256e8d420c971357da2884c0b812ba4efa15a7ad2ec66247"},
-    {file = "Pillow-7.1.1.tar.gz", hash = "sha256:0f89ddc77cf421b8cd34ae852309501458942bf370831b4a9b406156b599a14e"},
-]
-pipenv = [
-    {file = "pipenv-2018.11.26-py2-none-any.whl", hash = "sha256:7df8e33a2387de6f537836f48ac6fcd94eda6ed9ba3d5e3fd52e35b5bc7ff49e"},
-    {file = "pipenv-2018.11.26-py3-none-any.whl", hash = "sha256:56ad5f5cb48f1e58878e14525a6e3129d4306049cb76d2f6a3e95df0d5fc6330"},
-    {file = "pipenv-2018.11.26.tar.gz", hash = "sha256:a673e606e8452185e9817a987572b55360f4d28b50831ef3b42ac3cab3fee846"},
+    {file = "Pillow-7.1.2-cp35-cp35m-macosx_10_10_intel.whl", hash = "sha256:ae2b270f9a0b8822b98655cb3a59cdb1bd54a34807c6c56b76dd2e786c3b7db3"},
+    {file = "Pillow-7.1.2-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:d23e2aa9b969cf9c26edfb4b56307792b8b374202810bd949effd1c6e11ebd6d"},
+    {file = "Pillow-7.1.2-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:b532bcc2f008e96fd9241177ec580829dee817b090532f43e54074ecffdcd97f"},
+    {file = "Pillow-7.1.2-cp35-cp35m-win32.whl", hash = "sha256:12e4bad6bddd8546a2f9771485c7e3d2b546b458ae8ff79621214119ac244523"},
+    {file = "Pillow-7.1.2-cp35-cp35m-win_amd64.whl", hash = "sha256:9744350687459234867cbebfe9df8f35ef9e1538f3e729adbd8fde0761adb705"},
+    {file = "Pillow-7.1.2-cp36-cp36m-macosx_10_10_x86_64.whl", hash = "sha256:f54be399340aa602066adb63a86a6a5d4f395adfdd9da2b9a0162ea808c7b276"},
+    {file = "Pillow-7.1.2-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:1f694e28c169655c50bb89a3fa07f3b854d71eb47f50783621de813979ba87f3"},
+    {file = "Pillow-7.1.2-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:f784aad988f12c80aacfa5b381ec21fd3f38f851720f652b9f33facc5101cf4d"},
+    {file = "Pillow-7.1.2-cp36-cp36m-win32.whl", hash = "sha256:b37bb3bd35edf53125b0ff257822afa6962649995cbdfde2791ddb62b239f891"},
+    {file = "Pillow-7.1.2-cp36-cp36m-win_amd64.whl", hash = "sha256:b67a6c47ed963c709ed24566daa3f95a18f07d3831334da570c71da53d97d088"},
+    {file = "Pillow-7.1.2-cp37-cp37m-macosx_10_10_x86_64.whl", hash = "sha256:eaa83729eab9c60884f362ada982d3a06beaa6cc8b084cf9f76cae7739481dfa"},
+    {file = "Pillow-7.1.2-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:f46e0e024346e1474083c729d50de909974237c72daca05393ee32389dabe457"},
+    {file = "Pillow-7.1.2-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:0e2a3bceb0fd4e0cb17192ae506d5f082b309ffe5fc370a5667959c9b2f85fa3"},
+    {file = "Pillow-7.1.2-cp37-cp37m-win32.whl", hash = "sha256:ccc9ad2460eb5bee5642eaf75a0438d7f8887d484490d5117b98edd7f33118b7"},
+    {file = "Pillow-7.1.2-cp37-cp37m-win_amd64.whl", hash = "sha256:b943e71c2065ade6fef223358e56c167fc6ce31c50bc7a02dd5c17ee4338e8ac"},
+    {file = "Pillow-7.1.2-cp38-cp38-macosx_10_10_x86_64.whl", hash = "sha256:04766c4930c174b46fd72d450674612ab44cca977ebbcc2dde722c6933290107"},
+    {file = "Pillow-7.1.2-cp38-cp38-manylinux1_i686.whl", hash = "sha256:f455efb7a98557412dc6f8e463c1faf1f1911ec2432059fa3e582b6000fc90e2"},
+    {file = "Pillow-7.1.2-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:ee94fce8d003ac9fd206496f2707efe9eadcb278d94c271f129ab36aa7181344"},
+    {file = "Pillow-7.1.2-cp38-cp38-win32.whl", hash = "sha256:4b02b9c27fad2054932e89f39703646d0c543f21d3cc5b8e05434215121c28cd"},
+    {file = "Pillow-7.1.2-cp38-cp38-win_amd64.whl", hash = "sha256:3d25dd8d688f7318dca6d8cd4f962a360ee40346c15893ae3b95c061cdbc4079"},
+    {file = "Pillow-7.1.2-pp373-pypy36_pp73-win32.whl", hash = "sha256:0f01e63c34f0e1e2580cc0b24e86a5ccbbfa8830909a52ee17624c4193224cd9"},
+    {file = "Pillow-7.1.2-py3.8-macosx-10.9-x86_64.egg", hash = "sha256:70e3e0d99a0dcda66283a185f80697a9b08806963c6149c8e6c5f452b2aa59c0"},
+    {file = "Pillow-7.1.2.tar.gz", hash = "sha256:a0b49960110bc6ff5fead46013bcb8825d101026d466f3a4de3476defe0fb0dd"},
 ]
 pluggy = [
     {file = "pluggy-0.13.1-py2.py3-none-any.whl", hash = "sha256:966c145cd83c96502c3c3868f50408687b38434af77734af1e9ca461a4081d2d"},
     {file = "pluggy-0.13.1.tar.gz", hash = "sha256:15b2acde666561e1298d71b523007ed7364de07029219b604cf808bfa1c765b0"},
 ]
+psutil = [
+    {file = "psutil-5.7.0-cp27-none-win32.whl", hash = "sha256:298af2f14b635c3c7118fd9183843f4e73e681bb6f01e12284d4d70d48a60953"},
+    {file = "psutil-5.7.0-cp27-none-win_amd64.whl", hash = "sha256:75e22717d4dbc7ca529ec5063000b2b294fc9a367f9c9ede1f65846c7955fd38"},
+    {file = "psutil-5.7.0-cp35-cp35m-win32.whl", hash = "sha256:f344ca230dd8e8d5eee16827596f1c22ec0876127c28e800d7ae20ed44c4b310"},
+    {file = "psutil-5.7.0-cp35-cp35m-win_amd64.whl", hash = "sha256:e2d0c5b07c6fe5a87fa27b7855017edb0d52ee73b71e6ee368fae268605cc3f5"},
+    {file = "psutil-5.7.0-cp36-cp36m-win32.whl", hash = "sha256:a02f4ac50d4a23253b68233b07e7cdb567bd025b982d5cf0ee78296990c22d9e"},
+    {file = "psutil-5.7.0-cp36-cp36m-win_amd64.whl", hash = "sha256:1413f4158eb50e110777c4f15d7c759521703bd6beb58926f1d562da40180058"},
+    {file = "psutil-5.7.0-cp37-cp37m-win32.whl", hash = "sha256:d008ddc00c6906ec80040d26dc2d3e3962109e40ad07fd8a12d0284ce5e0e4f8"},
+    {file = "psutil-5.7.0-cp37-cp37m-win_amd64.whl", hash = "sha256:73f35ab66c6c7a9ce82ba44b1e9b1050be2a80cd4dcc3352cc108656b115c74f"},
+    {file = "psutil-5.7.0-cp38-cp38-win32.whl", hash = "sha256:60b86f327c198561f101a92be1995f9ae0399736b6eced8f24af41ec64fb88d4"},
+    {file = "psutil-5.7.0-cp38-cp38-win_amd64.whl", hash = "sha256:d84029b190c8a66a946e28b4d3934d2ca1528ec94764b180f7d6ea57b0e75e26"},
+    {file = "psutil-5.7.0.tar.gz", hash = "sha256:685ec16ca14d079455892f25bd124df26ff9137664af445563c1bd36629b5e0e"},
+]
 psycopg2 = [
     {file = "psycopg2-2.8.5-cp27-cp27m-win32.whl", hash = "sha256:a0984ff49e176062fcdc8a5a2a670c9bb1704a2f69548bce8f8a7bad41c661bf"},
     {file = "psycopg2-2.8.5-cp27-cp27m-win_amd64.whl", hash = "sha256:acf56d564e443e3dea152efe972b1434058244298a94348fc518d6dd6a9fb0bb"},
@@ -2632,8 +2739,8 @@ psycopg2 = [
     {file = "psycopg2-2.8.5.tar.gz", hash = "sha256:f7d46240f7a1ae1dd95aab38bd74f7428d46531f69219954266d669da60c0818"},
 ]
 py = [
-    {file = "py-1.8.1-py2.py3-none-any.whl", hash = "sha256:c20fdd83a5dbc0af9efd622bee9a5564e278f6380fffcacc43ba6f43db2813b0"},
-    {file = "py-1.8.1.tar.gz", hash = "sha256:5e27081401262157467ad6e7f851b7aa402c5852dbcb3dae06768434de5752aa"},
+    {file = "py-1.9.0-py2.py3-none-any.whl", hash = "sha256:366389d1db726cd2fcfc79732e75410e5fe4d31db13692115529d34069a043c2"},
+    {file = "py-1.9.0.tar.gz", hash = "sha256:9ca6883ce56b4e8da7e79ac18787889fa5206c79dcc67fb065376cd2fe03f342"},
 ]
 pyasn1 = [
     {file = "pyasn1-0.4.8-py2.4.egg", hash = "sha256:fec3e9d8e36808a28efb59b489e4528c10ad0f480e57dcc32b4de5c9d8c9fdf3"},
@@ -2666,32 +2773,68 @@ pyasn1-modules = [
     {file = "pyasn1_modules-0.2.8-py3.7.egg", hash = "sha256:c29a5e5cc7a3f05926aff34e097e84f8589cd790ce0ed41b67aed6857b26aafd"},
 ]
 pycodestyle = [
-    {file = "pycodestyle-2.5.0-py2.py3-none-any.whl", hash = "sha256:95a2219d12372f05704562a14ec30bc76b05a5b297b21a5dfe3f6fac3491ae56"},
-    {file = "pycodestyle-2.5.0.tar.gz", hash = "sha256:e40a936c9a450ad81df37f549d676d127b1b66000a6c500caa2b085bc0ca976c"},
+    {file = "pycodestyle-2.6.0-py2.py3-none-any.whl", hash = "sha256:2295e7b2f6b5bd100585ebcb1f616591b652db8a741695b3d8f5d28bdc934367"},
+    {file = "pycodestyle-2.6.0.tar.gz", hash = "sha256:c58a7d2815e0e8d7972bf1803331fb0152f867bd89adf8a01dfd55085434192e"},
+]
+pycryptodome = [
+    {file = "pycryptodome-3.9.8-cp27-cp27m-macosx_10_6_intel.whl", hash = "sha256:50348edd283afdccddc0938cdc674484533912ba8a99a27c7bfebb75030aa856"},
+    {file = "pycryptodome-3.9.8-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:80d57177a0b7c14d4594c62bbb47fe2f6309ad3b0a34348a291d570925c97a82"},
+    {file = "pycryptodome-3.9.8-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:fbe65d5cfe04ff2f7684160d50f5118bdefb01e3af4718eeb618bfed40f19d94"},
+    {file = "pycryptodome-3.9.8-cp27-cp27m-win32.whl", hash = "sha256:bcd5b8416e73e4b0d48afba3704d8c826414764dafaed7a1a93c442188d90ccc"},
+    {file = "pycryptodome-3.9.8-cp27-cp27m-win_amd64.whl", hash = "sha256:360955eece2cd0fa694a708d10303c6abd7b39614fa2547b6bd245da76198beb"},
+    {file = "pycryptodome-3.9.8-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:1e655746f539421d923fd48df8f6f40b3443d80b75532501c0085b64afed9df5"},
+    {file = "pycryptodome-3.9.8-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:709b9f144d23e290b9863121d1ace14a72e01f66ea9c903fbdc690520dfdfcf0"},
+    {file = "pycryptodome-3.9.8-cp35-cp35m-macosx_10_6_intel.whl", hash = "sha256:6276478ada411aca97c0d5104916354b3d740d368407912722bd4d11aa9ee4c2"},
+    {file = "pycryptodome-3.9.8-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:54bdedd28476dea8a3cd86cb67c0df1f0e3d71cae8022354b0f879c41a3d27b2"},
+    {file = "pycryptodome-3.9.8-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:f521178e5a991ffd04182ed08f552daca1affcb826aeda0e1945cd989a9d4345"},
+    {file = "pycryptodome-3.9.8-cp35-cp35m-win32.whl", hash = "sha256:a207231a52426de3ff20f5608f0687261a3329d97a036c51f7d4c606a6f30c23"},
+    {file = "pycryptodome-3.9.8-cp35-cp35m-win_amd64.whl", hash = "sha256:2b998dc45ef5f4e5cf5248a6edfcd8d8e9fb5e35df8e4259b13a1b10eda7b16b"},
+    {file = "pycryptodome-3.9.8-cp36-cp36m-macosx_10_6_intel.whl", hash = "sha256:03d5cca8618620f45fd40f827423f82b86b3a202c8d44108601b0f5f56b04299"},
+    {file = "pycryptodome-3.9.8-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:f78a68c2c820e4731e510a2df3eef0322f24fde1781ced970bf497b6c7d92982"},
+    {file = "pycryptodome-3.9.8-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:132a56abba24e2e06a479d8e5db7a48271a73a215f605017bbd476d31f8e71c1"},
+    {file = "pycryptodome-3.9.8-cp36-cp36m-win32.whl", hash = "sha256:67dcad1b8b201308586a8ca2ffe89df1e4f731d5a4cdd0610cc4ea790351c739"},
+    {file = "pycryptodome-3.9.8-cp36-cp36m-win_amd64.whl", hash = "sha256:b56638d58a3a4be13229c6a815cd448f9e3ce40c00880a5398471b42ee86f50e"},
+    {file = "pycryptodome-3.9.8-cp37-cp37m-macosx_10_6_intel.whl", hash = "sha256:bec2bcdf7c9ce7f04d718e51887f3b05dc5c1cfaf5d2c2e9065ecddd1b2f6c9a"},
+    {file = "pycryptodome-3.9.8-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:abc2e126c9490e58a36a0f83516479e781d83adfb134576a5cbe5c6af2a3e93c"},
+    {file = "pycryptodome-3.9.8-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:ef39c98d9b8c0736d91937d193653e47c3b19ddf4fc3bccdc5e09aaa4b0c5d21"},
+    {file = "pycryptodome-3.9.8-cp37-cp37m-win32.whl", hash = "sha256:4350a42028240c344ee855f032c7d4ad6ff4f813bfbe7121547b7dc579ecc876"},
+    {file = "pycryptodome-3.9.8-cp37-cp37m-win_amd64.whl", hash = "sha256:c8bf40cf6e281a4378e25846924327e728a887e8bf0ee83b2604a0f4b61692e8"},
+    {file = "pycryptodome-3.9.8-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:d8074c8448cfd0705dfa71ca333277fce9786d0b9cac75d120545de6253f996a"},
+    {file = "pycryptodome-3.9.8-cp38-cp38-manylinux1_i686.whl", hash = "sha256:8063a712fba642f78d3c506b0896846601b6de7f5c3d534e388ad0cc07f5a149"},
+    {file = "pycryptodome-3.9.8-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:dd302b6ae3965afeb5ef1b0d92486f986c0e65183cd7835973f0b593800590e6"},
+    {file = "pycryptodome-3.9.8-cp38-cp38-win32.whl", hash = "sha256:02e51e1d5828d58f154896ddfd003e2e7584869c275e5acbe290443575370fba"},
+    {file = "pycryptodome-3.9.8-cp38-cp38-win_amd64.whl", hash = "sha256:55eb61aca2c883db770999f50d091ff7c14016f2769ad7bca3d9b75d1d7c1b68"},
+    {file = "pycryptodome-3.9.8-cp39-cp39-manylinux1_i686.whl", hash = "sha256:39ef9fb52d6ec7728fce1f1693cb99d60ce302aeebd59bcedea70ca3203fda60"},
+    {file = "pycryptodome-3.9.8-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:de6e1cd75677423ff64712c337521e62e3a7a4fc84caabbd93207752e831a85a"},
+    {file = "pycryptodome-3.9.8.tar.gz", hash = "sha256:0e24171cf01021bc5dc17d6a9d4f33a048f09d62cc3f62541e95ef104588bda4"},
 ]
 pydocstyle = [
     {file = "pydocstyle-5.0.2-py3-none-any.whl", hash = "sha256:da7831660b7355307b32778c4a0dbfb137d89254ef31a2b2978f50fc0b4d7586"},
     {file = "pydocstyle-5.0.2.tar.gz", hash = "sha256:f4f5d210610c2d153fae39093d44224c17429e2ad7da12a8b419aba5c2f614b5"},
 ]
 pyflakes = [
-    {file = "pyflakes-2.1.1-py2.py3-none-any.whl", hash = "sha256:17dbeb2e3f4d772725c777fabc446d5634d1038f234e77343108ce445ea69ce0"},
-    {file = "pyflakes-2.1.1.tar.gz", hash = "sha256:d976835886f8c5b31d47970ed689944a0262b5f3afa00a5a7b4dc81e5449f8a2"},
+    {file = "pyflakes-2.2.0-py2.py3-none-any.whl", hash = "sha256:0d94e0e05a19e57a99444b6ddcf9a6eb2e5c68d3ca1e98e90707af8152c90a92"},
+    {file = "pyflakes-2.2.0.tar.gz", hash = "sha256:35b2d75ee967ea93b55750aa9edbbf72813e06a66ba54438df2cfac9e3c27fc8"},
 ]
 pygments = [
     {file = "Pygments-2.6.1-py3-none-any.whl", hash = "sha256:ff7a40b4860b727ab48fad6360eb351cc1b33cbf9b15a0f689ca5353e9463324"},
     {file = "Pygments-2.6.1.tar.gz", hash = "sha256:647344a061c249a3b74e230c739f434d7ea4d8b1d5f3721bc0f3558049b38f44"},
 ]
+pyjwt = [
+    {file = "PyJWT-1.7.1-py2.py3-none-any.whl", hash = "sha256:5c6eca3c2940464d106b99ba83b00c6add741c9becaec087fb7ccdefea71350e"},
+    {file = "PyJWT-1.7.1.tar.gz", hash = "sha256:8d59a976fb773f3e6a39c85636357c4f0e242707394cadadd9814f5cbaa20e96"},
+]
 pyparsing = [
     {file = "pyparsing-2.4.7-py2.py3-none-any.whl", hash = "sha256:ef9d7589ef3c200abe66653d3f1ab1033c3c419ae9b9bdb1240a85b024efc88b"},
     {file = "pyparsing-2.4.7.tar.gz", hash = "sha256:c203ec8783bf771a155b207279b9bccb8dea02d8f0c9e5f8ead507bc3246ecc1"},
 ]
 pytest = [
-    {file = "pytest-5.4.1-py3-none-any.whl", hash = "sha256:0e5b30f5cb04e887b91b1ee519fa3d89049595f428c1db76e73bd7f17b09b172"},
-    {file = "pytest-5.4.1.tar.gz", hash = "sha256:84dde37075b8805f3d1f392cc47e38a0e59518fb46a431cfdaf7cf1ce805f970"},
+    {file = "pytest-5.4.3-py3-none-any.whl", hash = "sha256:5c0db86b698e8f170ba4582a492248919255fcd4c79b1ee64ace34301fb589a1"},
+    {file = "pytest-5.4.3.tar.gz", hash = "sha256:7979331bfcba207414f5e1263b5a0f8f521d0f457318836a7355531ed1a4c7d8"},
 ]
 pytest-cov = [
-    {file = "pytest-cov-2.8.1.tar.gz", hash = "sha256:cc6742d8bac45070217169f5f72ceee1e0e55b0221f54bcf24845972d3a47f2b"},
-    {file = "pytest_cov-2.8.1-py2.py3-none-any.whl", hash = "sha256:cdbdef4f870408ebdbfeb44e63e07eb18bb4619fae852f6e760645fa36172626"},
+    {file = "pytest-cov-2.10.0.tar.gz", hash = "sha256:1a629dc9f48e53512fcbfda6b07de490c374b0c83c55ff7a1720b3fccff0ac87"},
+    {file = "pytest_cov-2.10.0-py2.py3-none-any.whl", hash = "sha256:6e6d18092dce6fad667cd7020deed816f858ad3b49d5b5e2b1cc1c97a4dba65c"},
 ]
 pytest-django = [
     {file = "pytest-django-3.9.0.tar.gz", hash = "sha256:664e5f42242e5e182519388f01b9f25d824a9feb7cd17d8f863c8d776f38baf9"},
@@ -2702,15 +2845,14 @@ pytest-django-testing-postgresql = [
     {file = "pytest_django_testing_postgresql-0.1.post0-py3-none-any.whl", hash = "sha256:78e52e3d1b0ef5f906d5d69247dd6ac7dfb10d840bd81abab92f3f8c30872cd3"},
 ]
 pytest-sugar = [
-    {file = "pytest-sugar-0.9.2.tar.gz", hash = "sha256:fcd87a74b2bce5386d244b49ad60549bfbc4602527797fac167da147983f58ab"},
-    {file = "pytest_sugar-0.9.2-py2.py3-none-any.whl", hash = "sha256:26cf8289fe10880cbbc130bd77398c4e6a8b936d8393b116a5c16121d95ab283"},
+    {file = "pytest-sugar-0.9.3.tar.gz", hash = "sha256:1630b5b7ea3624919b73fde37cffb87965c5087a4afab8a43074ff44e0d810c4"},
 ]
 python-box = [
     {file = "python-box-3.4.6.tar.gz", hash = "sha256:694a7555e3ff9fbbce734bbaef3aad92b8e4ed0659d3ed04d56b6a0a0eff26a9"},
     {file = "python_box-3.4.6-py2.py3-none-any.whl", hash = "sha256:a71d3dc9dbaa34c8597d3517c89a8041bd62fa875f23c0f3dad55e1958e3ce10"},
 ]
 python-crontab = [
-    {file = "python-crontab-2.4.1.tar.gz", hash = "sha256:2366c7aa373118315de7c082401907bacd28e8b1e4e0a6d702334d17b89e71aa"},
+    {file = "python-crontab-2.5.1.tar.gz", hash = "sha256:4bbe7e720753a132ca4ca9d4094915f40e9d9dc8a807a4564007651018ce8c31"},
 ]
 python-dateutil = [
     {file = "python-dateutil-2.8.1.tar.gz", hash = "sha256:73ebfe9dbf22e832286dafa60473e4cd239f8592f699aa5adaf10050e6e1823c"},
@@ -2721,15 +2863,15 @@ python-dotenv = [
     {file = "python_dotenv-0.13.0-py2.py3-none-any.whl", hash = "sha256:25c0ff1a3e12f4bde8d592cc254ab075cfe734fc5dd989036716fd17ee7e5ec7"},
 ]
 python-ldap = [
-    {file = "python-ldap-3.2.0.tar.gz", hash = "sha256:7d1c4b15375a533564aad3d3deade789221e450052b21ebb9720fb822eccdb8e"},
+    {file = "python-ldap-3.3.0.tar.gz", hash = "sha256:de04939485b53ee5d9a6855562d415b73060c52e681644386de4d5bd18e3f540"},
 ]
 python-memcached = [
     {file = "python-memcached-1.59.tar.gz", hash = "sha256:a2e28637be13ee0bf1a8b6843e7490f9456fd3f2a4cb60471733c7b5d5557e4f"},
     {file = "python_memcached-1.59-py2.py3-none-any.whl", hash = "sha256:4dac64916871bd3550263323fc2ce18e1e439080a2d5670c594cf3118d99b594"},
 ]
 pytz = [
-    {file = "pytz-2019.3-py2.py3-none-any.whl", hash = "sha256:1c557d7d0e871de1f5ccd5833f60fb2550652da6be2693c1e02300743d21500d"},
-    {file = "pytz-2019.3.tar.gz", hash = "sha256:b02c06db6cf09c12dd25137e563b31700d3b80fcc4ad23abb7a315f2789819be"},
+    {file = "pytz-2020.1-py2.py3-none-any.whl", hash = "sha256:a494d53b6d39c3c6e44c3bec237336e14305e4f29bbf800b599253057fbb79ed"},
+    {file = "pytz-2020.1.tar.gz", hash = "sha256:c35965d010ce31b23eeb663ed3cc8c906275d6be1a34393a1d73a41febf4a048"},
 ]
 pyyaml = [
     {file = "PyYAML-5.3.1-cp27-cp27m-win32.whl", hash = "sha256:74809a57b329d6cc0fdccee6318f44b9b8649961fa73144a98735b0aaf029f1f"},
@@ -2749,78 +2891,81 @@ qrcode = [
     {file = "qrcode-6.1.tar.gz", hash = "sha256:505253854f607f2abf4d16092c61d4e9d511a3b4392e60bff957a68592b04369"},
 ]
 redis = [
-    {file = "redis-3.4.1-py2.py3-none-any.whl", hash = "sha256:b205cffd05ebfd0a468db74f0eedbff8df1a7bfc47521516ade4692991bb0833"},
-    {file = "redis-3.4.1.tar.gz", hash = "sha256:0dcfb335921b88a850d461dc255ff4708294943322bd55de6cfd68972490ca1f"},
+    {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-2020.4.4-cp27-cp27m-win32.whl", hash = "sha256:90742c6ff121a9c5b261b9b215cb476eea97df98ea82037ec8ac95d1be7a034f"},
-    {file = "regex-2020.4.4-cp27-cp27m-win_amd64.whl", hash = "sha256:24f4f4062eb16c5bbfff6a22312e8eab92c2c99c51a02e39b4eae54ce8255cd1"},
-    {file = "regex-2020.4.4-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:08119f707f0ebf2da60d2f24c2f39ca616277bb67ef6c92b72cbf90cbe3a556b"},
-    {file = "regex-2020.4.4-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:c9423a150d3a4fc0f3f2aae897a59919acd293f4cb397429b120a5fcd96ea3db"},
-    {file = "regex-2020.4.4-cp36-cp36m-manylinux2010_i686.whl", hash = "sha256:c087bff162158536387c53647411db09b6ee3f9603c334c90943e97b1052a156"},
-    {file = "regex-2020.4.4-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:1cbe0fa0b7f673400eb29e9ef41d4f53638f65f9a2143854de6b1ce2899185c3"},
-    {file = "regex-2020.4.4-cp36-cp36m-win32.whl", hash = "sha256:0ce9537396d8f556bcfc317c65b6a0705320701e5ce511f05fc04421ba05b8a8"},
-    {file = "regex-2020.4.4-cp36-cp36m-win_amd64.whl", hash = "sha256:7e1037073b1b7053ee74c3c6c0ada80f3501ec29d5f46e42669378eae6d4405a"},
-    {file = "regex-2020.4.4-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:4385f12aa289d79419fede43f979e372f527892ac44a541b5446617e4406c468"},
-    {file = "regex-2020.4.4-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:a58dd45cb865be0ce1d5ecc4cfc85cd8c6867bea66733623e54bd95131f473b6"},
-    {file = "regex-2020.4.4-cp37-cp37m-manylinux2010_i686.whl", hash = "sha256:ccccdd84912875e34c5ad2d06e1989d890d43af6c2242c6fcfa51556997af6cd"},
-    {file = "regex-2020.4.4-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:ea4adf02d23b437684cd388d557bf76e3afa72f7fed5bbc013482cc00c816948"},
-    {file = "regex-2020.4.4-cp37-cp37m-win32.whl", hash = "sha256:2294f8b70e058a2553cd009df003a20802ef75b3c629506be20687df0908177e"},
-    {file = "regex-2020.4.4-cp37-cp37m-win_amd64.whl", hash = "sha256:e91ba11da11cf770f389e47c3f5c30473e6d85e06d7fd9dcba0017d2867aab4a"},
-    {file = "regex-2020.4.4-cp38-cp38-manylinux1_i686.whl", hash = "sha256:5635cd1ed0a12b4c42cce18a8d2fb53ff13ff537f09de5fd791e97de27b6400e"},
-    {file = "regex-2020.4.4-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:23069d9c07e115537f37270d1d5faea3e0bdded8279081c4d4d607a2ad393683"},
-    {file = "regex-2020.4.4-cp38-cp38-manylinux2010_i686.whl", hash = "sha256:c162a21e0da33eb3d31a3ac17a51db5e634fc347f650d271f0305d96601dc15b"},
-    {file = "regex-2020.4.4-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:fb95debbd1a824b2c4376932f2216cc186912e389bdb0e27147778cf6acb3f89"},
-    {file = "regex-2020.4.4-cp38-cp38-win32.whl", hash = "sha256:2a3bf8b48f8e37c3a40bb3f854bf0121c194e69a650b209628d951190b862de3"},
-    {file = "regex-2020.4.4-cp38-cp38-win_amd64.whl", hash = "sha256:5bfed051dbff32fd8945eccca70f5e22b55e4148d2a8a45141a3b053d6455ae3"},
-    {file = "regex-2020.4.4.tar.gz", hash = "sha256:295badf61a51add2d428a46b8580309c520d8b26e769868b922750cf3ce67142"},
+    {file = "regex-2020.6.8-cp27-cp27m-win32.whl", hash = "sha256:fbff901c54c22425a5b809b914a3bfaf4b9570eee0e5ce8186ac71eb2025191c"},
+    {file = "regex-2020.6.8-cp27-cp27m-win_amd64.whl", hash = "sha256:112e34adf95e45158c597feea65d06a8124898bdeac975c9087fe71b572bd938"},
+    {file = "regex-2020.6.8-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:92d8a043a4241a710c1cf7593f5577fbb832cf6c3a00ff3fc1ff2052aff5dd89"},
+    {file = "regex-2020.6.8-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:bae83f2a56ab30d5353b47f9b2a33e4aac4de9401fb582b55c42b132a8ac3868"},
+    {file = "regex-2020.6.8-cp36-cp36m-manylinux2010_i686.whl", hash = "sha256:b2ba0f78b3ef375114856cbdaa30559914d081c416b431f2437f83ce4f8b7f2f"},
+    {file = "regex-2020.6.8-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:95fa7726d073c87141f7bbfb04c284901f8328e2d430eeb71b8ffdd5742a5ded"},
+    {file = "regex-2020.6.8-cp36-cp36m-win32.whl", hash = "sha256:e3cdc9423808f7e1bb9c2e0bdb1c9dc37b0607b30d646ff6faf0d4e41ee8fee3"},
+    {file = "regex-2020.6.8-cp36-cp36m-win_amd64.whl", hash = "sha256:c78e66a922de1c95a208e4ec02e2e5cf0bb83a36ceececc10a72841e53fbf2bd"},
+    {file = "regex-2020.6.8-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:08997a37b221a3e27d68ffb601e45abfb0093d39ee770e4257bd2f5115e8cb0a"},
+    {file = "regex-2020.6.8-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:2f6f211633ee8d3f7706953e9d3edc7ce63a1d6aad0be5dcee1ece127eea13ae"},
+    {file = "regex-2020.6.8-cp37-cp37m-manylinux2010_i686.whl", hash = "sha256:55b4c25cbb3b29f8d5e63aeed27b49fa0f8476b0d4e1b3171d85db891938cc3a"},
+    {file = "regex-2020.6.8-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:89cda1a5d3e33ec9e231ece7307afc101b5217523d55ef4dc7fb2abd6de71ba3"},
+    {file = "regex-2020.6.8-cp37-cp37m-win32.whl", hash = "sha256:690f858d9a94d903cf5cada62ce069b5d93b313d7d05456dbcd99420856562d9"},
+    {file = "regex-2020.6.8-cp37-cp37m-win_amd64.whl", hash = "sha256:1700419d8a18c26ff396b3b06ace315b5f2a6e780dad387e4c48717a12a22c29"},
+    {file = "regex-2020.6.8-cp38-cp38-manylinux1_i686.whl", hash = "sha256:654cb773b2792e50151f0e22be0f2b6e1c3a04c5328ff1d9d59c0398d37ef610"},
+    {file = "regex-2020.6.8-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:52e1b4bef02f4040b2fd547357a170fc1146e60ab310cdbdd098db86e929b387"},
+    {file = "regex-2020.6.8-cp38-cp38-manylinux2010_i686.whl", hash = "sha256:cf59bbf282b627130f5ba68b7fa3abdb96372b24b66bdf72a4920e8153fc7910"},
+    {file = "regex-2020.6.8-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:5aaa5928b039ae440d775acea11d01e42ff26e1561c0ffcd3d805750973c6baf"},
+    {file = "regex-2020.6.8-cp38-cp38-win32.whl", hash = "sha256:97712e0d0af05febd8ab63d2ef0ab2d0cd9deddf4476f7aa153f76feef4b2754"},
+    {file = "regex-2020.6.8-cp38-cp38-win_amd64.whl", hash = "sha256:6ad8663c17db4c5ef438141f99e291c4d4edfeaacc0ce28b5bba2b0bf273d9b5"},
+    {file = "regex-2020.6.8.tar.gz", hash = "sha256:e9b64e609d37438f7d6e68c2546d2cb8062f3adb27e6336bc129b51be20773ac"},
 ]
 requests = [
-    {file = "requests-2.23.0-py2.py3-none-any.whl", hash = "sha256:43999036bfa82904b6af1d99e4882b560e5e2c68e5c4b0aa03b655f3d7d73fee"},
-    {file = "requests-2.23.0.tar.gz", hash = "sha256:b3f43d496c6daba4493e7c431722aeb7dbc6288f52a6e04e7b6023b0247817e6"},
+    {file = "requests-2.24.0-py2.py3-none-any.whl", hash = "sha256:fe75cc94a9443b9246fc7049224f75604b113c36acb93f87b80ed42c44cbb898"},
+    {file = "requests-2.24.0.tar.gz", hash = "sha256:b3559a131db72c33ee969480840fff4bb6dd111de7dd27c8ee1f820f4f00231b"},
 ]
 restructuredtext-lint = [
-    {file = "restructuredtext_lint-1.3.0.tar.gz", hash = "sha256:97b3da356d5b3a8514d8f1f9098febd8b41463bed6a1d9f126cf0a048b6fd908"},
+    {file = "restructuredtext_lint-1.3.1.tar.gz", hash = "sha256:470e53b64817211a42805c3a104d2216f6f5834b22fe7adb637d1de4d6501fb8"},
+]
+rules = [
+    {file = "rules-2.2.tar.gz", hash = "sha256:9bae429f9d4f91a375402990da1541f9e093b0ac077221d57124d06eeeca4405"},
 ]
 safety = [
-    {file = "safety-1.8.7-py2.py3-none-any.whl", hash = "sha256:05f77773bbab834502328b29ed013677aa53ed0c22b6e330aef7d2a7e1dfd838"},
-    {file = "safety-1.8.7.tar.gz", hash = "sha256:3016631e0dd17193d6cf12e8ed1af92df399585e8ee0e4b1300d9e7e32b54903"},
+    {file = "safety-1.9.0-py2.py3-none-any.whl", hash = "sha256:86c1c4a031fe35bd624fce143fbe642a0234d29f7cbf7a9aa269f244a955b087"},
+    {file = "safety-1.9.0.tar.gz", hash = "sha256:23bf20690d4400edc795836b0c983c2b4cbbb922233108ff925b7dd7750f00c9"},
 ]
 scramp = [
-    {file = "scramp-1.1.1-py3-none-any.whl", hash = "sha256:a2c740624642de84f77327da8f56b2f030c5afd10deccaedbb8eb6108a66dfc1"},
-    {file = "scramp-1.1.1.tar.gz", hash = "sha256:b57eb0ae2f9240b15b5d0dab2ea8e40b43eef13ac66d3f627a79ef85a6da0927"},
+    {file = "scramp-1.2.0-py3-none-any.whl", hash = "sha256:74815c25aad1fe0b5fb994e96c3de63e8695164358a80138352aaadfa4760350"},
+    {file = "scramp-1.2.0.tar.gz", hash = "sha256:d6865ed1d135ddb124a619d7cd3a5b505f69a7c92e248024dd7e48bc77752af5"},
 ]
 selenium = [
     {file = "selenium-3.141.0-py2.py3-none-any.whl", hash = "sha256:2d7131d7bc5a5b99a2d9b04aaf2612c411b03b8ca1b1ee8d3de5845a9be2cb3c"},
     {file = "selenium-3.141.0.tar.gz", hash = "sha256:deaf32b60ad91a4611b98d8002757f29e6f2c2d5fcaf202e1c9ad06d6772300d"},
 ]
 six = [
-    {file = "six-1.14.0-py2.py3-none-any.whl", hash = "sha256:8f3cd2e254d8f793e7f3d6d9df77b92252b52637291d0f0da013c76ea2724b6c"},
-    {file = "six-1.14.0.tar.gz", hash = "sha256:236bdbdce46e6e6a3d61a337c0f8b763ca1e8717c03b369e87a7ec7ce1319c0a"},
+    {file = "six-1.15.0-py2.py3-none-any.whl", hash = "sha256:8b74bedcbbbaca38ff6d7491d76f2b06b3592611af620f8426e82dddb04a5ced"},
+    {file = "six-1.15.0.tar.gz", hash = "sha256:30639c035cdb23534cd4aa2dd52c3bf48f06e5f4a941509c8bafd8ce11080259"},
 ]
 smmap = [
-    {file = "smmap-3.0.2-py2.py3-none-any.whl", hash = "sha256:52ea78b3e708d2c2b0cfe93b6fc3fbeec53db913345c26be6ed84c11ed8bebc1"},
-    {file = "smmap-3.0.2.tar.gz", hash = "sha256:b46d3fc69ba5f367df96d91f8271e8ad667a198d5a28e215a6c3d9acd133a911"},
+    {file = "smmap-3.0.4-py2.py3-none-any.whl", hash = "sha256:54c44c197c819d5ef1991799a7e30b662d1e520f2ac75c9efbeb54a742214cf4"},
+    {file = "smmap-3.0.4.tar.gz", hash = "sha256:9c98bbd1f9786d22f14b3d4126894d56befb835ec90cef151af566c7e19b5d24"},
 ]
 snowballstemmer = [
     {file = "snowballstemmer-2.0.0-py2.py3-none-any.whl", hash = "sha256:209f257d7533fdb3cb73bdbd24f436239ca3b2fa67d56f6ff88e86be08cc5ef0"},
     {file = "snowballstemmer-2.0.0.tar.gz", hash = "sha256:df3bac3df4c2c01363f3dd2cfa78cce2840a79b9f1c2d2de9ce8d31683992f52"},
 ]
 soupsieve = [
-    {file = "soupsieve-1.9.5-py2.py3-none-any.whl", hash = "sha256:bdb0d917b03a1369ce964056fc195cfdff8819c40de04695a80bc813c3cfa1f5"},
-    {file = "soupsieve-1.9.5.tar.gz", hash = "sha256:e2c1c5dee4a1c36bcb790e0fabd5492d874b8ebd4617622c4f6a731701060dda"},
+    {file = "soupsieve-1.9.6-py2.py3-none-any.whl", hash = "sha256:feb1e937fa26a69e08436aad4a9037cd7e1d4c7212909502ba30701247ff8abd"},
+    {file = "soupsieve-1.9.6.tar.gz", hash = "sha256:7985bacc98c34923a439967c1a602dc4f1e15f923b6fcf02344184f86cc7efaa"},
 ]
 spdx-license-list = [
-    {file = "spdx_license_list-0.4.0-py3-none-any.whl", hash = "sha256:e5c2d1efc4067ff83609a200c731db6c656fdfd26144ac8b50755d6c72515453"},
-    {file = "spdx_license_list-0.4.0.tar.gz", hash = "sha256:f8b5eeda2a1c88d8ce15f6324d5a6128a462932a2e55b032f017ac9a0e61f1c7"},
+    {file = "spdx_license_list-0.5.0-py3-none-any.whl", hash = "sha256:65c9f598dee3249d529300eb08800f8bf3d0d902868669146ada65192ecd0507"},
+    {file = "spdx_license_list-0.5.0.tar.gz", hash = "sha256:40cd53ff16401bab7059e6d1ef61839196b12079929a2763a50145d3b6949bc1"},
 ]
 sphinx = [
-    {file = "Sphinx-2.4.4-py3-none-any.whl", hash = "sha256:fc312670b56cb54920d6cc2ced455a22a547910de10b3142276495ced49231cb"},
-    {file = "Sphinx-2.4.4.tar.gz", hash = "sha256:b4c750d546ab6d7e05bdff6ac24db8ae3e8b8253a3569b754e445110a0a12b66"},
+    {file = "Sphinx-3.1.1-py3-none-any.whl", hash = "sha256:97c9e3bcce2f61d9f5edf131299ee9d1219630598d9f9a8791459a4d9e815be5"},
+    {file = "Sphinx-3.1.1.tar.gz", hash = "sha256:74fbead182a611ce1444f50218a1c5fc70b6cc547f64948f5182fb30a2a20258"},
 ]
 sphinx-autodoc-typehints = [
-    {file = "sphinx-autodoc-typehints-1.10.3.tar.gz", hash = "sha256:a6b3180167479aca2c4d1ed3b5cb044a70a76cccd6b38662d39288ebd9f0dff0"},
-    {file = "sphinx_autodoc_typehints-1.10.3-py3-none-any.whl", hash = "sha256:27c9e6ef4f4451766ab8d08b2d8520933b97beb21c913f3df9ab2e59b56e6c6c"},
+    {file = "sphinx-autodoc-typehints-1.11.0.tar.gz", hash = "sha256:bbf0b203f1019b0f9843ee8eef0cff856dc04b341f6dbe1113e37f2ebf243e11"},
+    {file = "sphinx_autodoc_typehints-1.11.0-py3-none-any.whl", hash = "sha256:89e19370a55db4aef1be2094d8fb1fb500ca455c55b3fcc8d2600ff805227e04"},
 ]
 sphinxcontrib-applehelp = [
     {file = "sphinxcontrib-applehelp-1.0.2.tar.gz", hash = "sha256:a072735ec80e7675e3f432fcae8610ecf509c5f1869d17e2eecff44389cdbc58"},
@@ -2855,15 +3000,15 @@ sqlparse = [
     {file = "sqlparse-0.3.1.tar.gz", hash = "sha256:e162203737712307dfe78860cc56c8da8a852ab2ee33750e33aeadf38d12c548"},
 ]
 stevedore = [
-    {file = "stevedore-1.32.0-py2.py3-none-any.whl", hash = "sha256:a4e7dc759fb0f2e3e2f7d8ffe2358c19d45b9b8297f393ef1256858d82f69c9b"},
-    {file = "stevedore-1.32.0.tar.gz", hash = "sha256:18afaf1d623af5950cc0f7e75e70f917784c73b652a34a12d90b309451b5500b"},
+    {file = "stevedore-2.0.1-py3-none-any.whl", hash = "sha256:c4724f8d7b8f6be42130663855d01a9c2414d6046055b5a65ab58a0e38637688"},
+    {file = "stevedore-2.0.1.tar.gz", hash = "sha256:609912b87df5ad338ff8e44d13eaad4f4170a65b79ae9cb0aa5632598994a1b7"},
 ]
 termcolor = [
     {file = "termcolor-1.1.0.tar.gz", hash = "sha256:1d6d69ce66211143803fbc56652b41d73b4a400a2891d7bf7a1cdf4c02de613b"},
 ]
 testfixtures = [
-    {file = "testfixtures-6.14.0-py2.py3-none-any.whl", hash = "sha256:799144b3cbef7b072452d9c36cbd024fef415ab42924b96aad49dfd9c763de66"},
-    {file = "testfixtures-6.14.0.tar.gz", hash = "sha256:cdfc3d73cb6d3d4dc3c67af84d912e86bf117d30ae25f02fe823382ef99383d2"},
+    {file = "testfixtures-6.14.1-py2.py3-none-any.whl", hash = "sha256:30566e24a1b34e4d3f8c13abf62557d01eeb4480bcb8f1745467bfb0d415a7d9"},
+    {file = "testfixtures-6.14.1.tar.gz", hash = "sha256:58d2b3146d93bc5ddb0cd24e0ccacb13e29bdb61e5c81235c58f7b8ee4470366"},
 ]
 "testing.common.database" = [
     {file = "testing.common.database-2.0.3-py2.py3-none-any.whl", hash = "sha256:e3ed492bf480a87f271f74c53b262caf5d85c8bc09989a8f534fa2283ec52492"},
@@ -2878,13 +3023,15 @@ text-unidecode = [
     {file = "text_unidecode-1.3-py2.py3-none-any.whl", hash = "sha256:1311f10e8b895935241623731c2ba64f4c455287888b18189350b67134a822e8"},
 ]
 toml = [
-    {file = "toml-0.10.0-py2.7.egg", hash = "sha256:f1db651f9657708513243e61e6cc67d101a39bad662eaa9b5546f789338e07a3"},
-    {file = "toml-0.10.0-py2.py3-none-any.whl", hash = "sha256:235682dd292d5899d361a811df37e04a8828a5b1da3115886b73cf81ebc9100e"},
-    {file = "toml-0.10.0.tar.gz", hash = "sha256:229f81c57791a41d65e399fc06bf0848bab550a9dfd5ed66df18ce5f05e73d5c"},
+    {file = "toml-0.10.1-py2.py3-none-any.whl", hash = "sha256:bda89d5935c2eac546d648028b9901107a595863cb36bae0c73ac804a9b4ce88"},
+    {file = "toml-0.10.1.tar.gz", hash = "sha256:926b612be1e5ce0634a2ca03470f95169cf16f939018233a670519cb4ac58b0f"},
 ]
 tqdm = [
-    {file = "tqdm-4.45.0-py2.py3-none-any.whl", hash = "sha256:ea9e3fd6bd9a37e8783d75bfc4c1faf3c6813da6bd1c3e776488b41ec683af94"},
-    {file = "tqdm-4.45.0.tar.gz", hash = "sha256:00339634a22c10a7a22476ee946bbde2dbe48d042ded784e4d88e0236eca5d81"},
+    {file = "tqdm-4.46.1-py2.py3-none-any.whl", hash = "sha256:07c06493f1403c1380b630ae3dcbe5ae62abcf369a93bbc052502279f189ab8c"},
+    {file = "tqdm-4.46.1.tar.gz", hash = "sha256:cd140979c2bebd2311dfb14781d8f19bd5a9debb92dcab9f6ef899c987fcf71f"},
+]
+twilio = [
+    {file = "twilio-6.43.0.tar.gz", hash = "sha256:1ff3b66992ebb59411794f669eab7f11bcfaacc5549eec1afb47af1c755872ac"},
 ]
 typed-ast = [
     {file = "typed_ast-1.4.1-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:73d785a950fc82dd2a25897d525d003f6378d1cb23ab305578394694202a58c3"},
@@ -2922,22 +3069,18 @@ vine = [
     {file = "vine-1.3.0-py2.py3-none-any.whl", hash = "sha256:ea4947cc56d1fd6f2095c8d543ee25dad966f78692528e68b4fada11ba3f98af"},
     {file = "vine-1.3.0.tar.gz", hash = "sha256:133ee6d7a9016f177ddeaf191c1f58421a1dcc6ee9a42c58b34bed40e1d2cd87"},
 ]
-virtualenv = [
-    {file = "virtualenv-20.0.18-py2.py3-none-any.whl", hash = "sha256:5021396e8f03d0d002a770da90e31e61159684db2859d0ba4850fbea752aa675"},
-    {file = "virtualenv-20.0.18.tar.gz", hash = "sha256:ac53ade75ca189bc97b6c1d9ec0f1a50efe33cbf178ae09452dcd9fd309013c1"},
-]
-virtualenv-clone = [
-    {file = "virtualenv-clone-0.5.4.tar.gz", hash = "sha256:665e48dd54c84b98b71a657acb49104c54e7652bce9c1c4f6c6976ed4c827a29"},
-    {file = "virtualenv_clone-0.5.4-py2.py3-none-any.whl", hash = "sha256:07e74418b7cc64f4fda987bf5bc71ebd59af27a7bc9e8a8ee9fd54b1f2390a27"},
-]
 wcwidth = [
-    {file = "wcwidth-0.1.9-py2.py3-none-any.whl", hash = "sha256:cafe2186b3c009a04067022ce1dcd79cb38d8d65ee4f4791b8888d6599d1bbe1"},
-    {file = "wcwidth-0.1.9.tar.gz", hash = "sha256:ee73862862a156bf77ff92b09034fc4825dd3af9cf81bc5b360668d425f3c5f1"},
+    {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"},
 ]
+yubiotp = [
+    {file = "YubiOTP-0.2.2.post1-py2.py3-none-any.whl", hash = "sha256:7e281801b24678f4bda855ce8ab975a7688a912f5a6cb22b6c2b16263a93cbd2"},
+    {file = "YubiOTP-0.2.2.post1.tar.gz", hash = "sha256:de83b1560226e38b5923f6ab919f962c8c2abb7c722104cb45b2b6db2ac86e40"},
+]
 zipp = [
     {file = "zipp-3.1.0-py3-none-any.whl", hash = "sha256:aa36550ff0c0b7ef7fa639055d797116ee891440eac1a56f378e2d3179e0320b"},
     {file = "zipp-3.1.0.tar.gz", hash = "sha256:c599e4d75c98f6798c509911d08a22e6c021d074469042177c8c86fb92eefd96"},
diff --git a/pyproject.toml b/pyproject.toml
index d07c890ad188c3a08f15095e07c9378125ece20e..f040f25c445107b92ab98dfa63885c0bbde7ddea 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -1,22 +1,30 @@
 [tool.poetry]
 name = "AlekSIS"
-version = "2.0a1"
+version = "2.0a3.dev0"
 packages = [
     { include = "aleksis" }
 ]
 readme = "README.rst"
-include = ["CHANGELOG.rst", "CODE_OF_CONDUCT.rst", "CONTRIBUTING.rst", "LICENCE.rst", "manage.py", "docs/*", "docs/**/*", "tox.ini"]
+include = ["CHANGELOG.rst", "CODE_OF_CONDUCT.rst", "CONTRIBUTING.rst", "Dockerfile", "LICENCE.rst", "manage.py", "docker/*", "docker/**/*", "docker-compose.yml", "docs/*", "docs/**/*", "tox.ini"]
 
 description = "AlekSIS (School Information System) — Core"
 authors = ["Dominik George <dominik.george@teckids.org>", "Julian Leucker <leuckeju@katharineum.de>", "mirabilos <thorsten.glaser@teckids.org>", "Frank Poetzsch-Heffter <p-h@katharineum.de>", "Tom Teichler <tom.teichler@teckids.org>", "Jonathan Weth <wethjo@katharineum.de>", "Hangzhi Yu <yuha@katharineum.de>"]
-license = "EUPL-1.2"
+maintainers = ["Jonathan Weth <wethjo@katharineum.de>", "Dominik George <dominik.george@teckids.org>"]
+license = "EUPL-1.2-or-later"
 homepage = "https://aleksis.org/"
-repository = "https://edugit.org/AlekSIS/Official/AlekSIS"
-documentation = "https://aleksis.edugit.io/AlekSIS/docs/html/"
+repository = "https://edugit.org/AlekSIS/official/AlekSIS"
+documentation = "https://aleksis.org/AlekSIS/docs/html/"
+keywords = ["SIS", "education", "school", "digitisation", "school apps"]
 classifiers = [
+    "Development Status :: 3 - Alpha",
     "Environment :: Web Environment",
+    "Framework :: Django :: 3.0",
+    "Intended Audience :: Developers",
     "Intended Audience :: Education",
-    "Topic :: Education"
+    "Topic :: Education",
+    "Topic :: Internet :: WWW/HTTP :: WSGI :: Application",
+    "Topic :: Software Development :: Libraries :: Application Frameworks",
+    "Typing :: Typed",
 ]
 
 [tool.poetry.dependencies]
@@ -29,13 +37,13 @@ django-middleware-global-request = "^0.1.2"
 django-menu-generator = "^1.0.4"
 django-tables2 = "^2.1"
 Pillow = "^7.0"
-django-phonenumber-field = {version = ">=3.0, <5.0", extras = ["phonenumbers"]}
+django-phonenumber-field = {version = ">=3.0, <4.0", extras = ["phonenumbers"]}
 django-sass-processor = "^0.8"
-libsass = "^0.19.2"
+libsass = "^0.20.0"
 colour = "^0.1.5"
 dynaconf = {version = "^2.0", extras = ["yaml", "toml", "ini"]}
 django-settings-context-processor = "^0.2"
-django-auth-ldap = { version = "^2.0", optional = true }
+django-auth-ldap = { version = "^2.2", optional = true }
 django-maintenance-mode = "^0.14.0"
 django-ipware = "^2.1"
 easy-thumbnails = "^2.6"
@@ -46,11 +54,11 @@ django-hattori = "^0.2"
 psycopg2 = "^2.8"
 django_select2 = "^7.1"
 requests = "^2.22"
-django-two-factor-auth = { version = "^1.10.0", extras = [ "YubiKey", "phonenumbers", "Call", "SMS" ] }
+django-two-factor-auth = { version = "^1.11.0", extras = [ "yubikey", "phonenumbers", "call", "sms" ] }
 django-yarnpkg = "^6.0"
 django-material = "^1.6.0"
 django-pwa = "^1.0.8"
-django-constance = { version = "^2.6.0", extras = ["database"] }
+django-dynamic-preferences = "^1.9"
 django_widget_tweaks = "^1.4.5"
 django-filter = "^2.2.0"
 django-templated-email = "^2.3.0"
@@ -64,22 +72,29 @@ django-celery-beat = {version="^2.0.0", optional=true}
 django-celery-email = {version="^3.0.0", optional=true}
 django-jsonstore = "^0.4.1"
 django-polymorphic = "^2.1.2"
-django-otp = "0.7.5"
-django-colorfield = "^0.2.1"
+django-otp = "0.9.3"
+django-colorfield = "^0.3.0"
 django-bleach = "^0.6.1"
+django-guardian = "^2.2.0"
+rules = "^2.2"
 django-cache-memoize = "^0.1.6"
-django-haystack = {version="3.0b1", allows-prereleases = true}
-celery-haystack = {version="^0.3.1", optional=true}
+django-haystack = {version="3.0b1", allow-prereleases = true}
+celery-haystack = {version="^0.10.0", optional=true}
 django-dbbackup = "^3.3.0"
-spdx-license-list = "^0.4.0"
+spdx-license-list = "^0.5.0"
 license-expression = "^1.2"
+django-reversion = "^3.0.7"
+django-favicon-plus-reloaded = "^1.0.4"
+django-health-check = "^3.12.1"
+psutil = "^5.7.0"
+celery-progress = "^0.0.10"
 
 [tool.poetry.extras]
 ldap = ["django-auth-ldap"]
 celery = ["Celery", "django-celery-results", "django-celery-beat", "django-celery-email", "celery-haystack"]
 
 [tool.poetry.dev-dependencies]
-sphinx = "^2.1"
+sphinx = "^3.0"
 sphinxcontrib-django = "^0.5.0"
 sphinx-autodoc-typehints = "^1.7"
 django-stubs = "^1.1"
@@ -89,7 +104,7 @@ pytest-django-testing-postgresql = "^0.1"
 selenium = "^3.141.0"
 safety = "^1.8.5"
 flake8 = "^3.7.9"
-flake8-django = "^0.0.4"
+flake8-django = "^1.0.0"
 flake8-fixme = "^1.1.1"
 flake8-mypy = "^17.8.0"
 flake8-bandit = "^2.1.2"
@@ -97,9 +112,9 @@ flake8-builtins = "^1.4.1"
 flake8-docstrings = "^1.5.0"
 flake8-rst-docstrings = "^0.0.13"
 black = "^19.10b0"
-flake8-black = "^0.1.1"
+flake8-black = "^0.2.0"
 isort = "^4.3.21"
-flake8-isort = "^2.8.0"
+flake8-isort = "^3.0.0"
 pytest-cov = "^2.8.1"
 pytest-sugar = "^0.9.2"
 
diff --git a/tox.ini b/tox.ini
index c6d978b969712b148cd9bbe602ecf385eff690c7..3729453a385969ca89622ab38e214b4b7ae9a104 100644
--- a/tox.ini
+++ b/tox.ini
@@ -5,11 +5,13 @@ envlist = py37,py38
 
 [testenv]
 whitelist_externals = poetry
+		      sudo
 skip_install = true
 envdir = {toxworkdir}/globalenv
-commands_pre = ./dev.sh install-all
+commands_pre =
+     - poetry install
 commands =
-    poetry run pytest --cov=. {posargs} aleksis/ apps/official/
+    - poetry run pytest --cov=. {posargs} aleksis/
 
 [testenv:selenium]
 setenv =
@@ -20,9 +22,9 @@ setenv =
 
 [testenv:lint]
 commands =
-    - poetry run black --check --diff aleksis/ apps/official/
-    - poetry run isort -c --diff --stdout -rc aleksis/ apps/official/
-    poetry run flake8 {posargs} aleksis/ apps/official/
+    - poetry run black --check --diff aleksis/
+    - poetry run isort -c --diff --stdout -rc aleksis/
+    poetry run flake8 {posargs} aleksis/
 
 [testenv:security]
 commands =
@@ -38,13 +40,13 @@ commands = poetry run make -C docs/ html {posargs}
 
 [testenv:reformat]
 commands =
-    poetry run isort -rc aleksis/ apps/official/
-    poetry run black aleksis/ apps/official/
+    poetry run isort -rc aleksis/
+    poetry run black aleksis/
 
 [flake8]
 max_line_length = 100
 exclude = migrations,tests
-ignore = E203,E231,W503
+ignore = BLK100,E203,E231,W503,D100,D101,D102,D103,D104,D105,D106,D107,RST215,RST214,F821,F841,S106,T100,T101,DJ05
 
 [isort]
 line_length = 100
@@ -54,6 +56,7 @@ use_parantheses = 1
 default_section = THIRDPARTY
 known_first_party = aleksis
 known_django = django
+skip = migrations
 sections = FUTURE,STDLIB,DJANGO,THIRDPARTY,FIRSTPARTY,LOCALFOLDER
 
 [mypy]