diff --git a/aleksis/core/apps.py b/aleksis/core/apps.py
index 0037842bc71590eb521aa67f25b5f4cc6b50fb92..aeda609326941428e483e05cabd6e5412b2832f7 100644
--- a/aleksis/core/apps.py
+++ b/aleksis/core/apps.py
@@ -2,6 +2,7 @@ from typing import Any, List, Optional, Tuple
 
 import django.apps
 from django.http import HttpRequest
+from django.utils.module_loading import autodiscover_modules
 
 from dynamic_preferences.registries import preference_models
 
@@ -36,6 +37,9 @@ class CoreConfig(AppConfig):
     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")
diff --git a/aleksis/core/filters.py b/aleksis/core/filters.py
index 12265c33c6b9e0d94a9dfb5e87d9111c721fc4ad..aa287690b3d72d7f4e72f1a41708e38401f2b0db 100644
--- a/aleksis/core/filters.py
+++ b/aleksis/core/filters.py
@@ -1,11 +1,74 @@
-from django_filters import CharFilter, FilterSet
+from typing import Sequence
+
+from django.db.models import Q
+from django.utils.translation import gettext as _
+
+from django_filters import CharFilter, FilterSet, ModelChoiceFilter, ModelMultipleChoiceFilter
 from material import Layout, Row
 
+from aleksis.core.models import Group, GroupType, Person, SchoolTerm
+
+
+class MultipleCharFilter(CharFilter):
+    """Filter for filtering multiple fields with one input.
+
+    >>> multiple_filter = MultipleCharFilter(["name__icontains", "short_name__icontains"])
+    """
+
+    def filter(self, qs, value):  # noqa
+        q = None
+        for field in self.fields:
+            if not q:
+                q = Q(**{field: value})
+            else:
+                q = q | Q(**{field: value})
+        return qs.filter(q)
+
+    def __init__(self, fields: Sequence[str], *args, **kwargs):
+        self.fields = fields
+        super().__init__(self, *args, **kwargs)
+
 
 class GroupFilter(FilterSet):
-    name = CharFilter(lookup_expr="icontains")
-    short_name = CharFilter(lookup_expr="icontains")
+    school_term = ModelChoiceFilter(queryset=SchoolTerm.objects.all())
+    group_type = ModelChoiceFilter(queryset=GroupType.objects.all())
+    parent_groups = ModelMultipleChoiceFilter(queryset=Group.objects.all())
+
+    search = MultipleCharFilter(["name__icontains", "short_name__icontains"], label=_("Search"))
 
     def __init__(self, *args, **kwargs):
         super().__init__(*args, **kwargs)
-        self.form.layout = Layout(Row("name", "short_name"))
+        self.form.layout = Layout(Row("search"), Row("school_term", "group_type", "parent_groups"))
+        self.form.initial = {"school_term": SchoolTerm.current}
+
+
+class PersonFilter(FilterSet):
+    name = MultipleCharFilter(
+        [
+            "first_name__icontains",
+            "additional_name__icontains",
+            "last_name__icontains",
+            "short_name__icontains",
+        ],
+        label=_("Search by name"),
+    )
+    contact = MultipleCharFilter(
+        [
+            "street__icontains",
+            "housenumber__icontains",
+            "postal_code__icontains",
+            "place__icontains",
+            "phone_number__icontains",
+            "mobile_number__icontains",
+            "email__icontains",
+        ],
+        label=_("Search by contact details"),
+    )
+
+    def __init__(self, *args, **kwargs):
+        super().__init__(*args, **kwargs)
+        self.form.layout = Layout(Row("name", "contact"), Row("is_active", "sex", "primary_group"))
+
+    class Meta:
+        model = Person
+        fields = ["sex", "is_active", "primary_group"]
diff --git a/aleksis/core/forms.py b/aleksis/core/forms.py
index abd45aabdee3f9d933376a0fc034f59218a8523f..e1c0be7ef4f28e609c93f3b58208aa0ea4d58b3a 100644
--- a/aleksis/core/forms.py
+++ b/aleksis/core/forms.py
@@ -9,8 +9,8 @@ from django_select2.forms import ModelSelect2MultipleWidget, Select2Widget
 from dynamic_preferences.forms import PreferenceForm
 from material import Fieldset, Layout, Row
 
-from .mixins import ExtensibleForm
-from .models import AdditionalField, Announcement, Group, GroupType, Person
+from .mixins import ExtensibleForm, SchoolTermRelatedExtensibleForm
+from .models import AdditionalField, Announcement, Group, GroupType, Person, SchoolTerm
 from .registries import (
     group_preferences_registry,
     person_preferences_registry,
@@ -80,10 +80,7 @@ class EditPersonForm(ExtensibleForm):
         Fieldset(_("Address"), Row("street", "housenumber"), Row("postal_code", "place")),
         Fieldset(_("Contact data"), "email", Row("phone_number", "mobile_number")),
         Fieldset(
-            _("Advanced personal data"),
-            Row("sex", "date_of_birth"),
-            Row("photo", "photo_cropping"),
-            "guardians",
+            _("Advanced personal data"), Row("sex", "date_of_birth"), Row("photo"), "guardians",
         ),
     )
 
@@ -106,11 +103,12 @@ class EditPersonForm(ExtensibleForm):
             "date_of_birth",
             "sex",
             "photo",
-            "photo_cropping",
             "guardians",
             "primary_group",
         ]
-        widgets = {"user": Select2Widget}
+        widgets = {
+            "user": Select2Widget,
+        }
 
     new_user = forms.CharField(
         required=False, label=_("New user"), help_text=_("Create a new account")
@@ -121,10 +119,11 @@ class EditPersonForm(ExtensibleForm):
         return PersonAccountForm.clean(self)
 
 
-class EditGroupForm(ExtensibleForm):
+class EditGroupForm(SchoolTermRelatedExtensibleForm):
     """Form to edit an existing group in the frontend."""
 
     layout = Layout(
+        Fieldset(_("School term"), "school_term"),
         Fieldset(_("Common data"), "name", "short_name", "group_type"),
         Fieldset(_("Persons"), "members", "owners", "parent_groups"),
         Fieldset(_("Additional data"), "additional_fields"),
@@ -170,7 +169,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(
@@ -205,6 +204,8 @@ class AnnouncementForm(ExtensibleForm):
 
         super().__init__(*args, **kwargs)
 
+        self.fields["groups"].queryset = Group.objects.for_current_school_term_or_all()
+
     def clean(self):
         data = super().clean()
 
@@ -298,3 +299,13 @@ class EditGroupTypeForm(forms.ModelForm):
     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/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 c1b19bb3970eac18d756ec0eae6bdaf8ae285d07..2e71c357384c1bc7e4ffa4ff1485f4edc4ffc7de 100644
--- a/aleksis/core/menus.py
+++ b/aleksis/core/menus.py
@@ -82,6 +82,17 @@ MENUS = {
                         ),
                     ],
                 },
+                {
+                    "name": _("School terms"),
+                    "url": "school_terms",
+                    "icon": "date_range",
+                    "validators": [
+                        (
+                            "aleksis.core.util.predicates.permission_validator",
+                            "core.view_schoolterm",
+                        ),
+                    ],
+                },
                 {
                     "name": _("Data management"),
                     "url": "data_management",
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_drop_image_cropping.py b/aleksis/core/migrations/0003_drop_image_cropping.py
new file mode 100644
index 0000000000000000000000000000000000000000..1fecda4aeed0c9c0c0075bc8972f648a8ec981a0
--- /dev/null
+++ b/aleksis/core/migrations/0003_drop_image_cropping.py
@@ -0,0 +1,22 @@
+# Generated by Django 3.0.7 on 2020-06-28 11:37
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('core', '0002_school_term'),
+    ]
+
+    operations = [
+        migrations.RemoveField(
+            model_name='person',
+            name='photo_cropping',
+        ),
+        migrations.AlterField(
+            model_name='person',
+            name='photo',
+            field=models.ImageField(blank=True, null=True, upload_to='', verbose_name='Photo'),
+        ),
+    ]
diff --git a/aleksis/core/mixins.py b/aleksis/core/mixins.py
index f6a8c18c483d0833d296abd6310cc2103331ba18..d7108b88b4452c0e25cf7dd0df613d004e82536e 100644
--- a/aleksis/core/mixins.py
+++ b/aleksis/core/mixins.py
@@ -4,21 +4,29 @@ from datetime import datetime
 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.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 DeleteView, ModelFormMixin
 
 import reversion
 from easyaudit.models import CRUDEvent
 from guardian.admin import GuardedModelAdmin
-from jsonstore.fields import JSONField, JSONFieldMixin
+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 _ExtensibleModelBase(models.base.ModelBase):
     """Ensure predefined behaviour on model creation.
@@ -35,6 +43,8 @@ class _ExtensibleModelBase(models.base.ModelBase):
             # Register all non-abstract models with django-reversion
             mcls = reversion.register(mcls)
 
+            mcls.extra_permissions = []
+
         return mcls
 
 
@@ -92,6 +102,8 @@ class ExtensibleModel(models.Model, metaclass=_ExtensibleModelBase):
     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."""
         pass
@@ -164,17 +176,17 @@ class ExtensibleModel(models.Model, metaclass=_ExtensibleModelBase):
     @classmethod
     def property_(cls, func: Callable[[], Any], name: Optional[str] = None) -> None:
         """Add the passed callable as a property."""
-        cls._safe_add(property(func), func.__name__)
+        cls._safe_add(property(func), name or func.__name__)
 
     @classmethod
     def method(cls, func: Callable[[], Any], name: Optional[str] = None) -> None:
         """Add the passed callable as a method."""
-        cls._safe_add(func, func.__name__)
+        cls._safe_add(func, name or func.__name__)
 
     @classmethod
     def class_method(cls, func: Callable[[], Any], name: Optional[str] = None) -> None:
         """Add the passed callable as a classmethod."""
-        cls._safe_add(classmethod(func), func.__name__)
+        cls._safe_add(classmethod(func), name or func.__name__)
 
     @classmethod
     def field(cls, **kwargs) -> None:
@@ -197,6 +209,66 @@ class ExtensibleModel(models.Model, metaclass=_ExtensibleModelBase):
 
         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(blank=True, null=True)
+        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."""
@@ -218,6 +290,11 @@ class ExtensibleModel(models.Model, metaclass=_ExtensibleModelBase):
         """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
 
@@ -279,3 +356,73 @@ 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 AdvancedDeleteView(DeleteView):
+    """Common confirm view for deleting.
+
+    .. warning ::
+
+        Using this view, objects are deleted permanently after confirming.
+        We recommend to include the mixin :class:`reversion.views.RevisionMixin`
+        from `django-reversion` to enable soft-delete.
+    """
+    success_message: Optional[str] = None
+
+    def delete(self, request, *args, **kwargs):
+        r = super().delete(request, *args, **kwargs)
+        if self.success_message:
+            messages.success(self.request, self.success_message)
+        return r
+
+
+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 7bc66be7505ed8ca651b4127b870da7f51192613..5d2f0839f425ff410d854942928ad7d651aa8b98 100644
--- a/aleksis/core/models.py
+++ b/aleksis/core/models.py
@@ -8,21 +8,23 @@ 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 get_site_preferences, now_tomorrow
 from .util.model_helpers import ICONS
@@ -43,6 +45,53 @@ FIELD_CHOICES = (
 )
 
 
+class SchoolTerm(ExtensibleModel):
+    """School term model.
+
+    This is used to manage start and end times of a school term and link data to it.
+    """
+
+    objects = CurrentSiteManagerWithoutMigrations.from_queryset(SchoolTermQuerySet)()
+
+    name = models.CharField(verbose_name=_("Name"), max_length=255, unique=True)
+
+    date_start = models.DateField(verbose_name=_("Start date"))
+    date_end = models.DateField(verbose_name=_("End date"))
+
+    @classmethod
+    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 = 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")
+        verbose_name_plural = _("School terms")
+
+
 class Person(ExtensibleModel):
     """Person model.
 
@@ -99,8 +148,7 @@ class Person(ExtensibleModel):
     date_of_birth = models.DateField(verbose_name=_("Date of birth"), blank=True, null=True)
     sex = models.CharField(verbose_name=_("Sex"), max_length=1, choices=SEX_CHOICES, blank=True)
 
-    photo = ImageCropField(verbose_name=_("Photo"), blank=True, null=True)
-    photo_cropping = ImageRatioField("photo", "600x800", size_warning=True)
+    photo = models.ImageField(verbose_name=_("Photo"), blank=True, null=True)
 
     guardians = models.ManyToManyField(
         "self",
@@ -250,12 +298,15 @@ class AdditionalField(ExtensibleModel):
         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(ExtensibleModel):
+class Group(SchoolTermRelatedExtensibleModel):
     """Group model.
 
     Any kind of group of persons in a school, including, but not limited
@@ -267,12 +318,18 @@ class Group(ExtensibleModel):
         verbose_name = _("Group")
         verbose_name_plural = _("Groups")
         permissions = (("assign_child_groups_to_groups", _("Can assign child groups to groups")),)
+        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"), max_length=255, unique=True)
+    name = models.CharField(verbose_name=_("Long name"), max_length=255)
     short_name = models.CharField(
-        verbose_name=_("Short name"), max_length=255, unique=True, blank=True, null=True  # noqa
+        verbose_name=_("Short name"), max_length=255, blank=True, null=True  # noqa
     )
 
     members = models.ManyToManyField(
@@ -302,7 +359,9 @@ class Group(ExtensibleModel):
         null=True,
         blank=True,
     )
-    additional_fields = models.ManyToManyField(AdditionalField, verbose_name=_("Additional fields"))
+    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])
@@ -313,7 +372,10 @@ class Group(ExtensibleModel):
         return list(self.members.all()) + list(self.owners.all())
 
     def __str__(self) -> str:
-        return f"{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)
@@ -657,6 +719,9 @@ class GroupType(ExtensibleModel):
     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")
diff --git a/aleksis/core/rules.py b/aleksis/core/rules.py
index 4f97eafddf3cfdb9725cf0e9e0cde05ef83d1ad0..35b00f23770f80e3a5ca821393af3bf0956833f8 100644
--- a/aleksis/core/rules.py
+++ b/aleksis/core/rules.py
@@ -66,6 +66,12 @@ edit_person_predicate = has_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)
@@ -88,6 +94,12 @@ edit_group_predicate = has_person & (
 )
 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"
@@ -98,14 +110,6 @@ add_perm("core.assign_child_groups_to_groups", assign_child_groups_to_groups_pre
 edit_school_information_predicate = has_person & has_global_perm("core.change_school")
 add_perm("core.edit_school_information", edit_school_information_predicate)
 
-# Edit school term
-edit_schoolterm_predicate = has_person & has_global_perm("core.change_schoolterm")
-add_perm("core.edit_schoolterm", edit_schoolterm_predicate)
-
-# Manage school
-manage_school_predicate = edit_school_information_predicate | edit_schoolterm_predicate
-add_perm("core.manage_school", manage_school_predicate)
-
 # Manage data
 manage_data_predicate = has_person & has_global_perm("core.manage_data")
 add_perm("core.manage_data", manage_data_predicate)
@@ -154,16 +158,6 @@ add_perm(
     ),
 )
 
-# View admin menu
-view_admin_menu_predicate = has_person & (
-    manage_data_predicate
-    | manage_school_predicate
-    | impersonate_predicate
-    | view_system_status_predicate
-    | view_announcements_predicate
-)
-add_perm("core.view_admin_menu", view_admin_menu_predicate)
-
 # View person personal details
 view_personal_details_predicate = has_person & (
     has_global_perm("core.view_personal_details")
@@ -246,3 +240,35 @@ 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)
diff --git a/aleksis/core/settings.py b/aleksis/core/settings.py
index 0593b2c090e82510fab716c72b50429d8423e495..5d45958e6bbd19c298b208b3c8d72e0eedb3909f 100644
--- a/aleksis/core/settings.py
+++ b/aleksis/core/settings.py
@@ -70,7 +70,6 @@ INSTALLED_APPS = [
     "django_yarnpkg",
     "django_tables2",
     "easy_thumbnails",
-    "image_cropping",
     "maintenance_mode",
     "menu_generator",
     "reversion",
@@ -85,6 +84,11 @@ 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",
@@ -152,12 +156,7 @@ TEMPLATES = [
     },
 ]
 
-THUMBNAIL_PROCESSORS = (
-    "image_cropping.thumbnail_processors.crop_corners",
-) + thumbnail_settings.THUMBNAIL_PROCESSORS
-
-# Already included by base template / Bootstrap
-IMAGE_CROPPING_JQUERY_URL = None
+THUMBNAIL_PROCESSORS = () + thumbnail_settings.THUMBNAIL_PROCESSORS
 
 WSGI_APPLICATION = "aleksis.core.wsgi.application"
 
@@ -204,6 +203,7 @@ if _settings.get("ldap.uri", None):
     import ldap  # noqa
     from django_auth_ldap.config import (
         LDAPSearch,
+        LDAPSearchUnion,
         NestedGroupOfNamesType,
         NestedGroupOfUniqueNamesType,
         PosixGroupType,
@@ -219,27 +219,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):
+    if _settings.get("ldap.groups.search", None):
         group_type = _settings.get("ldap.groups.type", "groupOfNames")
