diff --git a/aleksis/core/data_checks.py b/aleksis/core/data_checks.py
index 1279822f8d1f28bb40e040d9eaf0bb8126540940..f7152686f61fb63997eb9e0a6af9e23b47237f66 100644
--- a/aleksis/core/data_checks.py
+++ b/aleksis/core/data_checks.py
@@ -9,7 +9,8 @@ import reversion
 from reversion import set_comment
 from templated_email import send_templated_mail
 
-from .util.core_helpers import celery_optional, get_site_preferences
+from .celery import app
+from .util.core_helpers import get_site_preferences
 
 
 class SolveOption:
@@ -207,7 +208,7 @@ class DataCheckRegistry:
         return [(check.name, check.verbose_name) for check in cls.data_checks]
 
 
-@celery_optional
+@app.task
 def check_data():
     """Execute all registered data checks and send email if activated."""
     for check in DataCheckRegistry.data_checks:
diff --git a/aleksis/core/settings.py b/aleksis/core/settings.py
index ec033ede3403f8b0477fb89d8194e881db47123a..cad7ea6b2c0acc8707d0922c67ddc16ed94b3c46 100644
--- a/aleksis/core/settings.py
+++ b/aleksis/core/settings.py
@@ -81,6 +81,12 @@ INSTALLED_APPS = [
     "polymorphic",
     "django_global_request",
     "dbbackup",
+    "django_celery_beat",
+    "django_celery_results",
+    "celery_progress",
+    "health_check.contrib.celery",
+    "djcelery_email",
+    "celery_haystack",
     "settings_context_processor",
     "sass_processor",
     "django_any_js",
@@ -190,7 +196,6 @@ DATABASES = {
         "PASSWORD": _settings.get("database.password", None),
         "HOST": _settings.get("database.host", "127.0.0.1"),
         "PORT": _settings.get("database.port", "5432"),
-        "ATOMIC_REQUESTS": True,
         "CONN_MAX_AGE": _settings.get("database.conn_max_age", None),
     }
 }
@@ -484,21 +489,13 @@ if _settings.get("twilio.sid", None):
     TWILIO_TOKEN = _settings.get("twilio.token")
     TWILIO_CALLER_ID = _settings.get("twilio.callerid")
 
-if _settings.get("celery.enabled", False):
-    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"
-    CELERY_BEAT_SCHEDULER = "django_celery_beat.schedulers:DatabaseScheduler"
+CELERY_BROKER_URL = _settings.get("celery.broker", "redis://localhost")
+CELERY_RESULT_BACKEND = "django-db"
+CELERY_CACHE_BACKEND = "django-cache"
+CELERY_BEAT_SCHEDULER = "django_celery_beat.schedulers:DatabaseScheduler"
 
-    if _settings.get("celery.email", False):
-        INSTALLED_APPS += ("djcelery_email",)
-        EMAIL_BACKEND = "djcelery_email.backends.CeleryEmailBackend"
+if _settings.get("celery.email", False):
+    EMAIL_BACKEND = "djcelery_email.backends.CeleryEmailBackend"
 
 PWA_APP_NAME = lazy_preference("general", "title")
 PWA_APP_DESCRIPTION = lazy_preference("general", "description")
@@ -721,12 +718,8 @@ elif HAYSTACK_BACKEND_SHORT == "whoosh":
         },
     }
 
-if _settings.get("celery.enabled", False) and _settings.get("search.celery", True):
-    INSTALLED_APPS.append("celery_haystack")
-    HAYSTACK_SIGNAL_PROCESSOR = "celery_haystack.signals.CelerySignalProcessor"
-    CELERY_HAYSTACK_IGNORE_RESULT = True
-else:
-    HAYSTACK_SIGNAL_PROCESSOR = "haystack.signals.RealtimeSignalProcessor"
+HAYSTACK_SIGNAL_PROCESSOR = "celery_haystack.signals.CelerySignalProcessor"
+CELERY_HAYSTACK_IGNORE_RESULT = True
 
 HAYSTACK_SEARCH_RESULTS_PER_PAGE = 10
 
diff --git a/aleksis/core/tasks.py b/aleksis/core/tasks.py
index d085f027fad09e875bf612d8352d83151e77d880..731c4356c20bba915bd5ad0811307c3877810631 100644
--- a/aleksis/core/tasks.py
+++ b/aleksis/core/tasks.py
@@ -1,11 +1,11 @@
 from django.conf import settings
 from django.core import management
 
-from .util.core_helpers import celery_optional
+from .celery import app
 from .util.notifications import send_notification as _send_notification
 
 