-        AUTH_LDAP_GROUP_SEARCH = LDAPSearch(
-            _settings.get("ldap.groups.base"),
-            ldap.SCOPE_SUBTREE,
-            _settings.get("ldap.groups.filter", f"(objectClass={group_type})"),
+
+        # 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()
@@ -419,7 +436,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"
@@ -657,21 +679,25 @@ HAYSTACK_SEARCH_RESULTS_PER_PAGE = 10
 
 DJANGO_EASY_AUDIT_WATCH_REQUEST_EVENTS = False
 
-
 if _settings.get("auth.oauth2.enabled", False):
     AUTHLIB_OAUTH_CLIENTS = {
         "default": {
             "client_id": _settings.get("auth.oauth2.id", ""),
             "client_secret": _settings.get("auth.oauth2.secret", ""),
             "request_token_url": _settings.get("auth.oauth2.token_url", ""),
-#            "request_token_params": _settings.get("auth.oauth2.token_params", None),
+            "request_token_params": _settings.get("auth.oauth2.token_params", None),
             "access_token_url": _settings.get("auth.oauth2.access_url", ""),
-#            "access_token_params": _settings.get("auth.oauth2.access_params", None),
-#            "refresh_token_url": _settings.get("auth.oauth2.refresh)_token_url", None),
+            "access_token_params": _settings.get("auth.oauth2.access_params", None),
+            "refresh_token_url": _settings.get("auth.oauth2.refresh)_token_url", None),
             "authorize_url": _settings.get("auth.oauth2.authorize_url", ""),
-#            "api_base_url": _settings.get("auth.oauth2.api_url", ""),
-#            "client_kwargs": _settings.get("auth.oauth2.kwargs", None)
+            "api_base_url": _settings.get("auth.oauth2.api_url", ""),
+            "client_kwargs": _settings.get("auth.oauth2.kwargs", None)
         }
     }
 
     LOGIN_URL = "oauth_login"
+
+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/static/js/main.js b/aleksis/core/static/js/main.js
index dd57cbe262eb60e24646b680fa435c940008c4aa..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();
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/tables.py b/aleksis/core/tables.py
index 6f7ba041457f11420cf9155ff0cc8ca5012fd9d1..f562b61a151487c5047ef2861255a00ef7a5f785 100644
--- a/aleksis/core/tables.py
+++ b/aleksis/core/tables.py
@@ -4,6 +4,24 @@ 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."""
 
@@ -22,6 +40,7 @@ class GroupsTable(tables.Table):
 
     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):
diff --git a/aleksis/core/templates/403.html b/aleksis/core/templates/403.html
index c84e7f57ed0fa82dead672fde7ffd2c20a8bdc81..cbc962a93da9f8e8efd91d1af1b493c254834281 100644
--- a/aleksis/core/templates/403.html
+++ b/aleksis/core/templates/403.html
@@ -5,17 +5,23 @@
 {% 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">{% trans "Error" %} (403): {% blocktrans %}You are not allowed to access the requested page or
-          object.{% endblocktrans %}</span>
+      <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/admins_list.html" %}
+        {% include "core/partials/admins_list.html" %}
       </div>
     </div>
   </div>
diff --git a/aleksis/core/templates/404.html b/aleksis/core/templates/404.html
index 45873cde006feaf32b27999cf357d4888d70f644..33c311fcaf4c106d44c53788b44426584d014c33 100644
--- a/aleksis/core/templates/404.html
+++ b/aleksis/core/templates/404.html
@@ -19,7 +19,7 @@
             administrators:
           {% endblocktrans %}
         </p>
-        {% include "core/admins_list.html" %}
+        {% include "core/partials/admins_list.html" %}
       </div>
     </div>
   </div>
diff --git a/aleksis/core/templates/500.html b/aleksis/core/templates/500.html
index 621b9e424054cb321072450140c60548ebec3a0d..a084d76f804f9fa1089c225ac4a16db7bf360f69 100644
--- a/aleksis/core/templates/500.html
+++ b/aleksis/core/templates/500.html
@@ -6,7 +6,7 @@
   <div class="container">
     <div class="card red">
       <div class="card-content white-text">
-        <div class="material-icons small">error_outline</div>
+        <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>
@@ -15,7 +15,7 @@
             error. You can also contact them directly:
           {% endblocktrans %}
         </p>
-        {% include "core/admins_list.html" %}
+        {% include "core/partials/admins_list.html" %}
       </div>
     </div>
   </div>
diff --git a/aleksis/core/templates/503.html b/aleksis/core/templates/503.html
index 9ed4fcecbaa5ef620264374fef1074288c4d7785..dd65828745eb846ee1f6b934043996ffa06759e1 100644
--- a/aleksis/core/templates/503.html
+++ b/aleksis/core/templates/503.html
@@ -6,7 +6,7 @@
   <div class="container">
     <div class="card red">
       <div class="card-content white-text">
-        <div class="material-icons small">error_outline</div>
+        <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>
@@ -14,7 +14,7 @@
             This page is currently unavailable. If this error persists, contact your site administrators:
           {% endblocktrans %}
         </p>
-        {% include "core/admins_list.html" %}
+        {% include "core/partials/admins_list.html" %}
       </div>
     </div>
   </div>
diff --git a/aleksis/core/templates/core/edit_additional_field.html b/aleksis/core/templates/core/additional_field/edit.html
similarity index 89%
rename from aleksis/core/templates/core/edit_additional_field.html
rename to aleksis/core/templates/core/additional_field/edit.html
index b1487eb259b44f1425c950574f7df845d4e22129..cbfbdfffb0db5932710697a3e20eb5776f8a2e8b 100644
--- a/aleksis/core/templates/core/edit_additional_field.html
+++ b/aleksis/core/templates/core/additional_field/edit.html
@@ -11,7 +11,7 @@
   <form method="post">
     {% csrf_token %}
     {% form form=edit_additional_field_form %}{% endform %}
-    {% include "core/save_button.html" %}
+    {% include "core/partials/save_button.html" %}
   </form>
 
 {% endblock %}
diff --git a/aleksis/core/templates/core/additional_fields.html b/aleksis/core/templates/core/additional_field/list.html
similarity index 100%
rename from aleksis/core/templates/core/additional_fields.html
rename to aleksis/core/templates/core/additional_field/list.html
diff --git a/aleksis/core/templates/core/base.html b/aleksis/core/templates/core/base.html
index e30d428f060f540cbf7d4dbbb483a1371dbddf38..07db50c7fb3a29dc169543ae03c4f26dff1ec25f 100644
--- a/aleksis/core/templates/core/base.html
+++ b/aleksis/core/templates/core/base.html
@@ -7,7 +7,7 @@
 <!DOCTYPE html>
 <html lang="{{ LANGUAGE_CODE }}">
 <head>
-  {% include "core/meta.html" %}
+  {% include "core/partials/meta.html" %}
 
   <title>
     {% block no_browser_title %}
@@ -81,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 %}
@@ -121,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>
diff --git a/aleksis/core/templates/core/base_print.html b/aleksis/core/templates/core/base_print.html
index 6059d664fccb38ce18a55eb9ea6d1a406a6e9ad4..a229d723ed2afa4ec8ad1e7fa0e775889c9d9990 100644
--- a/aleksis/core/templates/core/base_print.html
+++ b/aleksis/core/templates/core/base_print.html
@@ -5,7 +5,7 @@
 <!DOCTYPE html>
 <html lang="{{ LANGUAGE_CODE }}">
 <head>
-  {% include "core/meta.html" %}
+  {% include "core/partials/meta.html" %}
 
   <title>
     {% block no_browser_title %}
@@ -29,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>
@@ -37,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="{{ request.site.preferences.theme__logo.url }}" 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>
@@ -55,7 +57,7 @@
 
           <footer>
             <div class="left">
-              {{ SCHOOL.name }}
+              {{ request.site.preferences.school__name }}
             </div>
 
             <div class="right">
@@ -69,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/groups_child_groups.html b/aleksis/core/templates/core/group/child_groups.html
similarity index 100%
rename from aleksis/core/templates/core/groups_child_groups.html
rename to aleksis/core/templates/core/group/child_groups.html
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
similarity index 57%
rename from aleksis/core/templates/core/group_full.html
rename to aleksis/core/templates/core/group/full.html
index 0add6f1a3041b723e4a4e3d87972e07a8daebf97..def54269f884515d3f07f96ee353616df2db3c7e 100644
--- a/aleksis/core/templates/core/group_full.html
+++ b/aleksis/core/templates/core/group/full.html
@@ -3,7 +3,7 @@
 {% extends "core/base.html" %}
 {% load rules %}
 
-{% load i18n static %}
+{% load i18n static material_form %}
 {% load render_table from django_tables2 %}
 
 {% block browser_title %}{{ group.name }}{% endblock %}
@@ -13,8 +13,9 @@
 
   {% 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 %}
 
-  {% if can_change_group or can_change_group_preferences %}
+  {% 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">
@@ -22,6 +23,14 @@
           {% 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>
@@ -31,6 +40,25 @@
     </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>
+
   <h5>{% blocktrans %}Owners{% endblocktrans %}</h5>
   {% render_table owners_table %}
 
diff --git a/aleksis/core/templates/core/group/list.html b/aleksis/core/templates/core/group/list.html
new file mode 100644
index 0000000000000000000000000000000000000000..436b193e74f4c7e7e718fe5ba69a0b9fa0812a2b
--- /dev/null
+++ b/aleksis/core/templates/core/group/list.html
@@ -0,0 +1,30 @@
+{# -*- engine:django -*- #}
+
+{% extends "core/base.html" %}
+
+{% load i18n material_form %}
+{% load render_table from django_tables2 %}
+
+{% block browser_title %}{% blocktrans %}Groups{% endblocktrans %}{% endblock %}
+{% block page_title %}{% blocktrans %}Groups{% endblocktrans %}{% endblock %}
+
+{% block content %}
+  <a class="btn green waves-effect waves-light" href="{% url 'create_group' %}">
+    <i class="material-icons left">add</i>
+    {% trans "Create group" %}
+  </a>
+
+  <h5>{% trans "Filter groups" %}</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" %}
+    <button type="reset" class="btn red waves-effect waves-light">
+      <i class="material-icons left">clear</i>
+      {% trans "Clear" %}
+    </button>
+  </form>
+
+  <h5>{% trans "Selected groups" %}</h5>
+  {% render_table groups_table %}
+{% endblock %}
diff --git a/aleksis/core/templates/core/edit_group_type.html b/aleksis/core/templates/core/group_type/edit.html
similarity index 89%
rename from aleksis/core/templates/core/edit_group_type.html
rename to aleksis/core/templates/core/group_type/edit.html
index c857de98b893cf243c3c108f3c76b5aa142e6037..843975b16bb6ccbd7cf8eeed8f2d5713f5320e23 100644
--- a/aleksis/core/templates/core/edit_group_type.html
+++ b/aleksis/core/templates/core/group_type/edit.html
@@ -11,7 +11,7 @@
   <form method="post">
     {% csrf_token %}
     {% form form=edit_group_type_form %}{% endform %}
-    {% include "core/save_button.html" %}
+    {% include "core/partials/save_button.html" %}
   </form>
 
 {% endblock %}
diff --git a/aleksis/core/templates/core/group_types.html b/aleksis/core/templates/core/group_type/list.html
similarity index 100%
rename from aleksis/core/templates/core/group_types.html
rename to aleksis/core/templates/core/group_type/list.html
diff --git a/aleksis/core/templates/core/index.html b/aleksis/core/templates/core/index.html
index ac5d19b71bc6ac4acd00d7d3c509e1c07f089dcc..683adca900d46a8ba50790829db4d3ecd0ba2716 100644
--- a/aleksis/core/templates/core/index.html
+++ b/aleksis/core/templates/core/index.html
@@ -27,7 +27,7 @@
       </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 %}
diff --git a/aleksis/core/templates/core/language_form.html b/aleksis/core/templates/core/language_form.html
deleted file mode 100644
index ee2637becccc05bf4aa4032cd71d3cb162b6f98e..0000000000000000000000000000000000000000
--- a/aleksis/core/templates/core/language_form.html
+++ /dev/null
@@ -1,19 +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 87%
rename from aleksis/core/templates/core/data_management.html
rename to aleksis/core/templates/core/management/data_management.html
index 520dcb6d54beb20c482158cfa538ccf5cf2dc1ab..fa8e61f5dac1d1e01e5a773b68015cc9befe8d8f 100644
--- a/aleksis/core/templates/core/data_management.html
+++ b/aleksis/core/templates/core/management/data_management.html
@@ -8,5 +8,5 @@
 
 {% 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/about.html b/aleksis/core/templates/core/pages/about.html
similarity index 100%
rename from aleksis/core/templates/core/about.html
rename to aleksis/core/templates/core/pages/about.html
diff --git a/aleksis/core/templates/core/pages/delete.html b/aleksis/core/templates/core/pages/delete.html
new file mode 100644
index 0000000000000000000000000000000000000000..db64ee8fc3b977ed9cb74333e1a25834ce3e58b7
--- /dev/null
+++ b/aleksis/core/templates/core/pages/delete.html
@@ -0,0 +1,25 @@
+{% extends "core/base.html" %}
+{% load data_helpers i18n %}
+
+{% block browser_title %}
+  {% verbose_name_object object as object_name %}
+  {% blocktrans with object_name=object_name %}Delete {{ object_name }}{% endblocktrans %}
+{% endblock %}
+
+{% block content %}
+  {% verbose_name_object object as object_name %}
+
+  <p class="flow-text">
+    {% blocktrans with object_name=object_name object=object %}
+      Do you really want to delete the {{ object_name }} "{{ object }}"?
+    {% endblocktrans %}
+  </p>
+
+  <form method="post" action="">
+    {% csrf_token %}
+    <button type="submit" class="btn red waves-effect waves-light">
+      <i class="material-icons left">delete</i>
+      {% trans "Delete" %}
+    </button>
+  </form>
+{% endblock %}
diff --git a/aleksis/core/templates/offline.html b/aleksis/core/templates/core/pages/offline.html
similarity index 92%
rename from aleksis/core/templates/offline.html
rename to aleksis/core/templates/core/pages/offline.html
index 6961e03de97ea1a51096df239ac55880729e0fe7..a6a70dc19f074e8c3f3ede50b5e9b5b80b5682f1 100644
--- a/aleksis/core/templates/offline.html
+++ b/aleksis/core/templates/core/pages/offline.html
@@ -13,5 +13,5 @@
       administrators:
     {% endblocktrans %}
   </p>
-  {% include "core/admins_list.html" %}
+  {% 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/system_status.html b/aleksis/core/templates/core/pages/system_status.html
similarity index 80%
rename from aleksis/core/templates/core/system_status.html
rename to aleksis/core/templates/core/pages/system_status.html
index a62d024cf9ff5875ce61c8305b84834e47393726..89c6ed65e3982a0318838a66ef7f6716935d0af3 100644
--- a/aleksis/core/templates/core/system_status.html
+++ b/aleksis/core/templates/core/pages/system_status.html
@@ -63,6 +63,43 @@
     </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">
diff --git a/aleksis/core/templates/core/admins_list.html b/aleksis/core/templates/core/partials/admins_list.html
similarity index 100%
rename from aleksis/core/templates/core/admins_list.html
rename to aleksis/core/templates/core/partials/admins_list.html
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/crud_events.html b/aleksis/core/templates/core/partials/crud_events.html
similarity index 100%
rename from aleksis/core/templates/core/crud_events.html
rename to aleksis/core/templates/core/partials/crud_events.html
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/meta.html b/aleksis/core/templates/core/partials/meta.html
similarity index 100%
rename from aleksis/core/templates/core/meta.html
rename to aleksis/core/templates/core/partials/meta.html
diff --git a/aleksis/core/templates/core/no_person.html b/aleksis/core/templates/core/partials/no_person.html
similarity index 100%
rename from aleksis/core/templates/core/no_person.html
rename to aleksis/core/templates/core/partials/no_person.html
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 100%
rename from aleksis/core/templates/core/sidenav.html
rename to aleksis/core/templates/core/partials/sidenav.html
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 78%
rename from aleksis/core/templates/core/edit_person.html
rename to aleksis/core/templates/core/person/edit.html
index 8a5d0ca39a8fa0cbfd437519a09723ce59278c2d..261249f6867a4370745f66d3bd0abc05891442cf 100644
--- a/aleksis/core/templates/core/edit_person.html
+++ b/aleksis/core/templates/core/person/edit.html
@@ -4,6 +4,9 @@
 
 {% load material_form i18n %}
 
+{% block extra_head %}
+  {{ edit_person_form.media }}
+{% endblock %}
 
 {% block browser_title %}{% blocktrans %}Edit person{% endblocktrans %}{% endblock %}
 {% block page_title %}{% blocktrans %}Edit person{% endblocktrans %}{% endblock %}
@@ -14,7 +17,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
similarity index 78%
rename from aleksis/core/templates/core/person_full.html
rename to aleksis/core/templates/core/person/full.html
index d676fd75d87889edaa2f61f09711f9de758ee3e1..4d8bea526492c236d015e7263da05e0bc5dca99e 100644
--- a/aleksis/core/templates/core/person_full.html
+++ b/aleksis/core/templates/core/person/full.html
@@ -2,7 +2,7 @@
 
 {% extends "core/base.html" %}
 
-{% load i18n static cropping rules %}
+{% load i18n static rules material_form %}
 {% load render_table from django_tables2 %}
 
 {% block browser_title %}{{ person.first_name }} {{ person.last_name }}{% endblock %}
@@ -12,8 +12,9 @@
 
   {% 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 %}
+  {% 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">
@@ -22,6 +23,13 @@
         </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>
@@ -36,7 +44,7 @@
     <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' %}"
+        <img class="person-img" src="{{ person.photo.url }}"
              alt="{{ person.first_name }} {{ person.last_name }}"/>
       {% else %}
         <img class="person-img" src="{% static 'img/fallback.png' %}"
@@ -109,6 +117,20 @@
     {% 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>
diff --git a/aleksis/core/templates/core/person/list.html b/aleksis/core/templates/core/person/list.html
new file mode 100644
index 0000000000000000000000000000000000000000..b48baf3e5ff8b0f229ab13dfb7c6b5a5885d105f
--- /dev/null
+++ b/aleksis/core/templates/core/person/list.html
@@ -0,0 +1,34 @@
+{# -*- 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 %}
+
+  <h5>{% trans "Filter persons" %}</h5>
+  <form method="get">
+    {% form form=persons_filter.form %}{% endform %}
+    {% trans "Search" as caption %}
+    {% include "core/partials/save_button.html" with caption=caption icon="search" %}
+    <button type="reset" class="btn red waves-effect waves-light">
+      <i class="material-icons left">clear</i>
+      {% trans "Clear" %}
+    </button>
+  </form>
+
+  <h5>{% trans "Selected persons" %}</h5>
+  {% render_table persons_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_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/school_term/edit.html b/aleksis/core/templates/core/school_term/edit.html
new file mode 100644
index 0000000000000000000000000000000000000000..aa1b1dcf5015e876d0b9aa316d25da31673f0a3f
--- /dev/null
+++ b/aleksis/core/templates/core/school_term/edit.html
@@ -0,0 +1,17 @@
+{# -*- 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 %}
+
+{% 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/groups.html b/aleksis/core/templates/core/school_term/list.html
similarity index 51%
rename from aleksis/core/templates/core/groups.html
rename to aleksis/core/templates/core/school_term/list.html
index fab23516a458f55347ebcc152ee6ffac20e004d5..14aa2f0a697a3efd54b27154b431ef3f424923de 100644
--- a/aleksis/core/templates/core/groups.html
+++ b/aleksis/core/templates/core/school_term/list.html
@@ -5,14 +5,14 @@
 {% load i18n %}
 {% load render_table from django_tables2 %}
 
-{% block browser_title %}{% blocktrans %}Groups{% endblocktrans %}{% endblock %}
-{% block page_title %}{% blocktrans %}Groups{% endblocktrans %}{% endblock %}
+{% 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_group' %}">
+  <a class="btn green waves-effect waves-light" href="{% url 'create_school_term' %}">
     <i class="material-icons left">add</i>
-    {% trans "Create group" %}
+    {% trans "Create school term" %}
   </a>
 
-  {% render_table groups_table %}
+  {% render_table table %}
 {% endblock %}
diff --git a/aleksis/core/templates/dynamic_preferences/form.html b/aleksis/core/templates/dynamic_preferences/form.html
index d8de37802c09a14327b0d949b5935fb69660c666..da3d608285c43673bd27932d0d19cef303b1cef7 100644
--- a/aleksis/core/templates/dynamic_preferences/form.html
+++ b/aleksis/core/templates/dynamic_preferences/form.html
@@ -22,7 +22,7 @@
     <form action="" enctype="multipart/form-data" method="post">
       {% csrf_token %}
       {% form form=form %}{% endform %}
-      {% include "core/save_button.html" with caption=_("Save preferences") %}
+      {% include "core/partials/save_button.html" with caption=_("Save preferences") %}
     </form>
   </div>
 {% endblock %}
diff --git a/aleksis/core/templatetags/data_helpers.py b/aleksis/core/templatetags/data_helpers.py
index f7393c73fd86eba6f861f87e321bf8dc5cbce6fd..ab2309f260dd6b039cc722bae86fa71637967a1f 100644
--- a/aleksis/core/templatetags/data_helpers.py
+++ b/aleksis/core/templatetags/data_helpers.py
@@ -3,6 +3,7 @@ from typing import Any, Optional, Union
 
 from django import template
 from django.contrib.contenttypes.models import ContentType
+from django.db.models import Model
 
 register = template.Library()
 
@@ -33,6 +34,17 @@ def verbose_name(app_label: str, model: str, field: Optional[str] = None) -> str
         return ct._meta.verbose_name.title()
 
 
+@register.simple_tag
+def verbose_name_object(model: Model, field: Optional[str] = None) -> str:
+    """Get a verbose name of a model or a field by a model or an instance of a model."""
+    if field:
+        # Field
+        return model._meta.get_field(field).verbose_name.title()
+    else:
+        # Whole model
+        return model._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."""
diff --git a/aleksis/core/urls.py b/aleksis/core/urls.py
index b10a31346c8e6c95bded8a924995079ec119c23a..a709ee6b5aa954e21df16f476f8dac78110783cb 100644
--- a/aleksis/core/urls.py
+++ b/aleksis/core/urls.py
@@ -9,23 +9,30 @@ 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("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("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"),
@@ -47,6 +54,7 @@ urlpatterns = [
     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_>",
@@ -143,6 +151,7 @@ urlpatterns = [
         {"registry_name": "group"},
         name="preferences_group",
     ),
+    path("health/", include(health_urls)),
 ]
 
 # Serve static files from STATIC_ROOT to make it work with runserver
@@ -163,6 +172,10 @@ if hasattr(settings, "AUTHLIB_OAUTH_CLIENTS"):
     urlpatterns += [path("oauth2/login", views.oauth_login, name="oauth_login")]
     urlpatterns += [path("oauth2/authorize", views.oauth_authorize, name="oauth_authorize")]
 
+# Add celery 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)))
diff --git a/aleksis/core/util/apps.py b/aleksis/core/util/apps.py
index 1500537334ebf5d339955c55a480776053b01d03..79548a79dfbbb94a36a40bfad54fef5a24d3653c 100644
--- a/aleksis/core/util/apps.py
+++ b/aleksis/core/util/apps.py
@@ -1,4 +1,3 @@
-from importlib import import_module
 from typing import Any, List, Optional, Sequence, Tuple
 
 import django.apps
@@ -19,15 +18,6 @@ class AppConfig(django.apps.AppConfig):
     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)
@@ -38,13 +28,6 @@ class AppConfig(django.apps.AppConfig):
         # Getting an app ready means it should look at its config once
         self.preference_updated(self)
 
-        # 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
-
     @classmethod
     def get_name(cls):
         """Get name of application package."""
@@ -206,6 +189,9 @@ class AppConfig(django.apps.AppConfig):
         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
@@ -214,3 +200,9 @@ 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 dc62a7990844748e11859121854e44e13f2772f7..ba28f66292217b77a8d15eb1988774bbe09776fb 100644
--- a/aleksis/core/util/core_helpers.py
+++ b/aleksis/core/util/core_helpers.py
@@ -1,5 +1,6 @@
 import os
 import pkgutil
+import time
 from datetime import datetime, timedelta
 from importlib import import_module
 from itertools import groupby
@@ -14,6 +15,10 @@ 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:
     """Take a sequence of integegers and produces a string with ranges.
@@ -170,6 +175,11 @@ def has_person(obj: Union[HttpRequest, Model]) -> bool:
         return True
 
 
+def is_celery_enabled():
+    """Check whether celery support is enabled."""
+    return hasattr(settings, "CELERY_RESULT_BACKEND")
+
+
 def celery_optional(orig: Callable) -> Callable:
     """Add a decorator that makes Celery optional for a function.
 
@@ -177,13 +187,13 @@ def celery_optional(orig: Callable) -> Callable:
     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)
@@ -191,6 +201,108 @@ def celery_optional(orig: Callable) -> Callable:
     return wrapped
 
 
+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)
@@ -231,3 +343,9 @@ def objectgetter_optional(
             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/views.py b/aleksis/core/views.py
index be165a5ce8bf0e53e5f2f71d8bba7dfabcc1bc5c..6e03d2482b6b91877d5ff98d29d5e47a9e12959d 100644
--- a/aleksis/core/views.py
+++ b/aleksis/core/views.py
@@ -2,23 +2,25 @@ from typing import Optional
 
 from django.apps import apps
 from django.conf import settings
-from django.contrib.auth.mixins import PermissionRequiredMixin
 from django.core.exceptions import PermissionDenied
 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
+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 rules.contrib.views import permission_required
+from health_check.views import MainView
+from rules.contrib.views import PermissionRequiredMixin, permission_required
 
-from .filters import GroupFilter
+from .filters import GroupFilter, PersonFilter
 from .forms import (
     AnnouncementForm,
     ChildGroupsForm,
@@ -29,8 +31,10 @@ from .forms import (
     GroupPreferenceForm,
     PersonPreferenceForm,
     PersonsAccountsFormSet,
+    SchoolTermForm,
     SitePreferenceForm,
 )
+from .mixins import AdvancedCreateView, AdvancedEditView
 from .models import (
     AdditionalField,
     Announcement,
@@ -39,13 +43,20 @@ from .models import (
     GroupType,
     Notification,
     Person,
+    SchoolTerm,
 )
 from .registries import (
     group_preferences_registry,
     person_preferences_registry,
     site_preferences_registry,
 )
-from .tables import AdditionalFieldsTable, GroupsTable, GroupTypesTable, PersonsTable
+from .tables import (
+    AdditionalFieldsTable,
+    GroupsTable,
+    GroupTypesTable,
+    PersonsTable,
+    SchoolTermTable,
+)
 from .util import messages
 from .util.apps import AppConfig
 from .util.core_helpers import objectgetter_optional
@@ -78,7 +89,7 @@ def index(request: HttpRequest) -> HttpResponse:
 
 def offline(request: HttpRequest) -> HttpResponse:
     """Offline message for PWA."""
-    return render(request, "core/offline.html")
+    return render(request, "core/pages/offline.html")
 
 
 def about(request: HttpRequest) -> HttpResponse:
@@ -89,7 +100,38 @@ def about(request: HttpRequest) -> HttpResponse:
         filter(lambda a: isinstance(a, AppConfig), apps.get_app_configs())
     )
 
-    return render(request, "core/about.html", context)
+    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"
+
+
+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.")
+
+
+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")
@@ -102,12 +144,16 @@ def persons(request: HttpRequest) -> HttpResponse:
         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)
 
 
 @permission_required(
@@ -128,7 +174,7 @@ def person(request: HttpRequest, id_: Optional[int] = None) -> HttpResponse:
     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)
 
 
 @permission_required("core.view_group", fn=objectgetter_optional(Group, None, False))
@@ -158,7 +204,7 @@ def group(request: HttpRequest, id_: int) -> HttpResponse:
     RequestConfig(request).configure(owners_table)
     context["owners_table"] = owners_table
 
-    return render(request, "core/group_full.html", context)
+    return render(request, "core/group/full.html", context)
 
 
 @permission_required("core.view_groups")
@@ -169,12 +215,16 @@ def groups(request: HttpRequest) -> HttpResponse:
     # Get all groups
     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(groups_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)
 
 
 @permission_required("core.link_persons_accounts")
@@ -194,7 +244,7 @@ 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)
 
 
 @permission_required("core.assign_child_groups_to_groups")
@@ -231,32 +281,37 @@ def groups_child_groups(request: HttpRequest) -> HttpResponse:
         context["group"] = group
         context["form"] = form
 
-    return render(request, "core/groups_child_groups.html", context)
+    return render(request, "core/group/child_groups.html", context)
 
 
-@permission_required(
-    "core.edit_person", fn=objectgetter_optional(Person, "request.user.person", True)
-)
+@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.user.person", True)(request, id_)
+    person = objectgetter_optional(Person)(request, id_)
     context["person"] = person
 
-    edit_person_form = EditPersonForm(request.POST or None, request.FILES or None, instance=person)
-
+    if id_:
+        # Edit form for existing group
+        edit_person_form = EditPersonForm(
+            request.POST or None, request.FILES or None, instance=person
+        )
+    else:
+        # Empty form to create a new group
+        if request.user.has_perm("core.create_person"):
+            edit_person_form = EditPersonForm(request.POST or None, request.FILES or None)
+        else:
+            raise PermissionDenied()
     if request.method == "POST":
         if edit_person_form.is_valid():
-            edit_person_form.save(commit=True)
+            with reversion.create_revision():
+                edit_person_form.save(commit=True)
             messages.success(request, _("The person has been saved."))
 
-            # Redirect to self to ensure post-processed data is displayed
-            return redirect("edit_person_by_id", id_=person.id)
-
     context["edit_person_form"] = edit_person_form
 
-    return render(request, "core/edit_person.html", context)
+    return render(request, "core/person/edit.html", context)
 
 
 def get_group_by_id(request: HttpRequest, id_: Optional[int] = None):
@@ -279,44 +334,54 @@ def edit_group(request: HttpRequest, id_: Optional[int] = None) -> HttpResponse:
         edit_group_form = EditGroupForm(request.POST or None, instance=group)
     else:
         # Empty form to create a new group
-        edit_group_form = EditGroupForm(request.POST or None)
+        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():
-            edit_group_form.save(commit=True)
+            with reversion.create_revision():
+                group = edit_group_form.save(commit=True)
 
             messages.success(request, _("The group has been saved."))
 
-            return redirect("groups")
+            return redirect("group_by_id", group.pk)
 
     context["edit_group_form"] = edit_group_form
 
-    return render(request, "core/edit_group.html", context)
+    return render(request, "core/group/edit.html", context)
 
 
 @permission_required("core.manage_data")
 def data_management(request: HttpRequest) -> HttpResponse:
     """View with special menu for data management."""
     context = {}
-    return render(request, "core/data_management.html", context)
+    return render(request, "core/management/data_management.html", context)
 
 
-@permission_required("core.view_system_status")
-def system_status(request: HttpRequest) -> HttpResponse:
+class SystemStatus(MainView, PermissionRequiredMixin):
     """View giving information about the system status."""
+
+    template_name = "core/pages/system_status.html"
+    permission_required = "core.view_system_status"
     context = {}
 
-    if "django_celery_results" in settings.INSTALLED_APPS:
-        from django_celery_results.models import TaskResult # noqa
-        from celery.task.control import inspect # noqa
-        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())
-            context["tasks"] = results
+    def get(self, request, *args, **kwargs):
+        status_code = 500 if self.errors else 200
+
+        if "django_celery_results" in settings.INSTALLED_APPS:
+            from django_celery_results.models import TaskResult  # noqa
+            from celery.task.control import inspect  # noqa
+
+            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/system_status.html", context)
+        context = {"plugins": self.plugins, "status_code": status_code}
+        return self.render_to_response(context, status=status_code)
 
 
 @permission_required(
@@ -468,6 +533,33 @@ def preferences(
     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)
 )
@@ -500,7 +592,7 @@ def edit_additional_field(request: HttpRequest, id_: Optional[int] = None) -> Ht
 
     context["edit_additional_field_form"] = edit_additional_field_form
 
-    return render(request, "core/edit_additional_field.html", context)
+    return render(request, "core/additional_field/edit.html", context)
 
 
 @permission_required("core.view_additionalfield")
@@ -518,7 +610,7 @@ def additional_fields(request: HttpRequest) -> HttpResponse:
     RequestConfig(request).configure(additional_fields_table)
     context["additional_fields_table"] = additional_fields_table
 