-@celery_optional
+@app.task
 def send_notification(notification: int, resend: bool = False) -> None:
     """Send a notification object to its recipient.
 
@@ -15,7 +15,7 @@ def send_notification(notification: int, resend: bool = False) -> None:
     _send_notification(notification, resend)
 
 
-@celery_optional
+@app.task
 def backup_data() -> None:
     """Backup database and media using django-dbbackup."""
     # Assemble command-line options for dbbackup management command
diff --git a/aleksis/core/urls.py b/aleksis/core/urls.py
index eb5c6c41ac26173ebe81df366b19acf80d6ed234..8183fba0d6da1ea956eb1da94b720b49e02363ac 100644
--- a/aleksis/core/urls.py
+++ b/aleksis/core/urls.py
@@ -15,7 +15,6 @@ from rules.contrib.views import permission_required
 from two_factor.urls import urlpatterns as tf_urls
 
 from . import views
-from .util.core_helpers import is_celery_enabled
 
 urlpatterns = [
     path("", include("django_prometheus.urls")),
@@ -25,6 +24,7 @@ urlpatterns = [
     path("data_management/", views.data_management, name="data_management"),
     path("status/", views.SystemStatus.as_view(), name="system_status"),
     path("", include(tf_urls)),
+    path("celery_progress/", include("celery_progress.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"),
@@ -211,9 +211,6 @@ if hasattr(settings, "TWILIO_ACCOUNT_SID"):
 
     urlpatterns += [path("", include(tf_twilio_urls))]
 
-if is_celery_enabled():
-    urlpatterns.append(path("celery_progress/", include("celery_progress.urls")))
-
 # Serve javascript-common if in development
 if settings.DEBUG:
     urlpatterns.append(path("__debug__/", include(debug_toolbar.urls)))
diff --git a/aleksis/core/util/celery_progress.py b/aleksis/core/util/celery_progress.py
index 9de760eeb56b8ce535a0cd8e88f7380c5411f78d..e957dd4f938291b11a85d00e115d302f512e609a 100644
--- a/aleksis/core/util/celery_progress.py
+++ b/aleksis/core/util/celery_progress.py
@@ -1,34 +1,162 @@
-from decimal import Decimal
-from typing import Union
+from functools import wraps
+from numbers import Number
+from typing import Callable, Generator, Iterable, Optional, Sequence, Union
+
+from django.contrib import messages
 
 from celery_progress.backend import PROGRESS_STATE, AbstractProgressRecorder
 
+from ..celery import app
+
 
 class ProgressRecorder(AbstractProgressRecorder):
+    """Track the progress of a Celery task and give data to the frontend.
+
+    This recorder provides the functions `set_progress` and `add_message`
+    which can be used to track the status of a Celery task.
+
+    How to use
+    ----------
+    1. Write a function and include tracking methods
+
+    ::
+
+        from django.contrib import messages
+
+        from aleksis.core.util.celery_progress import recorded_task
+
+        @recorded_task
+        def do_something(foo, bar, recorder, baz=None):
+            # ...
+            recorder.set_progress(total=len(list_with_data))
+
+            for i, item in enumerate(list_with_data):
+                # ...
+                recorder.set_progress(i+1)
+                # ...
+
+            recorder.add_message(messages.SUCCESS, "All data were imported successfully.")
+
+    You can also use `recorder.iterate` to simplify iterating and counting.
+
+    2. Track progress in view:
+
+    ::
+
+        def my_view(request):
+            context = {}
+            # ...
+            result = do_something.delay(foo, bar, baz=baz)
+
+            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)
+    """
+
     def __init__(self, task):
         self.task = task
-        self.messages = []
-        self.total = 100
-        self.current = 0
+        self._messages = []
+        self._current = 0
+        self._total = 100
 
-    def set_progress(self, current: Union[int, float], **kwargs):
-        self.current = current
+    def iterate(self, data: Union[Iterable, Sequence], total: Optional[int] = None) -> Generator:
+        """Iterate over a sequence or iterable, updating progress on the move.
+
+        ::
+
+            @recorded_task
+            def do_something(long_list, recorder):
+                for item in recorder.iterate(long_list):
+                    do_something_with(item)
+
+        :param data: A sequence (tuple, list, set,...) or an iterable
+        :param total: Total number of items, in case data does not support len()
+        """
+        if total is None and hasattr(data, "__len__"):
+            total = len(data)
+        else:
+            raise TypeError("No total value passed, and data does not support len()")
+
+        for current, item in enumerate(data):
+            self.set_progress(current, total)
+            yield item
+
+    def set_progress(
+        self,
+        current: Optional[Number] = None,
+        total: Optional[Number] = None,
+        description: Optional[str] = None,
+        level: int = messages.INFO,
+    ):
+        """Set the current progress in the frontend.
+
+        The progress percentage is automatically calculated in relation to self.total.
+
+        :param current: The number of processed items; relative to total, default unchanged
+        :param total: The total number of items (or 100 if using a percentage), default unchanged
+        :param description: A textual description, routed to the frontend as an INFO message
+        """
+        if current is not None:
+            self._current = current
+        if total is not None:
+            self._total = total
 
         percent = 0
-        if self.total > 0:
-            percent = (Decimal(current) / Decimal(self.total)) * Decimal(100)
-            percent = float(round(percent, 2))
+        if self._total > 0:
+            percent = self._current / self._total * 100
+
+        if description is not None:
+            self._messages.append((level, description))
 
         self.task.update_state(
             state=PROGRESS_STATE,
             meta={
-                "current": current,
-                "total": self.total,
+                "current": self._current,
+                "total": self._total,
                 "percent": percent,
-                "messages": self.messages,
+                "messages": self._messages,
             },
         )
 
-    def add_message(self, level: int, message: str, **kwargs):
-        self.messages.append((level, message))
-        self.set_progress(self.current)
+    def add_message(self, level: int, message: str) -> None:
+        """Show a message in the progress frontend.
+
+        This method is a shortcut for set_progress with no new progress arguments,
+        passing only the message and level as description.
+
+        :param level: The message level (default levels from django.contrib.messages)
+        :param message: The actual message (should be translated)
+        """
+        self.set_progress(description=message, level=level)
+
+
+def recorded_task(orig: Optional[Callable] = None, **kwargs) -> Union[Callable, app.Task]:
+    """Create a Celery task that receives a ProgressRecorder.
+
+    Returns a Task object with a wrapper that passes the recorder instance
+    as the recorder keyword argument.
+    """
+
+    def _real_decorator(orig: Callable) -> app.Task:
+        @wraps(orig)
+        def _inject_recorder(task, *args, **kwargs):
+            recorder = ProgressRecorder(task)
+            return orig(*args, **kwargs, recorder=recorder)
+
+        # Force bind to True because _inject_recorder needs the Task object
+        kwargs["bind"] = True
+        return app.task(_inject_recorder, **kwargs)
+
+    if orig and not kwargs:
+        return _real_decorator(orig)
+    return _real_decorator
diff --git a/aleksis/core/util/core_helpers.py b/aleksis/core/util/core_helpers.py
index 9e9a90faf5547b71f3d556d89d6f2459b743c04b..676894d28d62f32ee9c08de874edf6857f94a629 100644
--- a/aleksis/core/util/core_helpers.py
+++ b/aleksis/core/util/core_helpers.py
@@ -1,6 +1,5 @@
 import os
 import sys
-import time
 from datetime import datetime, timedelta
 from importlib import import_module
 from itertools import groupby
@@ -14,7 +13,6 @@ else:
     import importlib_metadata as metadata
 
 from django.conf import settings
-from django.db import transaction
 from django.db.models import Model, QuerySet
 from django.http import HttpRequest
 from django.shortcuts import get_object_or_404
@@ -22,9 +20,6 @@ from django.utils import timezone
 from django.utils.functional import lazy
 
 from cache_memoize import cache_memoize
-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:
@@ -188,140 +183,6 @@ 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.
-
-    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.
-
-    The wrapped function returns a tuple with either
-    the return value of the task's delay method and False
-    if the method has been executed asynchronously
-    or the return value of the executed method and True
-    if the method has been executed synchronously.
-    """
-    if is_celery_enabled():
-        from ..celery import app  # noqa
-
-        task = app.task(orig)
-
-    def wrapped(*args, **kwargs):
-        if is_celery_enabled():
-            return transaction.on_commit(lambda: task.delay(*args, **kwargs)), False
-        else:
-            return orig(*args, **kwargs), True
-
-    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)
diff --git a/aleksis/core/util/search.py b/aleksis/core/util/search.py
index 5c8af9f860df94649d46f1fc0dd0741e35c6ad07..34c736f9e13b55d6ac277d41b76a374efa2b43d1 100644
--- a/aleksis/core/util/search.py
+++ b/aleksis/core/util/search.py
@@ -1,15 +1,9 @@
-from django.conf import settings
-
+from celery_haystack.indexes import CelerySearchIndex as BaseSearchIndex
 from haystack import indexes
 
 # Not used here, but simplifies imports for apps
 Indexable = indexes.Indexable  # noqa
 