-    return render(request, "core/additional_fields.html", context)
+    return render(request, "core/additional_field/list.html", context)
 
 
 @permission_required(
@@ -558,7 +650,7 @@ def edit_group_type(request: HttpRequest, id_: Optional[int] = None) -> HttpResp
 
     context["edit_group_type_form"] = edit_group_type_form
 
-    return render(request, "core/edit_group_type.html", context)
+    return render(request, "core/group_type/edit.html", context)
 
 
 @permission_required("core.view_grouptype")
@@ -574,7 +666,7 @@ def group_types(request: HttpRequest) -> HttpResponse:
     RequestConfig(request).configure(group_types_table)
     context["group_types_table"] = group_types_table
 
-    return render(request, "core/group_types.html", context)
+    return render(request, "core/group_type/list.html", context)
 
 
 @permission_required("core.delete_grouptype", fn=objectgetter_optional(GroupType, None, False))
diff --git a/apps/official/AlekSIS-App-Chronos b/apps/official/AlekSIS-App-Chronos
index 9c747d3737c3ac31fce2eafcf8a977bb926feeb9..b081e4d32922d38e4f6229f33e7cecf5a1b408e0 160000
--- a/apps/official/AlekSIS-App-Chronos
+++ b/apps/official/AlekSIS-App-Chronos
@@ -1 +1 @@
-Subproject commit 9c747d3737c3ac31fce2eafcf8a977bb926feeb9
+Subproject commit b081e4d32922d38e4f6229f33e7cecf5a1b408e0
diff --git a/apps/official/AlekSIS-App-DashboardFeeds b/apps/official/AlekSIS-App-DashboardFeeds
index a921629be8c775720d4c81fc078114bddcd9e7d1..85814a4f6c4fc1d94a853b46be8544c11b5767ee 160000
--- a/apps/official/AlekSIS-App-DashboardFeeds
+++ b/apps/official/AlekSIS-App-DashboardFeeds
@@ -1 +1 @@
-Subproject commit a921629be8c775720d4c81fc078114bddcd9e7d1
+Subproject commit 85814a4f6c4fc1d94a853b46be8544c11b5767ee
diff --git a/apps/official/AlekSIS-App-Hjelp b/apps/official/AlekSIS-App-Hjelp
index 6a2e35f4756260a2a044f500295b29a357069ea0..31500a0580b6839122df35f114e471334903d26e 160000
--- a/apps/official/AlekSIS-App-Hjelp
+++ b/apps/official/AlekSIS-App-Hjelp
@@ -1 +1 @@
-Subproject commit 6a2e35f4756260a2a044f500295b29a357069ea0
+Subproject commit 31500a0580b6839122df35f114e471334903d26e
diff --git a/apps/official/AlekSIS-App-LDAP b/apps/official/AlekSIS-App-LDAP
index e782474f724b2eb281001897b4ad85f093e1539e..487a3423abc8b8bc8a4d89e474336c76b5cafa61 160000
--- a/apps/official/AlekSIS-App-LDAP
+++ b/apps/official/AlekSIS-App-LDAP
@@ -1 +1 @@
-Subproject commit e782474f724b2eb281001897b4ad85f093e1539e
+Subproject commit 487a3423abc8b8bc8a4d89e474336c76b5cafa61
diff --git a/apps/official/AlekSIS-App-Untis b/apps/official/AlekSIS-App-Untis
index 95cdb833b1eb8f235d73028a2c96390fd024e8b3..0b417dfef095fb7a52f03301abb66066a841b467 160000
--- a/apps/official/AlekSIS-App-Untis
+++ b/apps/official/AlekSIS-App-Untis
@@ -1 +1 @@
-Subproject commit 95cdb833b1eb8f235d73028a2c96390fd024e8b3
+Subproject commit 0b417dfef095fb7a52f03301abb66066a841b467
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/dev/01_setup.rst b/docs/dev/01_setup.rst
index df0ffd11c21112912f0934c9f16aed909bd8cbb9..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
diff --git a/poetry.lock b/poetry.lock
index 70a383c4f3f3482ae178d8d10d2f3c022a36f8b7..b681aa435a788b60a54b8ffadf25a0f0e90b152e 100644
--- a/poetry.lock
+++ b/poetry.lock
@@ -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"
@@ -161,7 +161,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"
@@ -180,10 +180,11 @@ 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.4"
+version = "4.4.6"
 
 [package.dependencies]
 billiard = ">=3.6.3.0,<4.0"
+future = ">=0.18.0"
 kombu = ">=4.6.10,<4.7"
 pytz = ">0.0-dev"
 vine = "1.3.0"
@@ -241,13 +242,26 @@ version = "0.10"
 [package.dependencies]
 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"
 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"
@@ -255,7 +269,7 @@ description = "Foreign Function Interface for Python calling C code."
 name = "cffi"
 optional = false
 python-versions = "*"
-version = "1.14.0"
+version = "1.14.1"
 
 [package.dependencies]
 pycparser = "*"
@@ -313,7 +327,7 @@ description = "Code coverage measurement for Python"
 name = "coverage"
 optional = false
 python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, <4"
-version = "5.1"
+version = "5.2.1"
 
 [package.extras]
 toml = ["toml"]
@@ -324,17 +338,18 @@ description = "cryptography is a package which provides cryptographic recipes an
 name = "cryptography"
 optional = false
 python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*"
-version = "2.9.2"
+version = "3.0"
 
 [package.dependencies]
 cffi = ">=1.8,<1.11.3 || >1.11.3"
 six = ">=1.4.1"
 
 [package.extras]
-docs = ["sphinx (>=1.6.5,<1.8.0 || >1.8.0)", "sphinx-rtd-theme"]
+docs = ["sphinx (>=1.6.5,<1.8.0 || >1.8.0,<3.1.0 || >3.1.0,<3.1.1 || >3.1.1)", "sphinx-rtd-theme"]
 docstest = ["doc8", "pyenchant (>=1.6.11)", "twine (>=1.12.0)", "sphinxcontrib-spelling (>=4.0.1)"]
 idna = ["idna (>=2.1)"]
-pep8test = ["flake8", "flake8-import-order", "pep8-naming"]
+pep8test = ["black", "flake8", "flake8-import-order", "pep8-naming"]
+ssh = ["bcrypt (>=3.1.5)"]
 test = ["pytest (>=3.6.0,<3.9.0 || >3.9.0,<3.9.1 || >3.9.1,<3.9.2 || >3.9.2)", "pretend", "iso8601", "pytz", "hypothesis (>=1.11.4,<3.79.2 || >3.79.2)"]
 
 [[package]]
@@ -351,7 +366,7 @@ description = "A high-level Python Web framework that encourages rapid developme
 name = "django"
 optional = false
 python-versions = ">=3.6"
-version = "3.0.7"
+version = "3.0.8"
 
 [package.dependencies]
 asgiref = ">=3.2,<4.0"
@@ -485,7 +500,7 @@ description = "simple color field for your models with a nice color-picker in th
 name = "django-colorfield"
 optional = false
 python-versions = "*"
-version = "0.3.0"
+version = "0.3.2"
 
 [[package]]
 category = "main"
@@ -518,7 +533,7 @@ description = "Dynamic global and instance settings for your django project"
 name = "django-dynamic-preferences"
 optional = false
 python-versions = "*"
-version = "1.9"
+version = "1.10"
 
 [package.dependencies]
 django = ">=1.11"
@@ -531,7 +546,7 @@ description = "Yet another Django audit log app, hopefully the simplest one."
 name = "django-easy-audit"
 optional = false
 python-versions = "*"
-version = "1.2.3a4"
+version = "1.3.0a3"
 
 [package.dependencies]
 beautifulsoup4 = "*"
@@ -553,11 +568,11 @@ 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"
@@ -576,10 +591,10 @@ description = "Implementation of per object permissions for Django."
 name = "django-guardian"
 optional = false
 python-versions = ">=3.5"
-version = "2.2.0"
+version = "2.3.0"
 
 [package.dependencies]
-Django = ">=2.1"
+Django = ">=2.2"
 
 [[package]]
 category = "main"
@@ -607,6 +622,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"
@@ -624,7 +650,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"
@@ -632,7 +658,7 @@ description = "A Django utility application that returns client's real IP addres
 name = "django-ipware"
 optional = false
 python-versions = "*"
-version = "2.1.0"
+version = "3.0.0"
 
 [[package]]
 category = "main"
@@ -709,7 +735,7 @@ description = "A pluggable framework for adding two-factor authentication to Dja
 name = "django-otp"
 optional = false
 python-versions = "*"
-version = "0.9.1"
+version = "0.9.3"
 
 [package.dependencies]
 django = ">=1.11"
@@ -723,12 +749,11 @@ description = "A django-otp plugin that verifies YubiKey OTP tokens."
 name = "django-otp-yubikey"
 optional = false
 python-versions = "*"
-version = "0.5.2"
+version = "0.6.0"
 
 [package.dependencies]
 YubiOTP = ">=0.2.2"
 django-otp = ">=0.5.0"
-six = ">=1.10.0"
 
 [[package]]
 category = "main"
@@ -742,6 +767,10 @@ version = "3.0.1"
 Django = ">=1.11.3"
 babel = "*"
 
+[package.dependencies.phonenumbers]
+optional = true
+version = ">=7.0.2"
+
 [package.extras]
 phonenumbers = ["phonenumbers (>=7.0.2)"]
 phonenumberslite = ["phonenumberslite (>=7.0.2)"]
@@ -763,7 +792,7 @@ description = "A Django app to include a manifest.json and Service Worker instan
 name = "django-pwa"
 optional = false
 python-versions = "*"
-version = "1.0.9"
+version = "1.0.10"
 
 [package.dependencies]
 django = ">=1.8"
@@ -773,11 +802,11 @@ category = "main"
 description = "Render a particular block from a template to a string."
 name = "django-render-block"
 optional = false
-python-versions = "*"
-version = "0.6"
+python-versions = ">=3.5"
+version = "0.7"
 
 [package.dependencies]
-django = ">=1.11"
+django = ">=2.2"
 
 [[package]]
 category = "main"
@@ -882,10 +911,10 @@ description = "Complete Two-Factor Authentication for Django"
 name = "django-two-factor-auth"
 optional = false
 python-versions = "*"
-version = "1.11.0"
+version = "1.12.1"
 
 [package.dependencies]
-Django = ">=1.11"
+Django = ">=2.2"
 django-formtools = "*"
 django-otp = ">=0.6.0,<0.99"
 django-phonenumber-field = ">=1.1.0,<3.99"
@@ -1003,7 +1032,7 @@ description = "Faker is a Python package that generates fake data for you."
 name = "faker"
 optional = false
 python-versions = ">=3.4"
-version = "4.1.0"
+version = "4.1.1"
 
 [package.dependencies]
 python-dateutil = ">=2.4"
@@ -1015,7 +1044,7 @@ description = "the modular source code checker: pep8 pyflakes and co"
 name = "flake8"
 optional = false
 python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,>=2.7"
-version = "3.8.2"
+version = "3.8.3"
 
 [package.dependencies]
 mccabe = ">=0.6.0,<0.7.0"
@@ -1046,7 +1075,7 @@ description = "flake8 plugin to call black as a code style validator"
 name = "flake8-black"
 optional = false
 python-versions = "*"
-version = "0.2.0"
+version = "0.2.1"
 
 [package.dependencies]
 black = "*"
@@ -1152,6 +1181,14 @@ 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"
@@ -1169,7 +1206,7 @@ description = "Python Git Library"
 name = "gitpython"
 optional = false
 python-versions = ">=3.4"
-version = "3.1.3"
+version = "3.1.7"
 
 [package.dependencies]
 gitdb = ">=4.0.1,<5"
@@ -1188,7 +1225,7 @@ description = "Internationalized Domain Names in Applications (IDNA)"
 name = "idna"
 optional = false
 python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
-version = "2.9"
+version = "2.10"
 
 [[package]]
 category = "dev"
@@ -1205,28 +1242,26 @@ 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"
 description = "A Python utility / library to sort Python imports."
 name = "isort"
 optional = false
-python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
-version = "4.3.21"
+python-versions = ">=3.6,<4.0"
+version = "5.2.0"
 
 [package.extras]
-pipfile = ["pipreqs", "requirementslib"]
-pyproject = ["toml"]
-requirements = ["pipreqs", "pip-api"]
-xdg_home = ["appdirs (>=1.4.0)"]
+pipfile_deprecated_finder = ["pipreqs", "requirementslib", "tomlkit (>=0.5.3)"]
+requirements_deprecated_finder = ["pipreqs", "pip-api"]
 
 [[package]]
 category = "dev"
@@ -1248,7 +1283,7 @@ 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.10"
+version = "4.6.11"
 
 [package.dependencies]
 amqp = ">=2.6.0,<2.7"
@@ -1317,7 +1352,7 @@ description = "More routines for operating on iterables, beyond itertools"
 name = "more-itertools"
 optional = false
 python-versions = ">=3.5"
-version = "8.3.0"
+version = "8.4.0"
 
 [[package]]
 category = "dev"
@@ -1385,10 +1420,10 @@ description = "PostgreSQL interface library"
 name = "pg8000"
 optional = false
 python-versions = ">=3.5"
-version = "1.15.2"
+version = "1.16.3"
 
 [package.dependencies]
-scramp = "1.1.1"
+scramp = "1.2.0"
 
 [[package]]
 category = "main"
@@ -1396,7 +1431,7 @@ description = "Python version of Google's common library for parsing, formatting
 name = "phonenumbers"
 optional = false
 python-versions = "*"
-version = "8.12.4"
+version = "8.12.7"
 
 [[package]]
 category = "main"
@@ -1404,7 +1439,7 @@ description = "Python Imaging Library (Fork)"
 name = "pillow"
 optional = false
 python-versions = ">=3.5"
-version = "7.1.2"
+version = "7.2.0"
 
 [[package]]
 category = "dev"
@@ -1422,6 +1457,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.2"
+
+[package.extras]
+test = ["ipaddress", "mock", "unittest2", "enum34", "pywin32", "wmi"]
+
 [[package]]
 category = "main"
 description = "psycopg2 - Python-PostgreSQL Database Adapter"
@@ -1436,7 +1482,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"
@@ -1479,7 +1525,7 @@ description = "Cryptographic library for Python"
 name = "pycryptodome"
 optional = false
 python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
-version = "3.9.7"
+version = "3.9.8"
 
 [[package]]
 category = "dev"
@@ -1561,11 +1607,11 @@ description = "Pytest plugin for measuring coverage."
 name = "pytest-cov"
 optional = false
 python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
-version = "2.9.0"
+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", "pytest-xdist", "virtualenv"]
@@ -1603,7 +1649,7 @@ description = "pytest-sugar is a plugin for pytest that changes the default look
 name = "pytest-sugar"
 optional = false
 python-versions = "*"
-version = "0.9.3"
+version = "0.9.4"
 
 [package.dependencies]
 packaging = ">=14.1"
@@ -1653,7 +1699,7 @@ description = "Add .env support to your django/flask apps in development and dep
 name = "python-dotenv"
 optional = false
 python-versions = "*"
-version = "0.13.0"
+version = "0.14.0"
 
 [package.extras]
 cli = ["click (>=5.0)"]
@@ -1664,7 +1710,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.1"
 
 [package.dependencies]
 pyasn1 = ">=0.3.7"
@@ -1732,7 +1778,7 @@ description = "Alternative regular expression module, to replace re."
 name = "regex"
 optional = false
 python-versions = "*"
-version = "2020.5.14"
+version = "2020.7.14"
 
 [[package]]
 category = "main"
@@ -1740,7 +1786,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"
@@ -1792,7 +1838,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"
@@ -1851,7 +1897,7 @@ description = "Python documentation generator"
 name = "sphinx"
 optional = false
 python-versions = ">=3.5"
-version = "3.0.4"
+version = "3.1.2"
 
 [package.dependencies]
 Jinja2 = ">=2.3"
@@ -1874,7 +1920,7 @@ sphinxcontrib-serializinghtml = "*"
 
 [package.extras]
 docs = ["sphinxcontrib-websupport"]
-lint = ["flake8 (>=3.5.0)", "flake8-import-order", "mypy (>=0.770)", "docutils-stubs"]
+lint = ["flake8 (>=3.5.0)", "flake8-import-order", "mypy (>=0.780)", "docutils-stubs"]
 test = ["pytest", "pytest-cov", "html5lib", "typed-ast", "cython"]
 
 [[package]]
@@ -1883,10 +1929,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"]
@@ -1984,12 +2030,15 @@ category = "dev"
 description = "Manage dynamic plugins for Python applications"
 name = "stevedore"
 optional = false
-python-versions = "*"
-version = "1.32.0"
+python-versions = ">=3.6"
+version = "3.2.0"
 
 [package.dependencies]
 pbr = ">=2.0.0,<2.1.0 || >2.1.0"
-six = ">=1.10.0"
+
+[package.dependencies.importlib-metadata]
+python = "<3.8"
+version = ">=1.7.0"
 
 [[package]]
 category = "dev"
@@ -2060,7 +2109,7 @@ description = "Fast, Extensible Progress Meter"
 name = "tqdm"
 optional = false
 python-versions = ">=2.6, !=3.0.*, !=3.1.*"
-version = "4.46.1"
+version = "4.48.0"
 
 [package.extras]
 dev = ["py-make (>=0.1.0)", "twine", "argopt", "pydoc-markdown"]
@@ -2071,7 +2120,7 @@ description = "Twilio API client and TwiML generator"
 name = "twilio"
 optional = false
 python-versions = "*"
-version = "6.41.0"
+version = "6.44.1"
 
 [package.dependencies]
 PyJWT = ">=1.4.2"
@@ -2104,7 +2153,7 @@ description = "HTTP library with thread-safe connection pooling, file post, and
 name = "urllib3"
 optional = false
 python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, <4"
-version = "1.25.9"
+version = "1.25.10"
 
 [package.extras]
 brotli = ["brotlipy (>=0.6.0)"]
@@ -2125,7 +2174,7 @@ description = "Measures the displayed width of unicode strings in a terminal"
 name = "wcwidth"
 optional = false
 python-versions = "*"
-version = "0.2.3"
+version = "0.2.5"
 
 [[package]]
 category = "main"
@@ -2165,7 +2214,7 @@ celery = ["Celery", "django-celery-results", "django-celery-beat", "django-celer
 ldap = ["django-auth-ldap"]
 
 [metadata]
-content-hash = "0588f210438515f6bbf895f4a11afb9305a95e6c12681e06d06c9152d21f4e60"
+content-hash = "263338d09e0379dbfeef8d5140717e05ffe2dad421deed99db925d8fd382b7f7"
 python-versions = "^3.7"
 
 [metadata.files]
@@ -2182,8 +2231,8 @@ appdirs = [
     {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.4.0-py2.py3-none-any.whl", hash = "sha256:6d1784dea7c0c8d4a5172b6c620f40b6e4cbfdf96d783691f2e1302a7b88e197"},
@@ -2223,54 +2272,58 @@ bleach = [
     {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.4-py2.py3-none-any.whl", hash = "sha256:9ae2e73b93cc7d6b48b56aaf49a68c91752d0ffd7dfdcc47f842ca79a6f13eae"},
-    {file = "celery-4.4.4.tar.gz", hash = "sha256:c2037b6a8463da43b19969a0fc13f9023ceca6352b4dd51be01c66fbbb13647e"},
+    {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.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"},
 ]
 cffi = [
-    {file = "cffi-1.14.0-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:1cae98a7054b5c9391eb3249b86e0e99ab1e02bb0cc0575da191aedadbdf4384"},
-    {file = "cffi-1.14.0-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:cf16e3cf6c0a5fdd9bc10c21687e19d29ad1fe863372b5543deaec1039581a30"},
-    {file = "cffi-1.14.0-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:f2b0fa0c01d8a0c7483afd9f31d7ecf2d71760ca24499c8697aeb5ca37dc090c"},
-    {file = "cffi-1.14.0-cp27-cp27m-win32.whl", hash = "sha256:99f748a7e71ff382613b4e1acc0ac83bf7ad167fb3802e35e90d9763daba4d78"},
-    {file = "cffi-1.14.0-cp27-cp27m-win_amd64.whl", hash = "sha256:c420917b188a5582a56d8b93bdd8e0f6eca08c84ff623a4c16e809152cd35793"},
-    {file = "cffi-1.14.0-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:399aed636c7d3749bbed55bc907c3288cb43c65c4389964ad5ff849b6370603e"},
-    {file = "cffi-1.14.0-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:cab50b8c2250b46fe738c77dbd25ce017d5e6fb35d3407606e7a4180656a5a6a"},
-    {file = "cffi-1.14.0-cp35-cp35m-macosx_10_9_x86_64.whl", hash = "sha256:001bf3242a1bb04d985d63e138230802c6c8d4db3668fb545fb5005ddf5bb5ff"},
-    {file = "cffi-1.14.0-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:e56c744aa6ff427a607763346e4170629caf7e48ead6921745986db3692f987f"},
-    {file = "cffi-1.14.0-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:b8c78301cefcf5fd914aad35d3c04c2b21ce8629b5e4f4e45ae6812e461910fa"},
-    {file = "cffi-1.14.0-cp35-cp35m-win32.whl", hash = "sha256:8c0ffc886aea5df6a1762d0019e9cb05f825d0eec1f520c51be9d198701daee5"},
-    {file = "cffi-1.14.0-cp35-cp35m-win_amd64.whl", hash = "sha256:8a6c688fefb4e1cd56feb6c511984a6c4f7ec7d2a1ff31a10254f3c817054ae4"},
-    {file = "cffi-1.14.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:95cd16d3dee553f882540c1ffe331d085c9e629499ceadfbda4d4fde635f4b7d"},
-    {file = "cffi-1.14.0-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:66e41db66b47d0d8672d8ed2708ba91b2f2524ece3dee48b5dfb36be8c2f21dc"},
-    {file = "cffi-1.14.0-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:028a579fc9aed3af38f4892bdcc7390508adabc30c6af4a6e4f611b0c680e6ac"},
-    {file = "cffi-1.14.0-cp36-cp36m-win32.whl", hash = "sha256:cef128cb4d5e0b3493f058f10ce32365972c554572ff821e175dbc6f8ff6924f"},
-    {file = "cffi-1.14.0-cp36-cp36m-win_amd64.whl", hash = "sha256:337d448e5a725bba2d8293c48d9353fc68d0e9e4088d62a9571def317797522b"},
-    {file = "cffi-1.14.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:e577934fc5f8779c554639376beeaa5657d54349096ef24abe8c74c5d9c117c3"},
-    {file = "cffi-1.14.0-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:62ae9af2d069ea2698bf536dcfe1e4eed9090211dbaafeeedf5cb6c41b352f66"},
-    {file = "cffi-1.14.0-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:14491a910663bf9f13ddf2bc8f60562d6bc5315c1f09c704937ef17293fb85b0"},
-    {file = "cffi-1.14.0-cp37-cp37m-win32.whl", hash = "sha256:c43866529f2f06fe0edc6246eb4faa34f03fe88b64a0a9a942561c8e22f4b71f"},
-    {file = "cffi-1.14.0-cp37-cp37m-win_amd64.whl", hash = "sha256:2089ed025da3919d2e75a4d963d008330c96751127dd6f73c8dc0c65041b4c26"},
-    {file = "cffi-1.14.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:3b911c2dbd4f423b4c4fcca138cadde747abdb20d196c4a48708b8a2d32b16dd"},
-    {file = "cffi-1.14.0-cp38-cp38-manylinux1_i686.whl", hash = "sha256:7e63cbcf2429a8dbfe48dcc2322d5f2220b77b2e17b7ba023d6166d84655da55"},
-    {file = "cffi-1.14.0-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:3d311bcc4a41408cf5854f06ef2c5cab88f9fded37a3b95936c9879c1640d4c2"},
-    {file = "cffi-1.14.0-cp38-cp38-win32.whl", hash = "sha256:675686925a9fb403edba0114db74e741d8181683dcf216be697d208857e04ca8"},
-    {file = "cffi-1.14.0-cp38-cp38-win_amd64.whl", hash = "sha256:00789914be39dffba161cfc5be31b55775de5ba2235fe49aa28c148236c4e06b"},
-    {file = "cffi-1.14.0.tar.gz", hash = "sha256:2d384f4a127a15ba701207f7639d94106693b6cd64173d6c8988e2c25f3ac2b6"},
+    {file = "cffi-1.14.1-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:66dd45eb9530e3dde8f7c009f84568bc7cac489b93d04ac86e3111fb46e470c2"},
+    {file = "cffi-1.14.1-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:4f53e4128c81ca3212ff4cf097c797ab44646a40b42ec02a891155cd7a2ba4d8"},
+    {file = "cffi-1.14.1-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:833401b15de1bb92791d7b6fb353d4af60dc688eaa521bd97203dcd2d124a7c1"},
+    {file = "cffi-1.14.1-cp27-cp27m-win32.whl", hash = "sha256:26f33e8f6a70c255767e3c3f957ccafc7f1f706b966e110b855bfe944511f1f9"},
+    {file = "cffi-1.14.1-cp27-cp27m-win_amd64.whl", hash = "sha256:b87dfa9f10a470eee7f24234a37d1d5f51e5f5fa9eeffda7c282e2b8f5162eb1"},
+    {file = "cffi-1.14.1-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:effd2ba52cee4ceff1a77f20d2a9f9bf8d50353c854a282b8760ac15b9833168"},
+    {file = "cffi-1.14.1-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:bac0d6f7728a9cc3c1e06d4fcbac12aaa70e9379b3025b27ec1226f0e2d404cf"},
+    {file = "cffi-1.14.1-cp35-cp35m-macosx_10_9_x86_64.whl", hash = "sha256:d6033b4ffa34ef70f0b8086fd4c3df4bf801fee485a8a7d4519399818351aa8e"},
+    {file = "cffi-1.14.1-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:8416ed88ddc057bab0526d4e4e9f3660f614ac2394b5e019a628cdfff3733849"},
+    {file = "cffi-1.14.1-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:892daa86384994fdf4856cb43c93f40cbe80f7f95bb5da94971b39c7f54b3a9c"},
+    {file = "cffi-1.14.1-cp35-cp35m-win32.whl", hash = "sha256:c991112622baee0ae4d55c008380c32ecfd0ad417bcd0417ba432e6ba7328caa"},
+    {file = "cffi-1.14.1-cp35-cp35m-win_amd64.whl", hash = "sha256:fcf32bf76dc25e30ed793145a57426064520890d7c02866eb93d3e4abe516948"},
+    {file = "cffi-1.14.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:f960375e9823ae6a07072ff7f8a85954e5a6434f97869f50d0e41649a1c8144f"},
+    {file = "cffi-1.14.1-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:a6d28e7f14ecf3b2ad67c4f106841218c8ab12a0683b1528534a6c87d2307af3"},
+    {file = "cffi-1.14.1-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:cda422d54ee7905bfc53ee6915ab68fe7b230cacf581110df4272ee10462aadc"},
+    {file = "cffi-1.14.1-cp36-cp36m-win32.whl", hash = "sha256:4a03416915b82b81af5502459a8a9dd62a3c299b295dcdf470877cb948d655f2"},
+    {file = "cffi-1.14.1-cp36-cp36m-win_amd64.whl", hash = "sha256:4ce1e995aeecf7cc32380bc11598bfdfa017d592259d5da00fc7ded11e61d022"},
+    {file = "cffi-1.14.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:e23cb7f1d8e0f93addf0cae3c5b6f00324cccb4a7949ee558d7b6ca973ab8ae9"},
+    {file = "cffi-1.14.1-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:ddff0b2bd7edcc8c82d1adde6dbbf5e60d57ce985402541cd2985c27f7bec2a0"},
+    {file = "cffi-1.14.1-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:f90c2267101010de42f7273c94a1f026e56cbc043f9330acd8a80e64300aba33"},
+    {file = "cffi-1.14.1-cp37-cp37m-win32.whl", hash = "sha256:3cd2c044517f38d1b577f05927fb9729d3396f1d44d0c659a445599e79519792"},
+    {file = "cffi-1.14.1-cp37-cp37m-win_amd64.whl", hash = "sha256:4fa72a52a906425416f41738728268072d5acfd48cbe7796af07a923236bcf96"},
+    {file = "cffi-1.14.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:267adcf6e68d77ba154334a3e4fc921b8e63cbb38ca00d33d40655d4228502bc"},
+    {file = "cffi-1.14.1-cp38-cp38-manylinux1_i686.whl", hash = "sha256:d3148b6ba3923c5850ea197a91a42683f946dba7e8eb82dfa211ab7e708de939"},
+    {file = "cffi-1.14.1-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:98be759efdb5e5fa161e46d404f4e0ce388e72fbf7d9baf010aff16689e22abe"},
+    {file = "cffi-1.14.1-cp38-cp38-win32.whl", hash = "sha256:6923d077d9ae9e8bacbdb1c07ae78405a9306c8fd1af13bfa06ca891095eb995"},
+    {file = "cffi-1.14.1-cp38-cp38-win_amd64.whl", hash = "sha256:b1d6ebc891607e71fd9da71688fcf332a6630b7f5b7f5549e6e631821c0e5d90"},
+    {file = "cffi-1.14.1.tar.gz", hash = "sha256:b2a2b0d276a136146e012154baefaea2758ef1f56ae9f4e01c612b0831e0bd2f"},
 ]
 chardet = [
     {file = "chardet-3.0.4-py2.py3-none-any.whl", hash = "sha256:fc323ffcaeaed0e0a02bf4d117757b98aed530d9ed4531e3e15460124c106691"},
@@ -2292,66 +2345,69 @@ configobj = [
     {file = "configobj-5.0.6.tar.gz", hash = "sha256:a2f5650770e1c87fb335af19a9b7eb73fc05ccf22144eb68db7d00cd2bcb0902"},
 ]
 coverage = [
-    {file = "coverage-5.1-cp27-cp27m-macosx_10_12_x86_64.whl", hash = "sha256:0cb4be7e784dcdc050fc58ef05b71aa8e89b7e6636b99967fadbdba694cf2b65"},
-    {file = "coverage-5.1-cp27-cp27m-macosx_10_13_intel.whl", hash = "sha256:c317eaf5ff46a34305b202e73404f55f7389ef834b8dbf4da09b9b9b37f76dd2"},
-    {file = "coverage-5.1-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:b83835506dfc185a319031cf853fa4bb1b3974b1f913f5bb1a0f3d98bdcded04"},
-    {file = "coverage-5.1-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:5f2294dbf7875b991c381e3d5af2bcc3494d836affa52b809c91697449d0eda6"},
-    {file = "coverage-5.1-cp27-cp27m-win32.whl", hash = "sha256:de807ae933cfb7f0c7d9d981a053772452217df2bf38e7e6267c9cbf9545a796"},
-    {file = "coverage-5.1-cp27-cp27m-win_amd64.whl", hash = "sha256:bf9cb9a9fd8891e7efd2d44deb24b86d647394b9705b744ff6f8261e6f29a730"},
-    {file = "coverage-5.1-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:acf3763ed01af8410fc36afea23707d4ea58ba7e86a8ee915dfb9ceff9ef69d0"},
-    {file = "coverage-5.1-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:dec5202bfe6f672d4511086e125db035a52b00f1648d6407cc8e526912c0353a"},
-    {file = "coverage-5.1-cp35-cp35m-macosx_10_12_x86_64.whl", hash = "sha256:7a5bdad4edec57b5fb8dae7d3ee58622d626fd3a0be0dfceda162a7035885ecf"},
-    {file = "coverage-5.1-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:1601e480b9b99697a570cea7ef749e88123c04b92d84cedaa01e117436b4a0a9"},
-    {file = "coverage-5.1-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:dbe8c6ae7534b5b024296464f387d57c13caa942f6d8e6e0346f27e509f0f768"},
-    {file = "coverage-5.1-cp35-cp35m-win32.whl", hash = "sha256:a027ef0492ede1e03a8054e3c37b8def89a1e3c471482e9f046906ba4f2aafd2"},
-    {file = "coverage-5.1-cp35-cp35m-win_amd64.whl", hash = "sha256:0e61d9803d5851849c24f78227939c701ced6704f337cad0a91e0972c51c1ee7"},
-    {file = "coverage-5.1-cp36-cp36m-macosx_10_13_x86_64.whl", hash = "sha256:2d27a3f742c98e5c6b461ee6ef7287400a1956c11421eb574d843d9ec1f772f0"},
-    {file = "coverage-5.1-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:66460ab1599d3cf894bb6baee8c684788819b71a5dc1e8fa2ecc152e5d752019"},
-    {file = "coverage-5.1-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:5c542d1e62eece33c306d66fe0a5c4f7f7b3c08fecc46ead86d7916684b36d6c"},
-    {file = "coverage-5.1-cp36-cp36m-win32.whl", hash = "sha256:2742c7515b9eb368718cd091bad1a1b44135cc72468c731302b3d641895b83d1"},
-    {file = "coverage-5.1-cp36-cp36m-win_amd64.whl", hash = "sha256:dead2ddede4c7ba6cb3a721870f5141c97dc7d85a079edb4bd8d88c3ad5b20c7"},
-    {file = "coverage-5.1-cp37-cp37m-macosx_10_13_x86_64.whl", hash = "sha256:01333e1bd22c59713ba8a79f088b3955946e293114479bbfc2e37d522be03355"},
-    {file = "coverage-5.1-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:e1ea316102ea1e1770724db01998d1603ed921c54a86a2efcb03428d5417e489"},
-    {file = "coverage-5.1-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:adeb4c5b608574a3d647011af36f7586811a2c1197c861aedb548dd2453b41cd"},
-    {file = "coverage-5.1-cp37-cp37m-win32.whl", hash = "sha256:782caea581a6e9ff75eccda79287daefd1d2631cc09d642b6ee2d6da21fc0a4e"},
-    {file = "coverage-5.1-cp37-cp37m-win_amd64.whl", hash = "sha256:00f1d23f4336efc3b311ed0d807feb45098fc86dee1ca13b3d6768cdab187c8a"},
-    {file = "coverage-5.1-cp38-cp38-macosx_10_13_x86_64.whl", hash = "sha256:402e1744733df483b93abbf209283898e9f0d67470707e3c7516d84f48524f55"},
-    {file = "coverage-5.1-cp38-cp38-manylinux1_i686.whl", hash = "sha256:a3f3654d5734a3ece152636aad89f58afc9213c6520062db3978239db122f03c"},
-    {file = "coverage-5.1-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:6402bd2fdedabbdb63a316308142597534ea8e1895f4e7d8bf7476c5e8751fef"},
-    {file = "coverage-5.1-cp38-cp38-win32.whl", hash = "sha256:8fa0cbc7ecad630e5b0f4f35b0f6ad419246b02bc750de7ac66db92667996d24"},
-    {file = "coverage-5.1-cp38-cp38-win_amd64.whl", hash = "sha256:79a3cfd6346ce6c13145731d39db47b7a7b859c0272f02cdb89a3bdcbae233a0"},
-    {file = "coverage-5.1-cp39-cp39-win32.whl", hash = "sha256:a82b92b04a23d3c8a581fc049228bafde988abacba397d57ce95fe95e0338ab4"},
-    {file = "coverage-5.1-cp39-cp39-win_amd64.whl", hash = "sha256:bb28a7245de68bf29f6fb199545d072d1036a1917dca17a1e75bbb919e14ee8e"},
-    {file = "coverage-5.1.tar.gz", hash = "sha256:f90bfc4ad18450c80b024036eaf91e4a246ae287701aaa88eaebebf150868052"},
+    {file = "coverage-5.2.1-cp27-cp27m-macosx_10_13_intel.whl", hash = "sha256:40f70f81be4d34f8d491e55936904db5c527b0711b2a46513641a5729783c2e4"},
+    {file = "coverage-5.2.1-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:675192fca634f0df69af3493a48224f211f8db4e84452b08d5fcebb9167adb01"},
+    {file = "coverage-5.2.1-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:2fcc8b58953d74d199a1a4d633df8146f0ac36c4e720b4a1997e9b6327af43a8"},
+    {file = "coverage-5.2.1-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:64c4f340338c68c463f1b56e3f2f0423f7b17ba6c3febae80b81f0e093077f59"},
+    {file = "coverage-5.2.1-cp27-cp27m-win32.whl", hash = "sha256:52f185ffd3291196dc1aae506b42e178a592b0b60a8610b108e6ad892cfc1bb3"},
+    {file = "coverage-5.2.1-cp27-cp27m-win_amd64.whl", hash = "sha256:30bc103587e0d3df9e52cd9da1dd915265a22fad0b72afe54daf840c984b564f"},
+    {file = "coverage-5.2.1-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:9ea749fd447ce7fb1ac71f7616371f04054d969d412d37611716721931e36efd"},
+    {file = "coverage-5.2.1-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:ce7866f29d3025b5b34c2e944e66ebef0d92e4a4f2463f7266daa03a1332a651"},
+    {file = "coverage-5.2.1-cp35-cp35m-macosx_10_13_x86_64.whl", hash = "sha256:4869ab1c1ed33953bb2433ce7b894a28d724b7aa76c19b11e2878034a4e4680b"},
+    {file = "coverage-5.2.1-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:a3ee9c793ffefe2944d3a2bd928a0e436cd0ac2d9e3723152d6fd5398838ce7d"},
+    {file = "coverage-5.2.1-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:28f42dc5172ebdc32622a2c3f7ead1b836cdbf253569ae5673f499e35db0bac3"},
+    {file = "coverage-5.2.1-cp35-cp35m-win32.whl", hash = "sha256:e26c993bd4b220429d4ec8c1468eca445a4064a61c74ca08da7429af9bc53bb0"},
+    {file = "coverage-5.2.1-cp35-cp35m-win_amd64.whl", hash = "sha256:4186fc95c9febeab5681bc3248553d5ec8c2999b8424d4fc3a39c9cba5796962"},
+    {file = "coverage-5.2.1-cp36-cp36m-macosx_10_13_x86_64.whl", hash = "sha256:b360d8fd88d2bad01cb953d81fd2edd4be539df7bfec41e8753fe9f4456a5082"},
+    {file = "coverage-5.2.1-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:1adb6be0dcef0cf9434619d3b892772fdb48e793300f9d762e480e043bd8e716"},
+    {file = "coverage-5.2.1-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:098a703d913be6fbd146a8c50cc76513d726b022d170e5e98dc56d958fd592fb"},
+    {file = "coverage-5.2.1-cp36-cp36m-win32.whl", hash = "sha256:962c44070c281d86398aeb8f64e1bf37816a4dfc6f4c0f114756b14fc575621d"},
+    {file = "coverage-5.2.1-cp36-cp36m-win_amd64.whl", hash = "sha256:b1ed2bdb27b4c9fc87058a1cb751c4df8752002143ed393899edb82b131e0546"},
+    {file = "coverage-5.2.1-cp37-cp37m-macosx_10_13_x86_64.whl", hash = "sha256:c890728a93fffd0407d7d37c1e6083ff3f9f211c83b4316fae3778417eab9811"},
+    {file = "coverage-5.2.1-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:538f2fd5eb64366f37c97fdb3077d665fa946d2b6d95447622292f38407f9258"},
+    {file = "coverage-5.2.1-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:27ca5a2bc04d68f0776f2cdcb8bbd508bbe430a7bf9c02315cd05fb1d86d0034"},
+    {file = "coverage-5.2.1-cp37-cp37m-win32.whl", hash = "sha256:aab75d99f3f2874733946a7648ce87a50019eb90baef931698f96b76b6769a46"},
+    {file = "coverage-5.2.1-cp37-cp37m-win_amd64.whl", hash = "sha256:c2ff24df02a125b7b346c4c9078c8936da06964cc2d276292c357d64378158f8"},
+    {file = "coverage-5.2.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:304fbe451698373dc6653772c72c5d5e883a4aadaf20343592a7abb2e643dae0"},
+    {file = "coverage-5.2.1-cp38-cp38-manylinux1_i686.whl", hash = "sha256:c96472b8ca5dc135fb0aa62f79b033f02aa434fb03a8b190600a5ae4102df1fd"},
+    {file = "coverage-5.2.1-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:8505e614c983834239f865da2dd336dcf9d72776b951d5dfa5ac36b987726e1b"},
+    {file = "coverage-5.2.1-cp38-cp38-win32.whl", hash = "sha256:700997b77cfab016533b3e7dbc03b71d33ee4df1d79f2463a318ca0263fc29dd"},
+    {file = "coverage-5.2.1-cp38-cp38-win_amd64.whl", hash = "sha256:46794c815e56f1431c66d81943fa90721bb858375fb36e5903697d5eef88627d"},
+    {file = "coverage-5.2.1-cp39-cp39-macosx_10_13_x86_64.whl", hash = "sha256:16042dc7f8e632e0dcd5206a5095ebd18cb1d005f4c89694f7f8aafd96dd43a3"},
+    {file = "coverage-5.2.1-cp39-cp39-manylinux1_i686.whl", hash = "sha256:c1bbb628ed5192124889b51204de27c575b3ffc05a5a91307e7640eff1d48da4"},
+    {file = "coverage-5.2.1-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:4f6428b55d2916a69f8d6453e48a505c07b2245653b0aa9f0dee38785939f5e4"},
+    {file = "coverage-5.2.1-cp39-cp39-win32.whl", hash = "sha256:9e536783a5acee79a9b308be97d3952b662748c4037b6a24cbb339dc7ed8eb89"},
+    {file = "coverage-5.2.1-cp39-cp39-win_amd64.whl", hash = "sha256:b8f58c7db64d8f27078cbf2a4391af6aa4e4767cc08b37555c4ae064b8558d9b"},
+    {file = "coverage-5.2.1.tar.gz", hash = "sha256:a34cb28e0747ea15e82d13e14de606747e9e484fb28d63c999483f5d5188e89b"},
 ]
 cryptography = [
-    {file = "cryptography-2.9.2-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:daf54a4b07d67ad437ff239c8a4080cfd1cc7213df57d33c97de7b4738048d5e"},
-    {file = "cryptography-2.9.2-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:3b3eba865ea2754738616f87292b7f29448aec342a7c720956f8083d252bf28b"},
-    {file = "cryptography-2.9.2-cp27-cp27m-manylinux2010_x86_64.whl", hash = "sha256:c447cf087cf2dbddc1add6987bbe2f767ed5317adb2d08af940db517dd704365"},
-    {file = "cryptography-2.9.2-cp27-cp27m-win32.whl", hash = "sha256:f118a95c7480f5be0df8afeb9a11bd199aa20afab7a96bcf20409b411a3a85f0"},
-    {file = "cryptography-2.9.2-cp27-cp27m-win_amd64.whl", hash = "sha256:c4fd17d92e9d55b84707f4fd09992081ba872d1a0c610c109c18e062e06a2e55"},
-    {file = "cryptography-2.9.2-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:d0d5aeaedd29be304848f1c5059074a740fa9f6f26b84c5b63e8b29e73dfc270"},
-    {file = "cryptography-2.9.2-cp27-cp27mu-manylinux2010_x86_64.whl", hash = "sha256:1e4014639d3d73fbc5ceff206049c5a9a849cefd106a49fa7aaaa25cc0ce35cf"},
-    {file = "cryptography-2.9.2-cp35-abi3-macosx_10_9_x86_64.whl", hash = "sha256:96c080ae7118c10fcbe6229ab43eb8b090fccd31a09ef55f83f690d1ef619a1d"},
-    {file = "cryptography-2.9.2-cp35-abi3-manylinux1_x86_64.whl", hash = "sha256:e993468c859d084d5579e2ebee101de8f5a27ce8e2159959b6673b418fd8c785"},
-    {file = "cryptography-2.9.2-cp35-abi3-manylinux2010_x86_64.whl", hash = "sha256:88c881dd5a147e08d1bdcf2315c04972381d026cdb803325c03fe2b4a8ed858b"},
-    {file = "cryptography-2.9.2-cp35-cp35m-win32.whl", hash = "sha256:651448cd2e3a6bc2bb76c3663785133c40d5e1a8c1a9c5429e4354201c6024ae"},
-    {file = "cryptography-2.9.2-cp35-cp35m-win_amd64.whl", hash = "sha256:726086c17f94747cedbee6efa77e99ae170caebeb1116353c6cf0ab67ea6829b"},
-    {file = "cryptography-2.9.2-cp36-cp36m-win32.whl", hash = "sha256:091d31c42f444c6f519485ed528d8b451d1a0c7bf30e8ca583a0cac44b8a0df6"},
-    {file = "cryptography-2.9.2-cp36-cp36m-win_amd64.whl", hash = "sha256:bb1f0281887d89617b4c68e8db9a2c42b9efebf2702a3c5bf70599421a8623e3"},
-    {file = "cryptography-2.9.2-cp37-cp37m-win32.whl", hash = "sha256:18452582a3c85b96014b45686af264563e3e5d99d226589f057ace56196ec78b"},
-    {file = "cryptography-2.9.2-cp37-cp37m-win_amd64.whl", hash = "sha256:22e91636a51170df0ae4dcbd250d318fd28c9f491c4e50b625a49964b24fe46e"},
-    {file = "cryptography-2.9.2-cp38-cp38-win32.whl", hash = "sha256:844a76bc04472e5135b909da6aed84360f522ff5dfa47f93e3dd2a0b84a89fa0"},
-    {file = "cryptography-2.9.2-cp38-cp38-win_amd64.whl", hash = "sha256:1dfa985f62b137909496e7fc182dac687206d8d089dd03eaeb28ae16eec8e7d5"},
-    {file = "cryptography-2.9.2.tar.gz", hash = "sha256:a0c30272fb4ddda5f5ffc1089d7405b7a71b0b0f51993cb4e5dbb4590b2fc229"},
+    {file = "cryptography-3.0-cp27-cp27m-macosx_10_10_x86_64.whl", hash = "sha256:ab49edd5bea8d8b39a44b3db618e4783ef84c19c8b47286bf05dfdb3efb01c83"},
+    {file = "cryptography-3.0-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:124af7255ffc8e964d9ff26971b3a6153e1a8a220b9a685dc407976ecb27a06a"},
+    {file = "cryptography-3.0-cp27-cp27m-manylinux2010_x86_64.whl", hash = "sha256:51e40123083d2f946794f9fe4adeeee2922b581fa3602128ce85ff813d85b81f"},
+    {file = "cryptography-3.0-cp27-cp27m-win32.whl", hash = "sha256:dea0ba7fe6f9461d244679efa968d215ea1f989b9c1957d7f10c21e5c7c09ad6"},
+    {file = "cryptography-3.0-cp27-cp27m-win_amd64.whl", hash = "sha256:8ecf9400d0893836ff41b6f977a33972145a855b6efeb605b49ee273c5e6469f"},
+    {file = "cryptography-3.0-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:0c608ff4d4adad9e39b5057de43657515c7da1ccb1807c3a27d4cf31fc923b4b"},
+    {file = "cryptography-3.0-cp27-cp27mu-manylinux2010_x86_64.whl", hash = "sha256:bec7568c6970b865f2bcebbe84d547c52bb2abadf74cefce396ba07571109c67"},
+    {file = "cryptography-3.0-cp35-abi3-macosx_10_10_x86_64.whl", hash = "sha256:0cbfed8ea74631fe4de00630f4bb592dad564d57f73150d6f6796a24e76c76cd"},
+    {file = "cryptography-3.0-cp35-abi3-manylinux1_x86_64.whl", hash = "sha256:a09fd9c1cca9a46b6ad4bea0a1f86ab1de3c0c932364dbcf9a6c2a5eeb44fa77"},
+    {file = "cryptography-3.0-cp35-abi3-manylinux2010_x86_64.whl", hash = "sha256:ce82cc06588e5cbc2a7df3c8a9c778f2cb722f56835a23a68b5a7264726bb00c"},
+    {file = "cryptography-3.0-cp35-cp35m-win32.whl", hash = "sha256:9367d00e14dee8d02134c6c9524bb4bd39d4c162456343d07191e2a0b5ec8b3b"},
+    {file = "cryptography-3.0-cp35-cp35m-win_amd64.whl", hash = "sha256:384d7c681b1ab904fff3400a6909261cae1d0939cc483a68bdedab282fb89a07"},
+    {file = "cryptography-3.0-cp36-cp36m-win32.whl", hash = "sha256:4d355f2aee4a29063c10164b032d9fa8a82e2c30768737a2fd56d256146ad559"},
+    {file = "cryptography-3.0-cp36-cp36m-win_amd64.whl", hash = "sha256:45741f5499150593178fc98d2c1a9c6722df88b99c821ad6ae298eff0ba1ae71"},
+    {file = "cryptography-3.0-cp37-cp37m-win32.whl", hash = "sha256:8ecef21ac982aa78309bb6f092d1677812927e8b5ef204a10c326fc29f1367e2"},
+    {file = "cryptography-3.0-cp37-cp37m-win_amd64.whl", hash = "sha256:4b9303507254ccb1181d1803a2080a798910ba89b1a3c9f53639885c90f7a756"},
+    {file = "cryptography-3.0-cp38-cp38-win32.whl", hash = "sha256:8713ddb888119b0d2a1462357d5946b8911be01ddbf31451e1d07eaa5077a261"},
+    {file = "cryptography-3.0-cp38-cp38-win_amd64.whl", hash = "sha256:bea0b0468f89cdea625bb3f692cd7a4222d80a6bdafd6fb923963f2b9da0e15f"},
+    {file = "cryptography-3.0.tar.gz", hash = "sha256:8e924dbc025206e97756e8903039662aa58aa9ba357d8e1d8fc29e3092322053"},
 ]
 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.7-py3-none-any.whl", hash = "sha256:e1630333248c9b3d4e38f02093a26f1e07b271ca896d73097457996e0fae12e8"},
-    {file = "Django-3.0.7.tar.gz", hash = "sha256:5052b34b34b3425233c682e0e11d658fd6efd587d11335a0203d827224ada8f2"},
+    {file = "Django-3.0.8-py3-none-any.whl", hash = "sha256:5457fc953ec560c5521b41fad9e6734a4668b7ba205832191bbdff40ec61073c"},
+    {file = "Django-3.0.8.tar.gz", hash = "sha256:31a5fbbea5fc71c99e288ec0b2f00302a0a92c44b13ede80b73a6a4d6d205582"},
 ]
 django-any-js = [
     {file = "django-any-js-1.0.3.post0.tar.gz", hash = "sha256:1da88b44b861b0f54f6b8ea0eb4c7c4fa1a5772e9a4320532cd4e0871a4e23f7"},
@@ -2393,8 +2449,8 @@ django-ckeditor = [
     {file = "django_ckeditor-5.9.0-py2.py3-none-any.whl", hash = "sha256:71c3c7bb46b0cbfb9712ef64af0d2a406eab233f44ecd7c42c24bdfa39ae3bde"},
 ]
 django-colorfield = [
-    {file = "django-colorfield-0.3.0.tar.gz", hash = "sha256:808fd1783be0331dc15f0d4e98d18e3b31257b4837ac89594b94f87170d6c6ce"},
-    {file = "django_colorfield-0.3.0-py2-none-any.whl", hash = "sha256:7bec0c7a8b3b170bf232f78ed23d2545ddc9c43cb2f45704ccdd57b948b29d8c"},
+    {file = "django-colorfield-0.3.2.tar.gz", hash = "sha256:f5dde281f4db8871eb5845aee614b4f1a47e7fd5b20476238793f519cd7bdf41"},
+    {file = "django_colorfield-0.3.2-py2-none-any.whl", hash = "sha256:e435ec31712f5e8b955cc7633aef1e49cc3b409c21dfcefeb2f6ef0e1cb69533"},
 ]
 django-dbbackup = [
     {file = "django-dbbackup-3.3.0.tar.gz", hash = "sha256:bb109735cae98b64ad084e5b461b7aca2d7b39992f10c9ed9435e3ebb6fb76c8"},
@@ -2404,28 +2460,28 @@ django-debug-toolbar = [
     {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"},
+    {file = "django-dynamic-preferences-1.10.tar.gz", hash = "sha256:2310291c7f40606be045938d65e117383549aa8a979c6c4b700464c6a6204a34"},
+    {file = "django_dynamic_preferences-1.10-py2.py3-none-any.whl", hash = "sha256:d5852c720c1989a67d87669035e11f6c033e7a507de6ec9bd28941cba24a2dc4"},
 ]
 django-easy-audit = [
-    {file = "django-easy-audit-1.2.3a4.tar.gz", hash = "sha256:55a6512c012fcffc47bca38376d775d15d44d24e823682ea59418c4edabe8f54"},
-    {file = "django_easy_audit-1.2.3a4-py3-none-any.whl", hash = "sha256:37c90a273559ba003d691fa0c30ee5ff792b7739d13953f7e8923c954480240f"},
+    {file = "django-easy-audit-1.3.0a3.tar.gz", hash = "sha256:80e9ed3f11d8927ecd5fa7e03e3aeebd87729d6feba60f9b26b554237dbe3508"},
+    {file = "django_easy_audit-1.3.0a3-py3-none-any.whl", hash = "sha256:2b3d067d81392bb18d2b6bb09e67ce4367e73f767d3f4e067af010d496dc657c"},
 ]
 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.2.0.tar.gz", hash = "sha256:8cacf49ebcc1e545f0a8997971eec0fe109f5ed31fc2a569a7bf5615453696e2"},
-    {file = "django_guardian-2.2.0-py3-none-any.whl", hash = "sha256:ac81e88372fdf1795d84ba065550e739b42e9c6d07cdf201cf5bbf9efa7f396c"},
+    {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"},
@@ -2435,15 +2491,19 @@ 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"},
+    {file = "django-ipware-3.0.0.tar.gz", hash = "sha256:161605eb011439550dd3ee496d0e999720b13f01952be25ea9e88982fbe48e83"},
 ]
 django-js-asset = [
     {file = "django-js-asset-1.2.2.tar.gz", hash = "sha256:c163ae80d2e0b22d8fb598047cd0dcef31f81830e127cfecae278ad574167260"},
@@ -2471,12 +2531,12 @@ django-middleware-global-request = [
     {file = "django-middleware-global-request-0.1.2.tar.gz", hash = "sha256:f6490759bc9f7dbde4001709554e29ca715daf847f2222914b4e47117dca9313"},
 ]
 django-otp = [
-    {file = "django-otp-0.9.1.tar.gz", hash = "sha256:f456639addace8b6d1eb77f9edaada1a53dbb4d6f3c19f17c476c4e3e4beb73f"},
-    {file = "django_otp-0.9.1-py3-none-any.whl", hash = "sha256:0c67cf6f4bd6fca84027879ace9049309213b6ac81f88e954376a6b5535d96c4"},
+    {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"},
+    {file = "django-otp-yubikey-0.6.0.tar.gz", hash = "sha256:6961f16cfec1dddfa3f3c794ec5967cbb4d412f488de1d7d14441df462ba9843"},
+    {file = "django_otp_yubikey-0.6.0-py2.py3-none-any.whl", hash = "sha256:0dd73c2145afc66f43d537d2789a7068a99bc05b5ffa33d2ad73a49e4c4c7dcb"},
 ]
 django-phonenumber-field = [
     {file = "django-phonenumber-field-3.0.1.tar.gz", hash = "sha256:794ebbc3068a7af75aa72a80cb0cec67e714ff8409a965968040f1fd210b2d97"},
@@ -2487,11 +2547,11 @@ django-polymorphic = [
     {file = "django_polymorphic-2.1.2-py2.py3-none-any.whl", hash = "sha256:0a25058e95e5e99fe0beeabb8f4734effe242d7b5b77dca416fba9fd3062da6a"},
 ]
 django-pwa = [
-    {file = "django-pwa-1.0.9.tar.gz", hash = "sha256:c11bcb40bbbb65f9037e4ae4d7233e6fa724c4410419b257cce4b6624a9542e9"},
-    {file = "django_pwa-1.0.9-py3-none-any.whl", hash = "sha256:8706cbd84489fb63d3523d5037d2cdfd8ff177417292bd7845b0f177d3c4ed3f"},
+    {file = "django-pwa-1.0.10.tar.gz", hash = "sha256:07ed9dd57108838e3fe44b551a82032ca4ed76e31cb3c3e8d51604e0fe7e81e9"},
+    {file = "django_pwa-1.0.10-py3-none-any.whl", hash = "sha256:b1a2057b1e72c40c3a14beb90b958482da185f1d40a141fcae3d76580984b930"},
 ]
 django-render-block = [
-    {file = "django_render_block-0.6-py2.py3-none-any.whl", hash = "sha256:95c7dc9610378a10e0c4a10d8364ec7307210889afccd6a67a6aaa0fd599bd4d"},
+    {file = "django_render_block-0.7-py3-none-any.whl", hash = "sha256:3e5963a2332727ca0db2bb8ca031404fba3ac9024702446eed5b7fb02209f7f3"},
 ]
 django-reversion = [
     {file = "django-reversion-3.0.7.tar.gz", hash = "sha256:72fc53580a6b538f0cfff10f27f42333f67d79c406399289c94ec5a193cfb3e1"},
@@ -2523,8 +2583,8 @@ django-timezone-field = [
     {file = "django_timezone_field-4.0-py3-none-any.whl", hash = "sha256:758b7d41084e9ea2e89e59eb616e9b6326e6fbbf9d14b6ef062d624fe8cc6246"},
 ]
 django-two-factor-auth = [
-    {file = "django-two-factor-auth-1.11.0.tar.gz", hash = "sha256:637bd96e76907d044206c3b038bf4b5eb4769e9e3a4718da755fafc97db38b26"},
-    {file = "django_two_factor_auth-1.11.0-py2.py3-none-any.whl", hash = "sha256:f1835e5368448d1b3d826ce7a36c590b3e39cd536523d372cfc5f9fbdc9731f4"},
+    {file = "django-two-factor-auth-1.12.1.tar.gz", hash = "sha256:8e698d548a5a7c02c7ba343bc5376a7bbdc4e59c20ef13223743fe42fa4a1281"},
+    {file = "django_two_factor_auth-1.12.1-py2.py3-none-any.whl", hash = "sha256:612adb0dd6e9ed3b4ecd6763f2e3f56358d7b5afb843a3a49994d1d3bc91ffc2"},
 ]
 django-widget-tweaks = [
     {file = "django-widget-tweaks-1.4.8.tar.gz", hash = "sha256:9f91ca4217199b7671971d3c1f323a2bec71a0c27dec6260b3c006fa541bc489"},
@@ -2549,18 +2609,18 @@ easy-thumbnails = [
     {file = "easy-thumbnails-2.7.tar.gz", hash = "sha256:e4e7a0dd4001f56bfd4058428f2c91eafe27d33ef3b8b33ac4e013b159b9ff91"},
 ]
 faker = [
-    {file = "Faker-4.1.0-py3-none-any.whl", hash = "sha256:34ae397aef03a0a17910452f1e8430d57fa59e2d67b20e9b637218e8f7dd22b3"},
-    {file = "Faker-4.1.0.tar.gz", hash = "sha256:103c46b9701a151299c5bffe6fefcd4fb5fb04c3b5d06bee4952d36255d44ea2"},
+    {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.8.2-py2.py3-none-any.whl", hash = "sha256:ccaa799ef9893cebe69fdfefed76865aeaefbb94cb8545617b2298786a4de9a5"},
-    {file = "flake8-3.8.2.tar.gz", hash = "sha256:c69ac1668e434d37a2d2880b3ca9aafd54b3a10a3ac1ab101d22f29e29cf8634"},
+    {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.2.0.tar.gz", hash = "sha256:10e7ff9f81f637a9471684e5624d6a32b11cba362b38df4e20fc8f761184440b"},
+    {file = "flake8-black-0.2.1.tar.gz", hash = "sha256:f26651bc10db786c03f4093414f7c9ea982ed8a244cec323c984feeffdf4c118"},
 ]
 flake8-builtins = [
     {file = "flake8-builtins-1.5.3.tar.gz", hash = "sha256:09998853b2405e98e61d2ff3027c47033adbdc17f9fe44ca58443d876eb00f3b"},
@@ -2593,41 +2653,44 @@ 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.5-py3-none-any.whl", hash = "sha256:91f36bfb1ab7949b3b40e23736db18231bf7593edada2ba5c3a174a7b23657ac"},
     {file = "gitdb-4.0.5.tar.gz", hash = "sha256:c9e1f2d0db7ddb9a704c2a0217be31214e91a4fe1dea1efad19ae42ba0c285c9"},
 ]
 gitpython = [
-    {file = "GitPython-3.1.3-py3-none-any.whl", hash = "sha256:ef1d60b01b5ce0040ad3ec20bc64f783362d41fa0822a2742d3586e1f49bb8ac"},
-    {file = "GitPython-3.1.3.tar.gz", hash = "sha256:e107af4d873daed64648b4f4beb89f89f0cfbe3ef558fc7821ed2331c2f8da1a"},
+    {file = "GitPython-3.1.7-py3-none-any.whl", hash = "sha256:fa3b92da728a457dd75d62bb5f3eb2816d99a7fe6c67398e260637a40e3fafb5"},
+    {file = "GitPython-3.1.7.tar.gz", hash = "sha256:2db287d71a284e22e5c2846042d0602465c7434d910406990d5b74df4afb0858"},
 ]
 html2text = [
     {file = "html2text-2020.1.16-py3-none-any.whl", hash = "sha256:c7c629882da0cf377d66f073329ccf34a12ed2adf0169b9285ae4e63ef54c82b"},
     {file = "html2text-2020.1.16.tar.gz", hash = "sha256:e296318e16b059ddb97f7a8a1d6a5c1d7af4544049a01e261731d2d5cc277bbb"},
 ]
 idna = [
-    {file = "idna-2.9-py2.py3-none-any.whl", hash = "sha256:a068a21ceac8a4d63dbfd964670474107f541babbd2250d61922f029858365fa"},
-    {file = "idna-2.9.tar.gz", hash = "sha256:7588d1c14ae4c77d74036e8c22ff447b26d0fde8f007354fd48a7814db15b7cb"},
+    {file = "idna-2.10-py2.py3-none-any.whl", hash = "sha256:b97d804b1e9b523befed77c48dacec60e6dcb0b5391d57af6a65a312a90648c0"},
+    {file = "idna-2.10.tar.gz", hash = "sha256:b307872f855b18632ce0c21c5e45be78c0ea7ae4c15c828c20788b26921eb3f6"},
 ]
 imagesize = [
     {file = "imagesize-1.2.0-py2.py3-none-any.whl", hash = "sha256:6965f19a6a2039c7d48bca7dba2473069ff854c36ae6f19d2cde309d998228a1"},
     {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"},
-    {file = "isort-4.3.21.tar.gz", hash = "sha256:54da7e92468955c4fceacd0c86bd0ec997b0e1ee80d97f67c35a78b719dccab1"},
+    {file = "isort-5.2.0-py3-none-any.whl", hash = "sha256:1b0265ff0639af0a6f0f95a44956a985a1256960905aaf57ecf9175356c54ced"},
+    {file = "isort-5.2.0.tar.gz", hash = "sha256:27c7f27adc4b1a6afde1b66c8af46d42da03671d68648e2a8ab2166df03b668e"},
 ]
 jinja2 = [
     {file = "Jinja2-2.11.2-py2.py3-none-any.whl", hash = "sha256:f0a4641d3cf955324a89c04f3d94663aa4d638abe8f733ecd3582848e1c37035"},
     {file = "Jinja2-2.11.2.tar.gz", hash = "sha256:89aab215427ef59c34ad58735269eb58b1a5808103067f7bb9d5836c651b3bb0"},
 ]
 kombu = [
-    {file = "kombu-4.6.10-py2.py3-none-any.whl", hash = "sha256:dc282bb277197d723bccda1a9ba30a27a28c9672d0ab93e9e51bb05a37bd29c3"},
-    {file = "kombu-4.6.10.tar.gz", hash = "sha256:437b9cdea193cc2ed0b8044c85fd0f126bb3615ca2f4d4a35b39de7cacfa3c1a"},
+    {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.20.0-cp27-cp27m-win32.whl", hash = "sha256:98f6dee9850b29e62977a963e3beb3cfeb98b128a267d59d2c3d675e298c8d57"},
@@ -2686,8 +2749,8 @@ mccabe = [
     {file = "mccabe-0.6.1.tar.gz", hash = "sha256:dd8d182285a0fe56bace7f45b5e7d1a6ebcbf524e8f3bd87eb0f125271b8831f"},
 ]
 more-itertools = [
-    {file = "more-itertools-8.3.0.tar.gz", hash = "sha256:558bb897a2232f5e4f8e2399089e35aecb746e1f9191b6584a151647e89267be"},
-    {file = "more_itertools-8.3.0-py3-none-any.whl", hash = "sha256:7818f596b1e87be009031c7653d01acc46ed422e6656b394b0f765ce66ed4982"},
+    {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"},
@@ -2725,42 +2788,58 @@ 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.16.3-py3-none-any.whl", hash = "sha256:cac9e40c4c978034f09dfc3bc7e8bd3df40215c9207affe1991a0310a14bc71b"},
+    {file = "pg8000-1.16.3.tar.gz", hash = "sha256:91b1584f25c84c2d0fe408bd44466cc16e1db9b9e95f454ff7c334edf3fe650c"},
 ]
 phonenumbers = [
-    {file = "phonenumbers-8.12.4-py2.py3-none-any.whl", hash = "sha256:c6c43d6459aac85b646d6b7a7ab79b3b629eb168f0e9b851b331e2e5872bbd01"},
-    {file = "phonenumbers-8.12.4.tar.gz", hash = "sha256:46c5997fe076026aa2d4b66d0c53eea4babae2e808e8a5f39c09e2dfa6612d08"},
+    {file = "phonenumbers-8.12.7-py2.py3-none-any.whl", hash = "sha256:772d69e620f85bb089d27c4e2bbf718c49ad327459accf9463ac65dbab67695c"},
+    {file = "phonenumbers-8.12.7.tar.gz", hash = "sha256:652c418f8e97c8438f227a524ddf8d7d325c4a47e4924ce865b827c24ec3194d"},
 ]
 pillow = [
-    {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"},
+    {file = "Pillow-7.2.0-cp35-cp35m-macosx_10_10_intel.whl", hash = "sha256:1ca594126d3c4def54babee699c055a913efb01e106c309fa6b04405d474d5ae"},
+    {file = "Pillow-7.2.0-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:c92302a33138409e8f1ad16731568c55c9053eee71bb05b6b744067e1b62380f"},
+    {file = "Pillow-7.2.0-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:8dad18b69f710bf3a001d2bf3afab7c432785d94fcf819c16b5207b1cfd17d38"},
+    {file = "Pillow-7.2.0-cp35-cp35m-manylinux2014_aarch64.whl", hash = "sha256:431b15cffbf949e89df2f7b48528be18b78bfa5177cb3036284a5508159492b5"},
+    {file = "Pillow-7.2.0-cp35-cp35m-win32.whl", hash = "sha256:09d7f9e64289cb40c2c8d7ad674b2ed6105f55dc3b09aa8e4918e20a0311e7ad"},
+    {file = "Pillow-7.2.0-cp35-cp35m-win_amd64.whl", hash = "sha256:0295442429645fa16d05bd567ef5cff178482439c9aad0411d3f0ce9b88b3a6f"},
+    {file = "Pillow-7.2.0-cp36-cp36m-macosx_10_10_x86_64.whl", hash = "sha256:ec29604081f10f16a7aea809ad42e27764188fc258b02259a03a8ff7ded3808d"},
+    {file = "Pillow-7.2.0-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:612cfda94e9c8346f239bf1a4b082fdd5c8143cf82d685ba2dba76e7adeeb233"},
+    {file = "Pillow-7.2.0-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:0a80dd307a5d8440b0a08bd7b81617e04d870e40a3e46a32d9c246e54705e86f"},
+    {file = "Pillow-7.2.0-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:06aba4169e78c439d528fdeb34762c3b61a70813527a2c57f0540541e9f433a8"},
+    {file = "Pillow-7.2.0-cp36-cp36m-win32.whl", hash = "sha256:f7e30c27477dffc3e85c2463b3e649f751789e0f6c8456099eea7ddd53be4a8a"},
+    {file = "Pillow-7.2.0-cp36-cp36m-win_amd64.whl", hash = "sha256:ffe538682dc19cc542ae7c3e504fdf54ca7f86fb8a135e59dd6bc8627eae6cce"},
+    {file = "Pillow-7.2.0-cp37-cp37m-macosx_10_10_x86_64.whl", hash = "sha256:94cf49723928eb6070a892cb39d6c156f7b5a2db4e8971cb958f7b6b104fb4c4"},
+    {file = "Pillow-7.2.0-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:6edb5446f44d901e8683ffb25ebdfc26988ee813da3bf91e12252b57ac163727"},
+    {file = "Pillow-7.2.0-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:52125833b070791fcb5710fabc640fc1df07d087fc0c0f02d3661f76c23c5b8b"},
+    {file = "Pillow-7.2.0-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:9ad7f865eebde135d526bb3163d0b23ffff365cf87e767c649550964ad72785d"},
+    {file = "Pillow-7.2.0-cp37-cp37m-win32.whl", hash = "sha256:c79f9c5fb846285f943aafeafda3358992d64f0ef58566e23484132ecd8d7d63"},
+    {file = "Pillow-7.2.0-cp37-cp37m-win_amd64.whl", hash = "sha256:d350f0f2c2421e65fbc62690f26b59b0bcda1b614beb318c81e38647e0f673a1"},
+    {file = "Pillow-7.2.0-cp38-cp38-macosx_10_10_x86_64.whl", hash = "sha256:6d7741e65835716ceea0fd13a7d0192961212fd59e741a46bbed7a473c634ed6"},
+    {file = "Pillow-7.2.0-cp38-cp38-manylinux1_i686.whl", hash = "sha256:edf31f1150778abd4322444c393ab9c7bd2af271dd4dafb4208fb613b1f3cdc9"},
+    {file = "Pillow-7.2.0-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:d08b23fdb388c0715990cbc06866db554e1822c4bdcf6d4166cf30ac82df8c41"},
+    {file = "Pillow-7.2.0-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:5e51ee2b8114def244384eda1c82b10e307ad9778dac5c83fb0943775a653cd8"},
+    {file = "Pillow-7.2.0-cp38-cp38-win32.whl", hash = "sha256:725aa6cfc66ce2857d585f06e9519a1cc0ef6d13f186ff3447ab6dff0a09bc7f"},
+    {file = "Pillow-7.2.0-cp38-cp38-win_amd64.whl", hash = "sha256:a060cf8aa332052df2158e5a119303965be92c3da6f2d93b6878f0ebca80b2f6"},
+    {file = "Pillow-7.2.0-pp36-pypy36_pp73-win32.whl", hash = "sha256:25930fadde8019f374400f7986e8404c8b781ce519da27792cbe46eabec00c4d"},
+    {file = "Pillow-7.2.0.tar.gz", hash = "sha256:97f9e7953a77d5a70f49b9a48da7776dc51e9b738151b22dacf101641594a626"},
 ]
 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.2-cp27-none-win32.whl", hash = "sha256:f2018461733b23f308c298653c8903d32aaad7873d25e1d228765e91ae42c3f2"},
+    {file = "psutil-5.7.2-cp27-none-win_amd64.whl", hash = "sha256:66c18ca7680a31bf16ee22b1d21b6397869dda8059dbdb57d9f27efa6615f195"},
+    {file = "psutil-5.7.2-cp35-cp35m-win32.whl", hash = "sha256:5e9d0f26d4194479a13d5f4b3798260c20cecf9ac9a461e718eb59ea520a360c"},
+    {file = "psutil-5.7.2-cp35-cp35m-win_amd64.whl", hash = "sha256:4080869ed93cce662905b029a1770fe89c98787e543fa7347f075ade761b19d6"},
+    {file = "psutil-5.7.2-cp36-cp36m-win32.whl", hash = "sha256:d8a82162f23c53b8525cf5f14a355f5d1eea86fa8edde27287dd3a98399e4fdf"},
+    {file = "psutil-5.7.2-cp36-cp36m-win_amd64.whl", hash = "sha256:0ee3c36428f160d2d8fce3c583a0353e848abb7de9732c50cf3356dd49ad63f8"},
+    {file = "psutil-5.7.2-cp37-cp37m-win32.whl", hash = "sha256:ff1977ba1a5f71f89166d5145c3da1cea89a0fdb044075a12c720ee9123ec818"},
+    {file = "psutil-5.7.2-cp37-cp37m-win_amd64.whl", hash = "sha256:a5b120bb3c0c71dfe27551f9da2f3209a8257a178ed6c628a819037a8df487f1"},
+    {file = "psutil-5.7.2-cp38-cp38-win32.whl", hash = "sha256:10512b46c95b02842c225f58fa00385c08fa00c68bac7da2d9a58ebe2c517498"},
+    {file = "psutil-5.7.2-cp38-cp38-win_amd64.whl", hash = "sha256:68d36986ded5dac7c2dcd42f2682af1db80d4bce3faa126a6145c1637e1b559f"},
+    {file = "psutil-5.7.2.tar.gz", hash = "sha256:90990af1c3c67195c44c9a889184f84f5b2320dce3ee3acbd054e3ba0b4a7beb"},
+]
 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"},
@@ -2777,8 +2856,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"},
@@ -2819,36 +2898,36 @@ pycparser = [
     {file = "pycparser-2.20.tar.gz", hash = "sha256:2d475327684562c3a96cc71adf7dc8c4f0565175cf86b6d7a404ff4c771f15f0"},
 ]
 pycryptodome = [
-    {file = "pycryptodome-3.9.7-cp27-cp27m-macosx_10_6_intel.whl", hash = "sha256:0e10f352ccbbcb5bb2dc4ecaf106564e65702a717d72ab260f9ac4c19753cfc2"},
-    {file = "pycryptodome-3.9.7-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:9c739b7795ccf2ef1fdad8d44e539a39ad300ee6786e804ea7f0c6a786eb5343"},
-    {file = "pycryptodome-3.9.7-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:9977086e0f93adb326379897437373871b80501e1d176fec63c7f46fb300c862"},
-    {file = "pycryptodome-3.9.7-cp27-cp27m-win32.whl", hash = "sha256:83295a3fb5cf50c48631eb5b440cb5e9832d8c14d81d1d45f4497b67a9987de8"},
-    {file = "pycryptodome-3.9.7-cp27-cp27m-win_amd64.whl", hash = "sha256:b1e332587b3b195542e77681389c296e1837ca01240399d88803a075447d3557"},
-    {file = "pycryptodome-3.9.7-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:9378c309aec1f8cd8bad361ed0816a440151b97a2a3f6ffdaba1d1a1fb76873a"},
-    {file = "pycryptodome-3.9.7-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:d4f94368ce2d65873a87ad867eb3bf63f4ba81eb97a9ee66d38c2b71ce5a7439"},
-    {file = "pycryptodome-3.9.7-cp34-cp34m-manylinux1_i686.whl", hash = "sha256:f655addaaaa9974108d4808f4150652589cada96074c87115c52e575bfcd87d5"},
-    {file = "pycryptodome-3.9.7-cp34-cp34m-manylinux1_x86_64.whl", hash = "sha256:9a94fca11fdc161460bd8659c15b6adef45c1b20da86402256eaf3addfaab324"},
-    {file = "pycryptodome-3.9.7-cp35-cp35m-macosx_10_6_intel.whl", hash = "sha256:ea83bcd9d6c03248ebd46e71ac313858e0afd5aa2fa81478c0e653242f3eb476"},
-    {file = "pycryptodome-3.9.7-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:07024fc364869eae8d6ac0d316e089956e6aeffe42dbdcf44fe1320d96becf7f"},
-    {file = "pycryptodome-3.9.7-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:426c188c83c10df71f053e04b4003b1437bae5cb37606440e498b00f160d71d0"},
-    {file = "pycryptodome-3.9.7-cp35-cp35m-win32.whl", hash = "sha256:d61b012baa8c2b659e9890011358455c0019a4108536b811602d2f638c40802a"},
-    {file = "pycryptodome-3.9.7-cp35-cp35m-win_amd64.whl", hash = "sha256:1f4752186298caf2e9ff5354f2e694d607ca7342aa313a62005235d46e28cf04"},
-    {file = "pycryptodome-3.9.7-cp36-cp36m-macosx_10_6_intel.whl", hash = "sha256:767ad0fb5d23efc36a4d5c2fc608ac603f3de028909bcf59abc943e0d0bc5a36"},
-    {file = "pycryptodome-3.9.7-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:2fbc472e0b567318fe2052281d5a8c0ae70099b446679815f655e9fbc18c3a65"},
-    {file = "pycryptodome-3.9.7-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:9230fcb5d948c3fb40049bace4d33c5d254f8232c2c0bba05d2570aea3ba4520"},
-    {file = "pycryptodome-3.9.7-cp36-cp36m-win32.whl", hash = "sha256:8f06556a8f7ea7b1e42eff39726bb0dca1c251205debae64e6eebea3cd7b438a"},
-    {file = "pycryptodome-3.9.7-cp36-cp36m-win_amd64.whl", hash = "sha256:d6e1bc5c94873bec742afe2dfadce0d20445b18e75c47afc0c115b19e5dd38dd"},
-    {file = "pycryptodome-3.9.7-cp37-cp37m-macosx_10_6_intel.whl", hash = "sha256:3ec3dc2f80f71fd0c955ce48b81bfaf8914c6f63a41a738f28885a1c4892968a"},
-    {file = "pycryptodome-3.9.7-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:cff31f5a8977534f255f729d5d2467526f2b10563a30bbdade92223e0bf264bd"},
-    {file = "pycryptodome-3.9.7-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:ed5761b37615a1f222c5345bbf45272ae2cf8c7dff88a4f53a1e9f977cbb6d95"},
-    {file = "pycryptodome-3.9.7-cp37-cp37m-win32.whl", hash = "sha256:f011cd0062e54658b7086a76f8cf0f4222812acc66e219e196ea2d0a8849d0ed"},
-    {file = "pycryptodome-3.9.7-cp37-cp37m-win_amd64.whl", hash = "sha256:626c0a1d4d83ec6303f970a17158114f75c3ba1736f7f2983f7b40a265861bd8"},
-    {file = "pycryptodome-3.9.7-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:8be56bde3312e022d9d1d6afa124556460ad5c844c2fc63642f6af723c098d35"},
-    {file = "pycryptodome-3.9.7-cp38-cp38-manylinux1_i686.whl", hash = "sha256:c818dc1f3eace93ee50c2b6b5c2becf7c418fa5dd1ba6fc0ef7db279ea21d5e4"},
-    {file = "pycryptodome-3.9.7-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:09b6d6bcc01a4eb1a2b4deeff5aa602a108ec5aed8ac75ae554f97d1d7f0a5ad"},
-    {file = "pycryptodome-3.9.7-cp38-cp38-win32.whl", hash = "sha256:7ac729d9091ed5478af2b4a4f44f5335a98febbc008af619e4569a59fe503e40"},
-    {file = "pycryptodome-3.9.7-cp38-cp38-win_amd64.whl", hash = "sha256:c109a26a21f21f695d369ff9b87f5d43e0d6c768d8384e10bc74142bed2e092e"},
-    {file = "pycryptodome-3.9.7.tar.gz", hash = "sha256:f1add21b6d179179b3c177c33d18a2186a09cc0d3af41ff5ed3f377360b869f2"},
+    {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"},
@@ -2875,8 +2954,8 @@ pytest = [
     {file = "pytest-5.4.3.tar.gz", hash = "sha256:7979331bfcba207414f5e1263b5a0f8f521d0f457318836a7355531ed1a4c7d8"},
 ]
 pytest-cov = [
-    {file = "pytest-cov-2.9.0.tar.gz", hash = "sha256:b6a814b8ed6247bd81ff47f038511b57fe1ce7f4cc25b9106f1a4b106f1d9322"},
-    {file = "pytest_cov-2.9.0-py2.py3-none-any.whl", hash = "sha256:c87dfd8465d865655a8213859f1b4749b43448b5fae465cb981e16d52a811424"},
+    {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"},
@@ -2887,7 +2966,7 @@ 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.3.tar.gz", hash = "sha256:1630b5b7ea3624919b73fde37cffb87965c5087a4afab8a43074ff44e0d810c4"},
+    {file = "pytest-sugar-0.9.4.tar.gz", hash = "sha256:b1b2186b0a72aada6859bea2a5764145e3aaa2c1cfbb23c3a19b5f7b697563d3"},
 ]
 python-box = [
     {file = "python-box-3.4.6.tar.gz", hash = "sha256:694a7555e3ff9fbbce734bbaef3aad92b8e4ed0659d3ed04d56b6a0a0eff26a9"},
@@ -2901,11 +2980,11 @@ python-dateutil = [
     {file = "python_dateutil-2.8.1-py2.py3-none-any.whl", hash = "sha256:75bb3f31ea686f1197762692a9ee6a7550b59fc6ca3a1f4b5d7e32fb98e2da2a"},
 ]
 python-dotenv = [
-    {file = "python-dotenv-0.13.0.tar.gz", hash = "sha256:3b9909bc96b0edc6b01586e1eed05e71174ef4e04c71da5786370cebea53ad74"},
-    {file = "python_dotenv-0.13.0-py2.py3-none-any.whl", hash = "sha256:25c0ff1a3e12f4bde8d592cc254ab075cfe734fc5dd989036716fd17ee7e5ec7"},
+    {file = "python-dotenv-0.14.0.tar.gz", hash = "sha256:8c10c99a1b25d9a68058a1ad6f90381a62ba68230ca93966882a4dbc3bc9c33d"},
+    {file = "python_dotenv-0.14.0-py2.py3-none-any.whl", hash = "sha256:c10863aee750ad720f4f43436565e4c1698798d763b63234fb5021b6c616e423"},
 ]
 python-ldap = [
-    {file = "python-ldap-3.2.0.tar.gz", hash = "sha256:7d1c4b15375a533564aad3d3deade789221e450052b21ebb9720fb822eccdb8e"},
+    {file = "python-ldap-3.3.1.tar.gz", hash = "sha256:4711cacf013e298754abd70058ccc995758177fb425f1c2d30e71adfc1d00aa5"},
 ]
 python-memcached = [
     {file = "python-memcached-1.59.tar.gz", hash = "sha256:a2e28637be13ee0bf1a8b6843e7490f9456fd3f2a4cb60471733c7b5d5557e4f"},
@@ -2937,31 +3016,31 @@ redis = [
     {file = "redis-3.5.3.tar.gz", hash = "sha256:0e7e0cfca8660dea8b7d5cd8c4f6c5e29e11f31158c0b0ae91a397f00e5a05a2"},
 ]
 regex = [
-    {file = "regex-2020.5.14-cp27-cp27m-win32.whl", hash = "sha256:e565569fc28e3ba3e475ec344d87ed3cd8ba2d575335359749298a0899fe122e"},
-    {file = "regex-2020.5.14-cp27-cp27m-win_amd64.whl", hash = "sha256:d466967ac8e45244b9dfe302bbe5e3337f8dc4dec8d7d10f5e950d83b140d33a"},
-    {file = "regex-2020.5.14-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:27ff7325b297fb6e5ebb70d10437592433601c423f5acf86e5bc1ee2919b9561"},
-    {file = "regex-2020.5.14-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:ea55b80eb0d1c3f1d8d784264a6764f931e172480a2f1868f2536444c5f01e01"},
-    {file = "regex-2020.5.14-cp36-cp36m-manylinux2010_i686.whl", hash = "sha256:c9bce6e006fbe771a02bda468ec40ffccbf954803b470a0345ad39c603402577"},
-    {file = "regex-2020.5.14-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:d881c2e657c51d89f02ae4c21d9adbef76b8325fe4d5cf0e9ad62f850f3a98fd"},
-    {file = "regex-2020.5.14-cp36-cp36m-win32.whl", hash = "sha256:99568f00f7bf820c620f01721485cad230f3fb28f57d8fbf4a7967ec2e446994"},
-    {file = "regex-2020.5.14-cp36-cp36m-win_amd64.whl", hash = "sha256:70c14743320a68c5dac7fc5a0f685be63bc2024b062fe2aaccc4acc3d01b14a1"},
-    {file = "regex-2020.5.14-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:a7c37f048ec3920783abab99f8f4036561a174f1314302ccfa4e9ad31cb00eb4"},
-    {file = "regex-2020.5.14-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:89d76ce33d3266173f5be80bd4efcbd5196cafc34100fdab814f9b228dee0fa4"},
-    {file = "regex-2020.5.14-cp37-cp37m-manylinux2010_i686.whl", hash = "sha256:51f17abbe973c7673a61863516bdc9c0ef467407a940f39501e786a07406699c"},
-    {file = "regex-2020.5.14-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:ce5cc53aa9fbbf6712e92c7cf268274eaff30f6bd12a0754e8133d85a8fb0f5f"},
-    {file = "regex-2020.5.14-cp37-cp37m-win32.whl", hash = "sha256:8044d1c085d49673aadb3d7dc20ef5cb5b030c7a4fa253a593dda2eab3059929"},
-    {file = "regex-2020.5.14-cp37-cp37m-win_amd64.whl", hash = "sha256:c2062c7d470751b648f1cacc3f54460aebfc261285f14bc6da49c6943bd48bdd"},
-    {file = "regex-2020.5.14-cp38-cp38-manylinux1_i686.whl", hash = "sha256:329ba35d711e3428db6b45a53b1b13a0a8ba07cbbcf10bbed291a7da45f106c3"},
-    {file = "regex-2020.5.14-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:579ea215c81d18da550b62ff97ee187b99f1b135fd894a13451e00986a080cad"},
-    {file = "regex-2020.5.14-cp38-cp38-manylinux2010_i686.whl", hash = "sha256:3a9394197664e35566242686d84dfd264c07b20f93514e2e09d3c2b3ffdf78fe"},
-    {file = "regex-2020.5.14-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:ce367d21f33e23a84fb83a641b3834dd7dd8e9318ad8ff677fbfae5915a239f7"},
-    {file = "regex-2020.5.14-cp38-cp38-win32.whl", hash = "sha256:1386e75c9d1574f6aa2e4eb5355374c8e55f9aac97e224a8a5a6abded0f9c927"},
-    {file = "regex-2020.5.14-cp38-cp38-win_amd64.whl", hash = "sha256:7e61be8a2900897803c293247ef87366d5df86bf701083b6c43119c7c6c99108"},
-    {file = "regex-2020.5.14.tar.gz", hash = "sha256:ce450ffbfec93821ab1fea94779a8440e10cf63819be6e176eb1973a6017aff5"},
+    {file = "regex-2020.7.14-cp27-cp27m-win32.whl", hash = "sha256:e46d13f38cfcbb79bfdb2964b0fe12561fe633caf964a77a5f8d4e45fe5d2ef7"},
+    {file = "regex-2020.7.14-cp27-cp27m-win_amd64.whl", hash = "sha256:6961548bba529cac7c07af2fd4d527c5b91bb8fe18995fed6044ac22b3d14644"},
+    {file = "regex-2020.7.14-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:c50a724d136ec10d920661f1442e4a8b010a4fe5aebd65e0c2241ea41dbe93dc"},
+    {file = "regex-2020.7.14-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:8a51f2c6d1f884e98846a0a9021ff6861bdb98457879f412fdc2b42d14494067"},
+    {file = "regex-2020.7.14-cp36-cp36m-manylinux2010_i686.whl", hash = "sha256:9c568495e35599625f7b999774e29e8d6b01a6fb684d77dee1f56d41b11b40cd"},
+    {file = "regex-2020.7.14-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:51178c738d559a2d1071ce0b0f56e57eb315bcf8f7d4cf127674b533e3101f88"},
+    {file = "regex-2020.7.14-cp36-cp36m-win32.whl", hash = "sha256:9eddaafb3c48e0900690c1727fba226c4804b8e6127ea409689c3bb492d06de4"},
+    {file = "regex-2020.7.14-cp36-cp36m-win_amd64.whl", hash = "sha256:14a53646369157baa0499513f96091eb70382eb50b2c82393d17d7ec81b7b85f"},
+    {file = "regex-2020.7.14-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:1269fef3167bb52631ad4fa7dd27bf635d5a0790b8e6222065d42e91bede4162"},
+    {file = "regex-2020.7.14-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:d0a5095d52b90ff38592bbdc2644f17c6d495762edf47d876049cfd2968fbccf"},
+    {file = "regex-2020.7.14-cp37-cp37m-manylinux2010_i686.whl", hash = "sha256:4c037fd14c5f4e308b8370b447b469ca10e69427966527edcab07f52d88388f7"},
+    {file = "regex-2020.7.14-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:bc3d98f621898b4a9bc7fecc00513eec8f40b5b83913d74ccb445f037d58cd89"},
+    {file = "regex-2020.7.14-cp37-cp37m-win32.whl", hash = "sha256:46bac5ca10fb748d6c55843a931855e2727a7a22584f302dd9bb1506e69f83f6"},
+    {file = "regex-2020.7.14-cp37-cp37m-win_amd64.whl", hash = "sha256:0dc64ee3f33cd7899f79a8d788abfbec168410be356ed9bd30bbd3f0a23a7204"},
+    {file = "regex-2020.7.14-cp38-cp38-manylinux1_i686.whl", hash = "sha256:5ea81ea3dbd6767873c611687141ec7b06ed8bab43f68fad5b7be184a920dc99"},
+    {file = "regex-2020.7.14-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:bbb332d45b32df41200380fff14712cb6093b61bd142272a10b16778c418e98e"},
+    {file = "regex-2020.7.14-cp38-cp38-manylinux2010_i686.whl", hash = "sha256:c11d6033115dc4887c456565303f540c44197f4fc1a2bfb192224a301534888e"},
+    {file = "regex-2020.7.14-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:75aaa27aa521a182824d89e5ab0a1d16ca207318a6b65042b046053cfc8ed07a"},
+    {file = "regex-2020.7.14-cp38-cp38-win32.whl", hash = "sha256:d6cff2276e502b86a25fd10c2a96973fdb45c7a977dca2138d661417f3728341"},
+    {file = "regex-2020.7.14-cp38-cp38-win_amd64.whl", hash = "sha256:7a2dd66d2d4df34fa82c9dc85657c5e019b87932019947faece7983f2089a840"},
+    {file = "regex-2020.7.14.tar.gz", hash = "sha256:3a3af27a8d23143c49a3420efe5b3f8cf1a48c6fc8bc6856b03f638abc1833bb"},
 ]
 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.1.tar.gz", hash = "sha256:470e53b64817211a42805c3a104d2216f6f5834b22fe7adb637d1de4d6501fb8"},
@@ -2974,8 +3053,8 @@ safety = [
     {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"},
@@ -3002,12 +3081,12 @@ spdx-license-list = [
     {file = "spdx_license_list-0.5.0.tar.gz", hash = "sha256:40cd53ff16401bab7059e6d1ef61839196b12079929a2763a50145d3b6949bc1"},
 ]
 sphinx = [
-    {file = "Sphinx-3.0.4-py3-none-any.whl", hash = "sha256:779a519adbd3a70fc7c468af08c5e74829868b0a5b34587b33340e010291856c"},
-    {file = "Sphinx-3.0.4.tar.gz", hash = "sha256:ea64df287958ee5aac46be7ac2b7277305b0381d213728c3a49d8bb9b8415807"},
+    {file = "Sphinx-3.1.2-py3-none-any.whl", hash = "sha256:97dbf2e31fc5684bb805104b8ad34434ed70e6c588f6896991b2fdfd2bef8c00"},
+    {file = "Sphinx-3.1.2.tar.gz", hash = "sha256:b9daeb9b39aa1ffefc2809b43604109825300300b987a24f45976c001ba1a8fd"},
 ]
 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"},
@@ -3042,8 +3121,8 @@ 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-3.2.0-py3-none-any.whl", hash = "sha256:c8f4f0ebbc394e52ddf49de8bcc3cf8ad2b4425ebac494106bbc5e3661ac7633"},
+    {file = "stevedore-3.2.0.tar.gz", hash = "sha256:38791aa5bed922b0a844513c5f9ed37774b68edc609e5ab8ab8d8fe0ce4315e5"},
 ]
 termcolor = [
     {file = "termcolor-1.1.0.tar.gz", hash = "sha256:1d6d69ce66211143803fbc56652b41d73b4a400a2891d7bf7a1cdf4c02de613b"},
@@ -3069,11 +3148,11 @@ toml = [
     {file = "toml-0.10.1.tar.gz", hash = "sha256:926b612be1e5ce0634a2ca03470f95169cf16f939018233a670519cb4ac58b0f"},
 ]
 tqdm = [
-    {file = "tqdm-4.46.1-py2.py3-none-any.whl", hash = "sha256:07c06493f1403c1380b630ae3dcbe5ae62abcf369a93bbc052502279f189ab8c"},
-    {file = "tqdm-4.46.1.tar.gz", hash = "sha256:cd140979c2bebd2311dfb14781d8f19bd5a9debb92dcab9f6ef899c987fcf71f"},
+    {file = "tqdm-4.48.0-py2.py3-none-any.whl", hash = "sha256:fcb7cb5b729b60a27f300b15c1ffd4744f080fb483b88f31dc8654b082cc8ea5"},
+    {file = "tqdm-4.48.0.tar.gz", hash = "sha256:6baa75a88582b1db6d34ce4690da5501d2a1cb65c34664840a456b2c9f794d29"},
 ]
 twilio = [
-    {file = "twilio-6.41.0.tar.gz", hash = "sha256:7c6329118583852bb06a2065dd2987a012310e5dfd834ef821d736b059bd1c74"},
+    {file = "twilio-6.44.1.tar.gz", hash = "sha256:a84b6a4e8f6c739cf3fa429033ad522d07c78e3788800e68fe5195d83db4f03a"},
 ]
 typed-ast = [
     {file = "typed_ast-1.4.1-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:73d785a950fc82dd2a25897d525d003f6378d1cb23ab305578394694202a58c3"},
@@ -3104,16 +3183,16 @@ typing-extensions = [
     {file = "typing_extensions-3.7.4.2.tar.gz", hash = "sha256:79ee589a3caca649a9bfd2a8de4709837400dfa00b6cc81962a1e6a1815969ae"},
 ]
 urllib3 = [
-    {file = "urllib3-1.25.9-py2.py3-none-any.whl", hash = "sha256:88206b0eb87e6d677d424843ac5209e3fb9d0190d0ee169599165ec25e9d9115"},
-    {file = "urllib3-1.25.9.tar.gz", hash = "sha256:3018294ebefce6572a474f0604c2021e33b3fd8006ecd11d62107a5d2a963527"},
+    {file = "urllib3-1.25.10-py2.py3-none-any.whl", hash = "sha256:e7983572181f5e1522d9c98453462384ee92a0be7fac5f1413a1e35c56cc0461"},
+    {file = "urllib3-1.25.10.tar.gz", hash = "sha256:91056c15fa70756691db97756772bb1eb9678fa585d9184f24534b100dc60f4a"},
 ]
 vine = [
     {file = "vine-1.3.0-py2.py3-none-any.whl", hash = "sha256:ea4947cc56d1fd6f2095c8d543ee25dad966f78692528e68b4fada11ba3f98af"},
     {file = "vine-1.3.0.tar.gz", hash = "sha256:133ee6d7a9016f177ddeaf191c1f58421a1dcc6ee9a42c58b34bed40e1d2cd87"},
 ]
 wcwidth = [
-    {file = "wcwidth-0.2.3-py2.py3-none-any.whl", hash = "sha256:980fbf4f3c196c0f329cdcd1e84c554d6a211f18e252e525a0cf4223154a41d6"},
-    {file = "wcwidth-0.2.3.tar.gz", hash = "sha256:edbc2b718b4db6cdf393eefe3a420183947d6aa312505ce6754516f458ff8830"},
+    {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"},
diff --git a/pyproject.toml b/pyproject.toml
index 2f7d813614eeff7620bb003f7f0d8c41f45ce74e..4fc9f6b4dec5741ff9eb49b200aa3807d4aa0bb9 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -43,9 +43,9 @@ 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"
+django-ipware = "^3.0"
 easy-thumbnails = "^2.6"
 django-image-cropping = "^1.2"
 django-impersonate = "^1.4"
@@ -72,7 +72,7 @@ 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.9.1"
+django-otp = "0.9.3"
 django-colorfield = "^0.3.0"
 django-bleach = "^0.6.1"
 django-guardian = "^2.2.0"
@@ -86,6 +86,9 @@ license-expression = "^1.2"
 django-reversion = "^3.0.7"
 django-favicon-plus-reloaded = "^1.0.4"
 authlib = "^0.14.3"
+django-health-check = "^3.12.1"
+psutil = "^5.7.0"
+celery-progress = "^0.0.10"
 
 [tool.poetry.extras]
 ldap = ["django-auth-ldap"]
@@ -111,7 +114,7 @@ flake8-docstrings = "^1.5.0"
 flake8-rst-docstrings = "^0.0.13"
 black = "^19.10b0"
 flake8-black = "^0.2.0"
-isort = "^4.3.21"
+isort = "^5.0.0"
 flake8-isort = "^3.0.0"
 pytest-cov = "^2.8.1"
 pytest-sugar = "^0.9.2"