-if settings.HAYSTACK_SIGNAL_PROCESSOR == "celery_haystack.signals.CelerySignalProcessor":
-    from celery_haystack.indexes import CelerySearchIndex as BaseSearchIndex
-else:
-    from haystack.indexes import SearchIndex as BaseSearchIndex
-
 
 class SearchIndex(BaseSearchIndex):
     """Base class for search indexes on AlekSIS models.
diff --git a/aleksis/core/views.py b/aleksis/core/views.py
index d110b30f63c8f6748198c3fc8268e81bb0caab9b..f1bc04013f92ed2bbde449c47cf44473be70ece0 100644
--- a/aleksis/core/views.py
+++ b/aleksis/core/views.py
@@ -1,7 +1,6 @@
 from typing import Any, Dict, Optional, Type
 
 from django.apps import apps
-from django.conf import settings
 from django.contrib.contenttypes.models import ContentType
 from django.core.exceptions import PermissionDenied
 from django.core.paginator import Paginator
@@ -18,6 +17,7 @@ from django.views.generic.detail import DetailView
 from django.views.generic.list import ListView
 
 import reversion
+from django_celery_results.models import TaskResult
 from django_tables2 import RequestConfig, SingleTableView
 from dynamic_preferences.forms import preference_form_builder
 from guardian.shortcuts import get_objects_for_user
@@ -31,6 +31,7 @@ from rules.contrib.views import PermissionRequiredMixin, permission_required
 
 from aleksis.core.data_checks import DataCheckRegistry, check_data
 
+from .celery import app
 from .filters import GroupFilter, PersonFilter
 from .forms import (
     AnnouncementForm,
@@ -411,17 +412,12 @@ class SystemStatus(PermissionRequiredMixin, MainView):
         status_code = 500 if self.errors else 200
         task_results = []
 
-        if "django_celery_results" in settings.INSTALLED_APPS:
-            from django_celery_results.models import TaskResult  # noqa
-
-            from .celery import app  # noqa
-
-            if app.control.inspect().registered_tasks():
-                job_list = list(app.control.inspect().registered_tasks().values())[0]
-                for job in job_list:
-                    task_results.append(
-                        TaskResult.objects.filter(task_name=job).order_by("date_done").last()
-                    )
+        if app.control.inspect().registered_tasks():
+            job_list = list(app.control.inspect().registered_tasks().values())[0]
+            for job in job_list:
+                task_results.append(
+                    TaskResult.objects.filter(task_name=job).order_by("date_done").last()
+                )
 
         context = {"plugins": self.plugins, "status_code": status_code, "tasks": task_results}
         return self.render_to_response(context, status=status_code)
diff --git a/poetry.lock b/poetry.lock
index 06443174db81ce59e51c3af70f2c4b9e58647671..697d09b19fd5db3dc309338bff5a534491407b85 100644
--- a/poetry.lock
+++ b/poetry.lock
@@ -49,7 +49,7 @@ name = "amqp"
 version = "5.0.5"
 description = "Low-level AMQP client for Python (fork of amqplib)."
 category = "main"
-optional = true
+optional = false
 python-versions = ">=3.6"
 
 [package.dependencies]
@@ -158,7 +158,7 @@ name = "billiard"
 version = "3.6.3.0"
 description = "Python multiprocessing fork with improvements and bugfixes"
 category = "main"
-optional = true
+optional = false
 python-versions = "*"
 
 [[package]]
@@ -229,7 +229,7 @@ name = "celery"
 version = "5.0.5"
 description = "Distributed Task Queue."
 category = "main"
-optional = true
+optional = false
 python-versions = ">=3.6,"
 
 [package.dependencies]
@@ -283,7 +283,7 @@ name = "celery-haystack-ng"
 version = "0.20.post2"
 description = "An app for integrating Celery with Haystack"
 category = "main"
-optional = true
+optional = false
 python-versions = "*"
 
 [package.dependencies]
@@ -333,7 +333,7 @@ name = "click-didyoumean"
 version = "0.0.3"
 description = "Enable git-like did-you-mean feature in click."
 category = "main"
-optional = true
+optional = false
 python-versions = "*"
 
 [package.dependencies]
@@ -344,7 +344,7 @@ name = "click-plugins"
 version = "1.1.1"
 description = "An extension module for click to enable registering CLI commands via setuptools entry-points."
 category = "main"
-optional = true
+optional = false
 python-versions = "*"
 
 [package.dependencies]
@@ -358,7 +358,7 @@ name = "click-repl"
 version = "0.1.6"
 description = "REPL plugin for Click"
 category = "main"
-optional = true
+optional = false
 python-versions = "*"
 
 [package.dependencies]
@@ -464,14 +464,14 @@ django = "*"
 
 [[package]]
 name = "django-auth-ldap"
-version = "2.2.0"
+version = "2.3.0"
 description = "Django LDAP authentication backend."
 category = "main"
 optional = true
-python-versions = ">=3.5"
+python-versions = ">=3.6"
 
 [package.dependencies]
-Django = ">=1.11"
+Django = ">=2.2"
 python-ldap = ">=3.1"
 
 [[package]]
@@ -524,7 +524,7 @@ name = "django-celery-beat"
 version = "2.2.0"
 description = "Database-backed Periodic Tasks."
 category = "main"
-optional = true
+optional = false
 python-versions = "*"
 
 [package.dependencies]
@@ -538,7 +538,7 @@ name = "django-celery-email"
 version = "3.0.0"
 description = "An async Django email backend using celery"
 category = "main"
-optional = true
+optional = false
 python-versions = "*"
 
 [package.dependencies]
@@ -551,7 +551,7 @@ name = "django-celery-results"
 version = "2.0.1"
 description = "Celery result backends for Django."
 category = "main"
-optional = true
+optional = false
 python-versions = "*"
 
 [package.dependencies]
@@ -761,7 +761,7 @@ python-versions = "*"
 
 [[package]]
 name = "django-material"
-version = "1.7.4"
+version = "1.7.5"
 description = "Material design for django forms and admin"
 category = "main"
 optional = false
@@ -977,7 +977,7 @@ name = "django-timezone-field"
 version = "4.1.1"
 description = "A Django app providing database and form fields for pytz timezone objects."
 category = "main"
-optional = true
+optional = false
 python-versions = ">=3.5"
 
 [package.dependencies]
@@ -1383,7 +1383,7 @@ name = "kombu"
 version = "5.0.2"
 description = "Messaging library for Python."
 category = "main"
-optional = true
+optional = false
 python-versions = ">=3.6"
 
 [package.dependencies]
@@ -1688,7 +1688,7 @@ python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
 
 [[package]]
 name = "pygments"
-version = "2.7.4"
+version = "2.8.0"
 description = "Pygments is a syntax highlighting package written in Python."
 category = "main"
 optional = false
@@ -1797,7 +1797,7 @@ name = "python-crontab"
 version = "2.5.1"
 description = "Python Crontab API"
 category = "main"
-optional = true
+optional = false
 python-versions = "*"
 
 [package.dependencies]
@@ -1880,7 +1880,7 @@ name = "redis"
 version = "3.5.3"
 description = "Python client for Redis key-value store"
 category = "main"
-optional = true
+optional = false
 python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
 
 [package.extras]
@@ -2029,7 +2029,7 @@ python-versions = "*"
 
 [[package]]
 name = "sphinx"
-version = "3.4.3"
+version = "3.5.1"
 description = "Python documentation generator"
 category = "dev"
 optional = false
@@ -2055,7 +2055,7 @@ sphinxcontrib-serializinghtml = "*"
 
 [package.extras]
 docs = ["sphinxcontrib-websupport"]
-lint = ["flake8 (>=3.5.0)", "isort", "mypy (>=0.790)", "docutils-stubs"]
+lint = ["flake8 (>=3.5.0)", "isort", "mypy (>=0.800)", "docutils-stubs"]
 test = ["pytest", "pytest-cov", "html5lib", "cython", "typed-ast"]
 
 [[package]]
@@ -2309,7 +2309,7 @@ name = "vine"
 version = "5.0.0"
 description = "Promises, promises, promises."
 category = "main"
-optional = true
+optional = false
 python-versions = ">=3.6"
 
 [[package]]
@@ -2352,13 +2352,12 @@ docs = ["sphinx", "jaraco.packaging (>=3.2)", "rst.linker (>=1.9)"]
 testing = ["pytest (>=3.5,!=3.7.3)", "pytest-checkdocs (>=1.2.3)", "pytest-flake8", "pytest-cov", "jaraco.test (>=3.2.0)", "jaraco.itertools", "func-timeout", "pytest-black (>=0.3.7)", "pytest-mypy"]
 
 [extras]
-celery = ["Celery", "django-celery-results", "django-celery-beat", "django-celery-email", "celery-haystack-ng"]
 ldap = ["django-auth-ldap"]
 
 [metadata]
 lock-version = "1.1"
 python-versions = "^3.7"
-content-hash = "fff497acb1f86363cd099c56eaf7fa063da88090b5d211d423e3b13c1e016a16"
+content-hash = "70b2b90f0d23ad6037d01bcadd9002a442a8e9e16bc1d09f148c9e74a30f3a4c"
 
 [metadata.files]
 alabaster = [
@@ -2549,8 +2548,8 @@ django-appconf = [
     {file = "django_appconf-1.0.4-py2.py3-none-any.whl", hash = "sha256:1b1d0e1069c843ebe8ae5aa48ec52403b1440402b320c3e3a206a0907e97bb06"},
 ]
 django-auth-ldap = [
-    {file = "django-auth-ldap-2.2.0.tar.gz", hash = "sha256:11af1773b08613339d2c3a0cec1308a4d563518f17b1719c3759994d0b4d04bf"},
-    {file = "django_auth_ldap-2.2.0-py3-none-any.whl", hash = "sha256:0ed2d88d81c39be915a9ab53b97ec0a33a3d16055518ab4c9bcffe8236d40370"},
+    {file = "django-auth-ldap-2.3.0.tar.gz", hash = "sha256:5894317122a086c9955ed366562869a81459cf6b663636b152857bb5d3a0a3b7"},
+    {file = "django_auth_ldap-2.3.0-py3-none-any.whl", hash = "sha256:cbbb476eff2504b5ab4fdf1fa92d93d2d3408fd9c8bc0c426169d987d0733153"},
 ]
 django-bleach = [
     {file = "django-bleach-0.6.1.tar.gz", hash = "sha256:674709c26040618aff0741ce8261fd151e5ead405bd50568c2034662d69daac3"},
@@ -2654,8 +2653,8 @@ django-maintenance-mode = [
     {file = "django_maintenance_mode-0.15.1-py3-none-any.whl", hash = "sha256:8c45b400253076655562c99a2ffb88f8353fc1c84496c1b9de812cc8132aea6f"},
 ]
 django-material = [
-    {file = "django-material-1.7.4.tar.gz", hash = "sha256:93af86e740b6db15a3b9df913c343217b198d7342a083db694acb319b49cb2dd"},
-    {file = "django_material-1.7.4-py2.py3-none-any.whl", hash = "sha256:70dcaa34b35dbc31fbdb7454c7a376358586d0f166abe15870e07e468d729425"},
+    {file = "django-material-1.7.5.tar.gz", hash = "sha256:d0df25b1d3ff629a4dfe2bc869550b25289f556940b45fd6d7c4897859446491"},
+    {file = "django_material-1.7.5-py2.py3-none-any.whl", hash = "sha256:141bdd1b3ded91be8c77f6de687523d63df986a559ec3eb82cd33f4af7d5983b"},
 ]
 django-menu-generator-ng = [
     {file = "django-menu-generator-ng-1.2.1.tar.gz", hash = "sha256:06097f6611913a0770d633b6fc02cc83af1d427cc42a4048ceefe5f3a0f9d3ab"},
@@ -2970,25 +2969,21 @@ pillow = [
     {file = "Pillow-8.1.0-cp36-cp36m-macosx_10_10_x86_64.whl", hash = "sha256:d355502dce85ade85a2511b40b4c61a128902f246504f7de29bbeec1ae27933a"},
     {file = "Pillow-8.1.0-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:93a473b53cc6e0b3ce6bf51b1b95b7b1e7e6084be3a07e40f79b42e83503fbf2"},
     {file = "Pillow-8.1.0-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:2353834b2c49b95e1313fb34edf18fca4d57446675d05298bb694bca4b194174"},
-    {file = "Pillow-8.1.0-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:1d208e670abfeb41b6143537a681299ef86e92d2a3dac299d3cd6830d5c7bded"},
     {file = "Pillow-8.1.0-cp36-cp36m-win32.whl", hash = "sha256:dd9eef866c70d2cbbea1ae58134eaffda0d4bfea403025f4db6859724b18ab3d"},
     {file = "Pillow-8.1.0-cp36-cp36m-win_amd64.whl", hash = "sha256:b09e10ec453de97f9a23a5aa5e30b334195e8d2ddd1ce76cc32e52ba63c8b31d"},
     {file = "Pillow-8.1.0-cp37-cp37m-macosx_10_10_x86_64.whl", hash = "sha256:b02a0b9f332086657852b1f7cb380f6a42403a6d9c42a4c34a561aa4530d5234"},
     {file = "Pillow-8.1.0-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:ca20739e303254287138234485579b28cb0d524401f83d5129b5ff9d606cb0a8"},
     {file = "Pillow-8.1.0-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:604815c55fd92e735f9738f65dabf4edc3e79f88541c221d292faec1904a4b17"},
-    {file = "Pillow-8.1.0-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:cf6e33d92b1526190a1de904df21663c46a456758c0424e4f947ae9aa6088bf7"},
     {file = "Pillow-8.1.0-cp37-cp37m-win32.whl", hash = "sha256:47c0d93ee9c8b181f353dbead6530b26980fe4f5485aa18be8f1fd3c3cbc685e"},
     {file = "Pillow-8.1.0-cp37-cp37m-win_amd64.whl", hash = "sha256:96d4dc103d1a0fa6d47c6c55a47de5f5dafd5ef0114fa10c85a1fd8e0216284b"},
     {file = "Pillow-8.1.0-cp38-cp38-macosx_10_10_x86_64.whl", hash = "sha256:7916cbc94f1c6b1301ac04510d0881b9e9feb20ae34094d3615a8a7c3db0dcc0"},
     {file = "Pillow-8.1.0-cp38-cp38-manylinux1_i686.whl", hash = "sha256:3de6b2ee4f78c6b3d89d184ade5d8fa68af0848f9b6b6da2b9ab7943ec46971a"},
     {file = "Pillow-8.1.0-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:cdbbe7dff4a677fb555a54f9bc0450f2a21a93c5ba2b44e09e54fcb72d2bd13d"},
-    {file = "Pillow-8.1.0-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:f50e7a98b0453f39000619d845be8b06e611e56ee6e8186f7f60c3b1e2f0feae"},
     {file = "Pillow-8.1.0-cp38-cp38-win32.whl", hash = "sha256:cb192176b477d49b0a327b2a5a4979552b7a58cd42037034316b8018ac3ebb59"},
     {file = "Pillow-8.1.0-cp38-cp38-win_amd64.whl", hash = "sha256:6c5275bd82711cd3dcd0af8ce0bb99113ae8911fc2952805f1d012de7d600a4c"},
     {file = "Pillow-8.1.0-cp39-cp39-macosx_10_10_x86_64.whl", hash = "sha256:165c88bc9d8dba670110c689e3cc5c71dbe4bfb984ffa7cbebf1fac9554071d6"},
     {file = "Pillow-8.1.0-cp39-cp39-manylinux1_i686.whl", hash = "sha256:5e2fe3bb2363b862671eba632537cd3a823847db4d98be95690b7e382f3d6378"},
     {file = "Pillow-8.1.0-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:7612520e5e1a371d77e1d1ca3a3ee6227eef00d0a9cddb4ef7ecb0b7396eddf7"},
-    {file = "Pillow-8.1.0-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:d673c4990acd016229a5c1c4ee8a9e6d8f481b27ade5fc3d95938697fa443ce0"},
     {file = "Pillow-8.1.0-cp39-cp39-win32.whl", hash = "sha256:dc577f4cfdda354db3ae37a572428a90ffdbe4e51eda7849bf442fb803f09c9b"},
     {file = "Pillow-8.1.0-cp39-cp39-win_amd64.whl", hash = "sha256:22d070ca2e60c99929ef274cfced04294d2368193e935c5d6febfd8b601bf865"},
     {file = "Pillow-8.1.0-pp36-pypy36_pp73-macosx_10_10_x86_64.whl", hash = "sha256:a3d3e086474ef12ef13d42e5f9b7bbf09d39cf6bd4940f982263d6954b13f6a9"},
@@ -3055,8 +3050,6 @@ psycopg2 = [
     {file = "psycopg2-2.8.6-cp37-cp37m-win_amd64.whl", hash = "sha256:56fee7f818d032f802b8eed81ef0c1232b8b42390df189cab9cfa87573fe52c5"},
     {file = "psycopg2-2.8.6-cp38-cp38-win32.whl", hash = "sha256:ad2fe8a37be669082e61fb001c185ffb58867fdbb3e7a6b0b0d2ffe232353a3e"},
     {file = "psycopg2-2.8.6-cp38-cp38-win_amd64.whl", hash = "sha256:56007a226b8e95aa980ada7abdea6b40b75ce62a433bd27cec7a8178d57f4051"},
-    {file = "psycopg2-2.8.6-cp39-cp39-win32.whl", hash = "sha256:2c93d4d16933fea5bbacbe1aaf8fa8c1348740b2e50b3735d1b0bf8154cbf0f3"},
-    {file = "psycopg2-2.8.6-cp39-cp39-win_amd64.whl", hash = "sha256:d5062ae50b222da28253059880a871dc87e099c25cb68acf613d9d227413d6f7"},
     {file = "psycopg2-2.8.6.tar.gz", hash = "sha256:fb23f6c71107c37fd667cb4ea363ddeb936b348bbd6449278eb92c189699f543"},
 ]
 ptyprocess = [
@@ -3142,8 +3135,8 @@ pyflakes = [
     {file = "pyflakes-2.2.0.tar.gz", hash = "sha256:35b2d75ee967ea93b55750aa9edbbf72813e06a66ba54438df2cfac9e3c27fc8"},
 ]
 pygments = [
-    {file = "Pygments-2.7.4-py3-none-any.whl", hash = "sha256:bc9591213a8f0e0ca1a5e68a479b4887fdc3e75d0774e5c71c31920c427de435"},
-    {file = "Pygments-2.7.4.tar.gz", hash = "sha256:df49d09b498e83c1a73128295860250b0b7edd4c723a32e9bc0d295c7c2ec337"},
+    {file = "Pygments-2.8.0-py3-none-any.whl", hash = "sha256:b21b072d0ccdf29297a82a2363359d99623597b8a265b8081760e4d0f7153c88"},
+    {file = "Pygments-2.8.0.tar.gz", hash = "sha256:37a13ba168a02ac54cc5891a42b1caec333e59b66addb7fa633ea8a6d73445c0"},
 ]
 pyjwt = [
     {file = "PyJWT-1.7.1-py2.py3-none-any.whl", hash = "sha256:5c6eca3c2940464d106b99ba83b00c6add741c9becaec087fb7ccdefea71350e"},
@@ -3283,29 +3276,22 @@ restructuredtext-lint = [
     {file = "ruamel.yaml.clib-0.2.2-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:73b3d43e04cc4b228fa6fa5d796409ece6fcb53a6c270eb2048109cbcbc3b9c2"},
     {file = "ruamel.yaml.clib-0.2.2-cp35-cp35m-macosx_10_6_intel.whl", hash = "sha256:53b9dd1abd70e257a6e32f934ebc482dac5edb8c93e23deb663eac724c30b026"},
     {file = "ruamel.yaml.clib-0.2.2-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:839dd72545ef7ba78fd2aa1a5dd07b33696adf3e68fae7f31327161c1093001b"},
-    {file = "ruamel.yaml.clib-0.2.2-cp35-cp35m-manylinux2014_aarch64.whl", hash = "sha256:1236df55e0f73cd138c0eca074ee086136c3f16a97c2ac719032c050f7e0622f"},
     {file = "ruamel.yaml.clib-0.2.2-cp35-cp35m-win32.whl", hash = "sha256:b1e981fe1aff1fd11627f531524826a4dcc1f26c726235a52fcb62ded27d150f"},
     {file = "ruamel.yaml.clib-0.2.2-cp35-cp35m-win_amd64.whl", hash = "sha256:4e52c96ca66de04be42ea2278012a2342d89f5e82b4512fb6fb7134e377e2e62"},
     {file = "ruamel.yaml.clib-0.2.2-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:a873e4d4954f865dcb60bdc4914af7eaae48fb56b60ed6daa1d6251c72f5337c"},
     {file = "ruamel.yaml.clib-0.2.2-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:ab845f1f51f7eb750a78937be9f79baea4a42c7960f5a94dde34e69f3cce1988"},
-    {file = "ruamel.yaml.clib-0.2.2-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:2fd336a5c6415c82e2deb40d08c222087febe0aebe520f4d21910629018ab0f3"},
     {file = "ruamel.yaml.clib-0.2.2-cp36-cp36m-win32.whl", hash = "sha256:e9f7d1d8c26a6a12c23421061f9022bb62704e38211fe375c645485f38df34a2"},
     {file = "ruamel.yaml.clib-0.2.2-cp36-cp36m-win_amd64.whl", hash = "sha256:2602e91bd5c1b874d6f93d3086f9830f3e907c543c7672cf293a97c3fabdcd91"},
     {file = "ruamel.yaml.clib-0.2.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:44c7b0498c39f27795224438f1a6be6c5352f82cb887bc33d962c3a3acc00df6"},
     {file = "ruamel.yaml.clib-0.2.2-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:8e8fd0a22c9d92af3a34f91e8a2594eeb35cba90ab643c5e0e643567dc8be43e"},
-    {file = "ruamel.yaml.clib-0.2.2-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:75f0ee6839532e52a3a53f80ce64925ed4aed697dd3fa890c4c918f3304bd4f4"},
     {file = "ruamel.yaml.clib-0.2.2-cp37-cp37m-win32.whl", hash = "sha256:464e66a04e740d754170be5e740657a3b3b6d2bcc567f0c3437879a6e6087ff6"},
     {file = "ruamel.yaml.clib-0.2.2-cp37-cp37m-win_amd64.whl", hash = "sha256:52ae5739e4b5d6317b52f5b040b1b6639e8af68a5b8fd606a8b08658fbd0cab5"},
     {file = "ruamel.yaml.clib-0.2.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:4df5019e7783d14b79217ad9c56edf1ba7485d614ad5a385d1b3c768635c81c0"},
     {file = "ruamel.yaml.clib-0.2.2-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:5254af7d8bdf4d5484c089f929cb7f5bafa59b4f01d4f48adda4be41e6d29f99"},
-    {file = "ruamel.yaml.clib-0.2.2-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:8be05be57dc5c7b4a0b24edcaa2f7275866d9c907725226cdde46da09367d923"},
     {file = "ruamel.yaml.clib-0.2.2-cp38-cp38-win32.whl", hash = "sha256:74161d827407f4db9072011adcfb825b5258a5ccb3d2cd518dd6c9edea9e30f1"},
     {file = "ruamel.yaml.clib-0.2.2-cp38-cp38-win_amd64.whl", hash = "sha256:058a1cc3df2a8aecc12f983a48bda99315cebf55a3b3a5463e37bb599b05727b"},
     {file = "ruamel.yaml.clib-0.2.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:c6ac7e45367b1317e56f1461719c853fd6825226f45b835df7436bb04031fd8a"},
     {file = "ruamel.yaml.clib-0.2.2-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:b4b0d31f2052b3f9f9b5327024dc629a253a83d8649d4734ca7f35b60ec3e9e5"},
-    {file = "ruamel.yaml.clib-0.2.2-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:1f8c0a4577c0e6c99d208de5c4d3fd8aceed9574bb154d7a2b21c16bb924154c"},
-    {file = "ruamel.yaml.clib-0.2.2-cp39-cp39-win32.whl", hash = "sha256:46d6d20815064e8bb023ea8628cfb7402c0f0e83de2c2227a88097e239a7dffd"},
-    {file = "ruamel.yaml.clib-0.2.2-cp39-cp39-win_amd64.whl", hash = "sha256:6c0a5dc52fc74eb87c67374a4e554d4761fd42a4d01390b7e868b30d21f4b8bb"},
     {file = "ruamel.yaml.clib-0.2.2.tar.gz", hash = "sha256:2d24bd98af676f4990c4d715bcdc2a60b19c56a3fb3a763164d2d8ca0e806ba7"},
 ]
 rules = [
@@ -3344,8 +3330,8 @@ spdx-license-list = [
     {file = "spdx_license_list-0.5.2.tar.gz", hash = "sha256:952996f72ab807972dc2278bb9b91e5294767211e51f09aad9c0e2ff5b82a31b"},
 ]
 sphinx = [
-    {file = "Sphinx-3.4.3-py3-none-any.whl", hash = "sha256:c314c857e7cd47c856d2c5adff514ac2e6495f8b8e0f886a8a37e9305dfea0d8"},
-    {file = "Sphinx-3.4.3.tar.gz", hash = "sha256:41cad293f954f7d37f803d97eb184158cfd90f51195131e94875bc07cd08b93c"},
+    {file = "Sphinx-3.5.1-py3-none-any.whl", hash = "sha256:e90161222e4d80ce5fc811ace7c6787a226b4f5951545f7f42acf97277bfc35c"},
+    {file = "Sphinx-3.5.1.tar.gz", hash = "sha256:11d521e787d9372c289472513d807277caafb1684b33eb4f08f7574c405893a9"},
 ]
 sphinx-autodoc-typehints = [
     {file = "sphinx-autodoc-typehints-1.11.1.tar.gz", hash = "sha256:244ba6d3e2fdb854622f643c7763d6f95b6886eba24bec28e86edf205e4ddb20"},
diff --git a/pyproject.toml b/pyproject.toml
index 37a8497170dc423a5040533f23cfed740ecbd036..cc3e3fe7c6b0b84438e3870728a38c4d15c2ec6b 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -68,10 +68,10 @@ html2text = "^2020.0.0"
 django-ckeditor = "^6.0.0"
 django-js-reverse = "^0.9.1"
 calendarweek = "^0.4.3"
-Celery = {version="^5.0.0", optional=true, extras=["django", "redis"]}
-django-celery-results = {version="^2.0.0", optional=true}
-django-celery-beat = {version="^2.0.0", optional=true}
-django-celery-email = {version="^3.0.0", optional=true}
+Celery = {version="^5.0.0", extras=["django", "redis"]}
+django-celery-results = "^2.0.1"
+django-celery-beat = "^2.2.0"
+django-celery-email = "^3.0.0"
 django-jsonstore = "^0.5.0"
 django-polymorphic = "^3.0.0"
 django-colorfield = "^0.3.0"
@@ -80,7 +80,7 @@ django-guardian = "^2.2.0"
 rules = "^2.2"
 django-cache-memoize = "^0.1.6"
 django-haystack = {version="3.0b1", allow-prereleases = true}
-celery-haystack-ng = {version = "^0.20", optional = true}
+celery-haystack-ng = "^0.20"
 django-dbbackup = "^3.3.0"
 spdx-license-list = "^0.5.0"
 license-expression = "^1.2"
@@ -99,7 +99,6 @@ ipython = "^7.20.0"
 
 [tool.poetry.extras]
 ldap = ["django-auth-ldap"]
-celery = ["Celery", "django-celery-results", "django-celery-beat", "django-celery-email", "celery-haystack-ng"]
 
 [tool.poetry.dev-dependencies]
 aleksis-builddeps = "*"