diff --git a/aleksis/core/apps.py b/aleksis/core/apps.py
index e5e2d4008c132c24341d26f6532c254167b13741..558c2a060899cfb35bf7dd5d3ec7eff3ef3d19ec 100644
--- a/aleksis/core/apps.py
+++ b/aleksis/core/apps.py
@@ -1,4 +1,4 @@
-from typing import Any, Optional
+from typing import TYPE_CHECKING, Any, Optional
 
 import django.apps
 from django.apps import apps
@@ -26,6 +26,9 @@ from .util.core_helpers import (
 )
 from .util.sass_helpers import clean_scss
 
+if TYPE_CHECKING:
+    from django.contrib.auth.models import User
+
 
 class CoreConfig(AppConfig):
     name = "aleksis.core"
diff --git a/aleksis/core/data_checks.py b/aleksis/core/data_checks.py
index f16ab2e9f0b3b39ea86d9f53112a4619fb960e90..c5c64f821b163ce53fa000e4169c639002cb91c1 100644
--- a/aleksis/core/data_checks.py
+++ b/aleksis/core/data_checks.py
@@ -1,5 +1,6 @@
 import logging
 from datetime import timedelta
+from typing import TYPE_CHECKING
 
 from django.apps import apps
 from django.contrib.contenttypes.models import ContentType
@@ -18,6 +19,9 @@ from .util.celery_progress import ProgressRecorder, recorded_task
 from .util.core_helpers import get_site_preferences
 from .util.email import send_email
 
+if TYPE_CHECKING:
+    from aleksis.core.models import DataCheckResult
+
 
 class SolveOption:
     """Define a solve option for one or more data checks.
@@ -49,7 +53,7 @@ class SolveOption:
     verbose_name: str = ""
 
     @classmethod
-    def solve(cls, check_result: "DataCheckResult"):
+    def solve(cls, check_result: DataCheckResult):
         pass
 
 
@@ -60,7 +64,7 @@ class IgnoreSolveOption(SolveOption):
     verbose_name = _("Ignore problem")
 
     @classmethod
-    def solve(cls, check_result: "DataCheckResult"):
+    def solve(cls, check_result: DataCheckResult):
         """Mark the object as solved without doing anything more."""
         check_result.solved = True
         check_result.save()
@@ -175,7 +179,7 @@ class DataCheck(RegistryObject):
         cls.delete_old_results()
 
     @classmethod
-    def solve(cls, check_result: "DataCheckResult", solve_option: str):
+    def solve(cls, check_result: DataCheckResult, solve_option: str):
         """Execute a solve option for an object detected by this check.
 
         :param check_result: The result item from database
@@ -192,7 +196,7 @@ class DataCheck(RegistryObject):
             solve_option_obj.solve(check_result)
 
     @classmethod
-    def register_result(cls, instance) -> "DataCheckResult":
+    def register_result(cls, instance) -> DataCheckResult:
         """Register an object with data issues in the result database.
 
         :param instance: The affected object
@@ -284,7 +288,7 @@ class DeactivateDashboardWidgetSolveOption(SolveOption):
     verbose_name = _("Deactivate DashboardWidget")
 
     @classmethod
-    def solve(cls, check_result: "DataCheckResult"):
+    def solve(cls, check_result: DataCheckResult):
         widget = check_result.related_object
         widget.active = False
         widget.save()
@@ -316,10 +320,10 @@ def field_validation_data_check_factory(app_name: str, model_name: str, field_na
 
     class FieldValidationDataCheck(DataCheck):
         name = f"field_validation_{slugify(model_name)}_{slugify(field_name)}"
-        verbose_name = _(
-            "Validate field %s of model %s." % (field_name, app_name + "." + model_name)
+        verbose_name = _("Validate field {field} of model {model}.").format(
+            field=field_name, model=app_name + "." + model_name
         )
-        problem_name = _("The field %s couldn't be validated successfully." % field_name)
+        problem_name = _("The field {} couldn't be validated successfully.").format(field_name)
         solve_options = {
             IgnoreSolveOption.name: IgnoreSolveOption,
         }
@@ -330,7 +334,7 @@ def field_validation_data_check_factory(app_name: str, model_name: str, field_na
             for obj in model.objects.all():
                 try:
                     model._meta.get_field(field_name).validate(getattr(obj, field_name), obj)
-                except ValidationError as e:
+                except ValidationError:
                     logging.info(f"Check {model_name} {obj}")
                     cls.register_result(obj)
 
diff --git a/aleksis/core/filters.py b/aleksis/core/filters.py
index 0bccc2664489f3698fc77bbe7299a9210ed81dac..5b93e37dd008556b940eac6409ef0b94e3f586f2 100644
--- a/aleksis/core/filters.py
+++ b/aleksis/core/filters.py
@@ -23,10 +23,7 @@ class MultipleCharFilter(CharFilter):
     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})
+            q = Q(**{field: value}) if not q else q | Q(**{field: value})
         return qs.filter(q)
 
     def __init__(self, fields: Sequence[str], *args, **kwargs):
diff --git a/aleksis/core/forms.py b/aleksis/core/forms.py
index 03d08a968a972df7994c4c784495b8feb91765d9..841ff59abbf6a83dc256549e101e240c39a38135 100644
--- a/aleksis/core/forms.py
+++ b/aleksis/core/forms.py
@@ -542,7 +542,6 @@ class AssignPermissionForm(forms.Form):
         all_objects = self.cleaned_data["all_objects"]
         objects = self.cleaned_data["objects"]
         permission_name = f"{self.permission.content_type.app_label}.{self.permission.codename}"
-        created = 0
 
         # Create permissions for users
         for person in persons:
@@ -621,7 +620,7 @@ class AccountRegisterForm(SignupForm, ExtensibleForm):
 
     def __init__(self, *args, **kwargs):
         request = kwargs.pop("request", None)
-        super(AccountRegisterForm, self).__init__(*args, **kwargs)
+        super().__init__(*args, **kwargs)
 
         person = None
         if request.session.get("account_verified_email"):
@@ -629,16 +628,16 @@ class AccountRegisterForm(SignupForm, ExtensibleForm):
 
             try:
                 person = Person.objects.get(email=email)
-            except (Person.DoesNotExist, Person.MultipleObjectsReturned):
-                raise SuspiciousOperation()
+            except (Person.DoesNotExist, Person.MultipleObjectsReturned) as exc:
+                raise SuspiciousOperation from exc
 
         elif request.session.get("invitation_code"):
             try:
                 invitation = PersonInvitation.objects.get(
                     key=request.session.get("invitation_code")
                 )
-            except PersonInvitation.DoesNotExist:
-                raise SuspiciousOperation()
+            except PersonInvitation.DoesNotExist as exc:
+                raise SuspiciousOperation from exc
 
             person = invitation.person
 
@@ -667,9 +666,8 @@ class AccountRegisterForm(SignupForm, ExtensibleForm):
             person_qs = Person.objects.filter(pk=self.instance.pk)
         else:
             person_qs = Person.objects.filter(email=data["email"])
-            if not person_qs.exists():
-                if get_site_preferences()["account__auto_create_person"]:
-                    Person.objects.create(user=user, **data)
+            if not person_qs.exists() and get_site_preferences()["account__auto_create_person"]:
+                Person.objects.create(user=user, **data)
         if person_qs.exists():
             person = person_qs.first()
             for field, value in data.items():
@@ -682,8 +680,8 @@ class AccountRegisterForm(SignupForm, ExtensibleForm):
 
             try:
                 invitation = PersonInvitation.objects.get(key=invitation_code)
-            except PersonInvitation.DoesNotExist:
-                raise SuspiciousOperation()
+            except PersonInvitation.DoesNotExist as exc:
+                raise SuspiciousOperation from exc
 
             accept_invitation(invitation, request, user)
         self.custom_signup(request, user)
@@ -782,7 +780,7 @@ class ActionForm(forms.Form):
             if selected_objects.count() < self.cleaned_data["selected_objects"].count():
                 raise ValidationError(
                     _("You do not have permission to run {} on all selected objects.").format(
-                        getattr(value, "short_description", value.__name__)
+                        getattr(action, "short_description", action.__name__)
                     )
                 )
         return self.cleaned_data["selected_objects"]
diff --git a/aleksis/core/management/commands/vite.py b/aleksis/core/management/commands/vite.py
index 747f328ea82060b2b0148d9c928a498e018cefc9..fe8b42312f4f26fa4dbb5389dc6382e4536bf0c2 100644
--- a/aleksis/core/management/commands/vite.py
+++ b/aleksis/core/management/commands/vite.py
@@ -17,7 +17,7 @@ class Command(BaseYarnCommand):
         parser.add_argument("--no-install", action="store_true", default=False)
 
     def handle(self, *args, **options):
-        super(Command, self).handle(*args, **options)
+        super().handle(*args, **options)
 
         # Inject settings into Vite
         write_vite_values(os.path.join(settings.NODE_MODULES_ROOT, "django-vite-values.json"))
diff --git a/aleksis/core/managers.py b/aleksis/core/managers.py
index 1c9266ab7cb65f8a2109ad4863fcc1a2614958c3..0371942562d66f8a161fcea58a2873a435a74350 100644
--- a/aleksis/core/managers.py
+++ b/aleksis/core/managers.py
@@ -1,5 +1,5 @@
 from datetime import date
-from typing import Union
+from typing import TYPE_CHECKING, Union
 
 from django.apps import apps
 from django.contrib.sites.managers import CurrentSiteManager as _CurrentSiteManager
@@ -10,6 +10,9 @@ from calendarweek import CalendarWeek
 from django_cte import CTEManager, CTEQuerySet
 from polymorphic.managers import PolymorphicManager, PolymorphicQuerySet
 
+if TYPE_CHECKING:
+    from .models import SchoolTerm
+
 
 class AlekSISBaseManager(_CurrentSiteManager):
     """Base manager for AlekSIS model customisation."""
diff --git a/aleksis/core/mixins.py b/aleksis/core/mixins.py
index 2e7aba6784f2012de091b4db4248eef88c741e30..dce75e8bdf36ace34e7ba77f3030b3aa6d36c572 100644
--- a/aleksis/core/mixins.py
+++ b/aleksis/core/mixins.py
@@ -1,17 +1,14 @@
 # flake8: noqa: DJ12
 
 import os
-from datetime import datetime
 from typing import Any, Callable, ClassVar, Iterable, List, Optional, Union
 
 from django.conf import settings
 from django.contrib import messages
 from django.contrib.auth.views import LoginView, RedirectURLMixin
-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 JSONField, QuerySet
+from django.db.models import JSONField
 from django.db.models.fields import CharField, TextField
 from django.forms.forms import BaseForm
 from django.forms.models import ModelForm, ModelFormMetaclass, fields_for_model
@@ -21,7 +18,6 @@ from django.utils.translation import gettext as _
 from django.views.generic import CreateView, UpdateView
 from django.views.generic.edit import DeleteView, ModelFormMixin
 
-import recurring_ical_events
 import reversion
 from django_ical.feedgenerator import ITEM_ELEMENT_FIELD_MAP
 from dynamic_preferences.settings import preferences_settings
@@ -138,9 +134,6 @@ class ExtensibleModel(models.Model, metaclass=_ExtensibleModelBase):
     site = models.ForeignKey(
         Site, on_delete=models.CASCADE, default=settings.SITE_ID, editable=False, related_name="+"
     )
-    objects = AlekSISBaseManager()
-    # FIXME this is now broken, remove sites framework
-    objects_all = models.Manager()
 
     managed_by_app_label = models.CharField(
         max_length=255,
@@ -149,8 +142,27 @@ class ExtensibleModel(models.Model, metaclass=_ExtensibleModelBase):
         blank=True,
     )
 
+    extended_data = JSONField(default=dict, editable=False)
+
     extra_permissions = []
 
+    # FIXME this is now broken, remove sites framework
+    objects_all = models.Manager()
+    objects = AlekSISBaseManager()
+
+    class Meta:
+        abstract = True
+
+    def save(self, *args, **kwargs):
+        """Ensure all functionality of our extensions that needs saving gets it."""
+        # For auto-created remote syncable fields
+        if hasattr(self, "_save_reverse"):
+            for related in self._save_reverse:
+                related.save()
+            del self._save_reverse
+
+        super().save(*args, **kwargs)
+
     def get_absolute_url(self) -> str:
         """Get the URL o a view representing this model instance."""
         pass
@@ -178,8 +190,6 @@ class ExtensibleModel(models.Model, metaclass=_ExtensibleModelBase):
 
         return versions_with_changes
 
-    extended_data = JSONField(default=dict, editable=False)
-
     @classmethod
     def _safe_add(cls, obj: Any, name: Optional[str]) -> None:
         # Decide the name for the attribute
@@ -276,10 +286,7 @@ class ExtensibleModel(models.Model, metaclass=_ExtensibleModelBase):
 
         @_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)
+            id_field_val = None if value is None else getattr(value, to_field)
             setattr(self, id_field_name, id_field_val)
 
         # Add property to wrap get/set on foreign model instance
@@ -305,12 +312,15 @@ class ExtensibleModel(models.Model, metaclass=_ExtensibleModelBase):
 
     @classmethod
     def syncable_fields(
-        cls, recursive: bool = True, exclude_remotes: list = []
+        cls, recursive: bool = True, exclude_remotes: list = None
     ) -> list[models.Field]:
         """Collect all fields that can be synced on a model.
 
         If recursive is True, it recurses into related models and generates virtual
         proxy fields to access fields in related models."""
+        if not exclude_remotes:
+            exclude_remotes = []
+
         fields = []
         for field in cls._meta.get_fields():
             if field.is_relation and field.one_to_one and recursive:
@@ -327,7 +337,10 @@ class ExtensibleModel(models.Model, metaclass=_ExtensibleModelBase):
                 ):
                     # generate virtual field names for proxy access
                     name = f"_{field.name}__{subfield.name}"
-                    verbose_name = f"{field.name} ({field.related_model._meta.verbose_name}) → {subfield.verbose_name}"
+                    verbose_name = (
+                        f"{field.name} ({field.related_model._meta.verbose_name})"
+                        " → {subfield.verbose_name}"
+                    )
 
                     if not hasattr(cls, name):
                         # Add proxy properties to handle access to related model
@@ -341,7 +354,7 @@ class ExtensibleModel(models.Model, metaclass=_ExtensibleModelBase):
                             {
                                 "name": name,
                                 "verbose_name": verbose_name,
-                                "to_python": lambda v: subfield.to_python(v),
+                                "to_python": lambda v: subfield.to_python(v),  # noqa: B023
                             },
                         )
                     )
@@ -371,19 +384,6 @@ class ExtensibleModel(models.Model, metaclass=_ExtensibleModelBase):
         """Annotate a ``ObjectPermissionChecker`` for use with permission system."""
         self._permission_checker = checker
 
-    def save(self, *args, **kwargs):
-        """Ensure all functionality of our extensions that needs saving gets it."""
-        # For auto-created remote syncable fields
-        if hasattr(self, "_save_reverse"):
-            for related in self._save_reverse:
-                related.save()
-            del self._save_reverse
-
-        super().save(*args, **kwargs)
-
-    class Meta:
-        abstract = True
-
 
 class _ExtensiblePolymorphicModelBase(_ExtensibleModelBase, PolymorphicModelBase):
     """Base class for extensible, polymorphic models."""
@@ -401,7 +401,7 @@ class ExtensiblePolymorphicModel(
         abstract = True
 
 
-class PureDjangoModel(object):
+class PureDjangoModel:
     """No-op mixin to mark a model as deliberately not using ExtensibleModel."""
 
     pass
@@ -423,10 +423,7 @@ class _ExtensibleFormMetaclass(ModelFormMetaclass):
         x = super().__new__(cls, name, bases, dct)
 
         # Enforce a default for the base layout for forms that o not specify one
-        if hasattr(x, "layout"):
-            base_layout = x.layout.elements
-        else:
-            base_layout = []
+        base_layout = x.layout.elements if hasattr(x, "layout") else []
 
         x.base_layout = base_layout
         x.layout = Layout(*base_layout)
diff --git a/aleksis/core/models.py b/aleksis/core/models.py
index e52bf4928231598268b076ab5582282d095112d9..b193d26340f3a6c1d52f2547340c8d42787aa4a3 100644
--- a/aleksis/core/models.py
+++ b/aleksis/core/models.py
@@ -1,10 +1,8 @@
 # flake8: noqa: DJ01
 import base64
-import hmac
-import uuid
 from datetime import date, datetime, timedelta
-from typing import Any, Iterable, Iterator, List, Optional, Sequence, Union
-from urllib.parse import urljoin, urlparse
+from typing import TYPE_CHECKING, Any, Iterable, Iterator, List, Optional, Sequence, Union
+from urllib.parse import urljoin
 
 from django.conf import settings
 from django.contrib.auth import get_user_model
@@ -16,9 +14,8 @@ from django.contrib.sites.models import Site
 from django.contrib.sites.shortcuts import get_current_site
 from django.core.exceptions import ValidationError
 from django.core.validators import MaxValueValidator
-from django.db import models, transaction
+from django.db import models
 from django.db.models import Q, QuerySet
-from django.db.models.signals import m2m_changed
 from django.dispatch import receiver
 from django.forms.widgets import Media
 from django.urls import reverse
@@ -44,7 +41,6 @@ from guardian.shortcuts import get_objects_for_user
 from icalendar import vCalAddress, vText
 from icalendar.prop import vRecur
 from invitations import signals
-from invitations.adapters import get_invitations_adapter
 from invitations.base_invitation import AbstractBaseInvitation
 from invitations.models import Invitation
 from model_utils import FieldTracker
@@ -62,9 +58,7 @@ from recurrence.fields import RecurrenceField
 from timezone_field import TimeZoneField
 
 from aleksis.core.data_checks import (
-    BrokenDashboardWidgetDataCheck,
     DataCheck,
-    field_validation_data_check_factory,
 )
 
 from .managers import (
@@ -91,6 +85,9 @@ from .util.core_helpers import generate_random_code, get_site_preferences, now_t
 from .util.email import send_email
 from .util.model_helpers import ICONS
 
+if TYPE_CHECKING:
+    from django.contrib.auth.models import User
+
 FIELD_CHOICES = (
     ("BooleanField", _("Boolean (Yes/No)")),
     ("CharField", _("Text (one line)")),
@@ -208,8 +205,11 @@ class Person(ExtensibleModel):
         verbose_name=_("Additional name(s)"), max_length=255, blank=True
     )
 
-    short_name = models.CharField(
-        verbose_name=_("Short name"), max_length=255, blank=True, null=True  # noqa
+    short_name = models.CharField(  # noqa
+        verbose_name=_("Short name"),
+        max_length=255,
+        blank=True,
+        null=True,
     )
 
     street = models.CharField(verbose_name=_("Street"), max_length=255, blank=True)
@@ -440,9 +440,8 @@ class Person(ExtensibleModel):
         pattern = pattern or get_site_preferences()["account__primary_group_pattern"]
         field = field or get_site_preferences()["account__primary_group_field"]
 
-        if pattern:
-            if force or not self.primary_group:
-                self.primary_group = self.member_of.filter(**{f"{field}__regex": pattern}).first()
+        if pattern and (force or not self.primary_group):
+            self.primary_group = self.member_of.filter(**{f"{field}__regex": pattern}).first()
 
     def notify_about_changed_data(
         self, changed_fields: Iterable[str], recipients: Optional[List[str]] = None
@@ -530,8 +529,11 @@ class Group(SchoolTermRelatedExtensibleModel):
     icon_ = "account-multiple-outline"
 
     name = models.CharField(verbose_name=_("Long name"), max_length=255)
-    short_name = models.CharField(
-        verbose_name=_("Short name"), max_length=255, blank=True, null=True  # noqa
+    short_name = models.CharField(  # noqa
+        verbose_name=_("Short name"),
+        max_length=255,
+        blank=True,
+        null=True,
     )
 
     members = models.ManyToManyField(
@@ -1406,6 +1408,9 @@ class UserAdditionalAttributes(models.Model, PureDjangoModel):
 
     attributes = models.JSONField(verbose_name=_("Additional attributes"), default=dict)
 
+    def __str__(self):
+        return str(self.user)
+
     @classmethod
     def get_user_attribute(
         cls, username: str, attribute: str, default: Optional[Any] = None
diff --git a/aleksis/core/schema/base.py b/aleksis/core/schema/base.py
index 61836a66d69681609e22134d46ed6f0b63b38b20..a5ade224d4855752f2f55bd338176f3c1b71c375 100644
--- a/aleksis/core/schema/base.py
+++ b/aleksis/core/schema/base.py
@@ -140,11 +140,11 @@ class DjangoFilterMixin:
             raise NotImplementedError(f"{cls.__name__} must implement class Meta for filtering.")
 
         if hasattr(meta, "filterset_class"):
-            filterset = getattr(meta, "filterset_class")
+            filterset = meta.filterset_class
             if filterset is not None:
                 return filterset
 
-        model: Model = getattr(meta, "model")
+        model: Model = meta.model
         fields = getattr(meta, "filter_fields", None)
 
         if not model:
diff --git a/aleksis/core/tables.py b/aleksis/core/tables.py
index 8963981bb22412b51abca69120b90dc3b0e18d72..5db97c470d7fbbcd24cd7550fd609073e3159214 100644
--- a/aleksis/core/tables.py
+++ b/aleksis/core/tables.py
@@ -165,7 +165,7 @@ class PermissionDeleteColumn(tables.LinkColumn):
             text=_("Delete"),
             attrs={"a": {"class": "btn-flat waves-effect waves-red red-text"}},
             verbose_name=_("Actions"),
-            **kwargs
+            **kwargs,
         )
 
 
diff --git a/aleksis/core/templatetags/data_helpers.py b/aleksis/core/templatetags/data_helpers.py
index ab2309f260dd6b039cc722bae86fa71637967a1f..2c08f61d82846bf3459c8b3a79ccfe63eb63ae18 100644
--- a/aleksis/core/templatetags/data_helpers.py
+++ b/aleksis/core/templatetags/data_helpers.py
@@ -13,7 +13,7 @@ def get_dict(value: Any, arg: Any) -> Any:
     """Get an attribute of an object dynamically from a string name."""
     if hasattr(value, str(arg)):
         return getattr(value, arg)
-    elif hasattr(value, "keys") and arg in value.keys():
+    elif hasattr(value, "keys") and arg in value:
         return value[arg]
     elif str(arg).isnumeric() and len(value) > int(arg):
         return value[int(arg)]
diff --git a/aleksis/core/templatetags/html_helpers.py b/aleksis/core/templatetags/html_helpers.py
index 98608a084574994400bffe46c50f944b52296e24..1005cf053b1354bef598d180c5e8659803fe29b5 100644
--- a/aleksis/core/templatetags/html_helpers.py
+++ b/aleksis/core/templatetags/html_helpers.py
@@ -23,8 +23,8 @@ def add_class_to_el(value: str, arg: str) -> str:
     el, cls = arg.split(",")
     soup = BeautifulSoup(value, "html.parser")
 
-    for el in soup.find_all(el):
-        el["class"] = el.get("class", []) + [cls]
+    for sub_el in soup.find_all(el):
+        sub_el["class"] = sub_el.get("class", []) + [cls]
 
     return str(soup)
 
@@ -58,7 +58,8 @@ def generate_random_id(prefix: str, length: int = 10) -> str:
         {% generate_random_id "prefix-" %}
     """
     return prefix + "".join(
-        random.choice(string.ascii_lowercase) for i in range(length)  # noqa: S311
+        random.choice(string.ascii_lowercase)  # noqa: S311
+        for i in range(length)
     )
 
 
diff --git a/aleksis/core/util/apps.py b/aleksis/core/util/apps.py
index 26f0752dc352cf0793ad7a9c7d70f95e015dd457..fce211f57ea53d280841d7e74ce99d30f091827e 100644
--- a/aleksis/core/util/apps.py
+++ b/aleksis/core/util/apps.py
@@ -15,6 +15,8 @@ from .core_helpers import copyright_years
 from .spdx import LICENSES
 
 if TYPE_CHECKING:
+    from django.contrib.auth.models import User
+
     from oauth2_provider.models import AbstractApplication
 
 
diff --git a/aleksis/core/util/auth_helpers.py b/aleksis/core/util/auth_helpers.py
index ca80aeae4a59ac069023465559d599f929bab6d8..018c528bd75d8942bb5ae1ebdd6ff3b1aae3a17c 100644
--- a/aleksis/core/util/auth_helpers.py
+++ b/aleksis/core/util/auth_helpers.py
@@ -77,7 +77,7 @@ class AppScopes(BaseScopes):
         application: Optional[AbstractApplication] = None,
         request: Optional[HttpRequest] = None,
         *args,
-        **kwargs
+        **kwargs,
     ) -> list[str]:
         scopes = []
         for app in AppConfig.__subclasses__():
@@ -92,7 +92,7 @@ class AppScopes(BaseScopes):
         application: Optional[AbstractApplication] = None,
         request: Optional[HttpRequest] = None,
         *args,
-        **kwargs
+        **kwargs,
     ) -> list[str]:
         scopes = []
         for app in AppConfig.__subclasses__():
diff --git a/aleksis/core/util/core_helpers.py b/aleksis/core/util/core_helpers.py
index 9329dda6192b554efa5e546a0160b5225e09da17..a04aaa2faef16f200e3a0fe175f5c4b1fe410e65 100644
--- a/aleksis/core/util/core_helpers.py
+++ b/aleksis/core/util/core_helpers.py
@@ -5,7 +5,7 @@ from importlib import import_module, metadata
 from itertools import groupby
 from operator import itemgetter
 from types import ModuleType
-from typing import Any, Callable, Dict, Optional, Sequence, Union
+from typing import TYPE_CHECKING, Any, Callable, Dict, Optional, Sequence, Union
 from warnings import warn
 
 from django.conf import settings
@@ -27,6 +27,11 @@ from cachalot.signals import post_invalidation
 from cache_memoize import cache_memoize
 from icalendar import Calendar, Event, Todo
 
+if TYPE_CHECKING:
+    from django.contrib.contenttypes.models import ContentType
+
+    from favicon.models import Favicon
+
 
 def copyright_years(years: Sequence[int], separator: str = ", ", joiner: str = "–") -> str:
     """Take a sequence of integers and produces a string with ranges.
@@ -172,7 +177,8 @@ def get_or_create_favicon(title: str, default: str, is_favicon: bool = False) ->
         changed = True
 
     if created:
-        favicon.faviconImage.save(os.path.basename(default), File(open(default, "rb")))
+        with open(default, "rb") as f:
+            favicon.faviconImage.save(os.path.basename(default), File(f))
         changed = True
 
     if changed:
@@ -213,12 +219,10 @@ def has_person(obj: Union[HttpRequest, Model]) -> bool:
         return False
 
     person = getattr(obj, "person", None)
-    if person is None:
+    if person is None or getattr(person, "is_dummy", False):
         return False
-    elif getattr(person, "is_dummy", False):
-        return False
-    else:
-        return True
+
+    return True
 
 
 def custom_information_processor(request: Union[HttpRequest, None]) -> dict:
@@ -533,10 +537,8 @@ class ExtendedICal20Feed(feedgenerator.ICal20Feed):
     def write_items(self, calendar, with_meta=True):
         for item in self.items:
             component_type = item.get("component_type")
-            if component_type == "todo":
-                element = Todo()
-            else:
-                element = Event()
+            element = Todo() if component_type == "todo" else Event()
+
             for ifield, efield in feedgenerator.ITEM_ELEMENT_FIELD_MAP:
                 val = item.get(ifield)
                 if val is not None:
diff --git a/aleksis/core/util/notifications.py b/aleksis/core/util/notifications.py
index 061b8d3d8fae61f68b1c3f2fa9e9f3f421491292..183858f9407ce5afb04c1dd8b60693ec4973cd02 100644
--- a/aleksis/core/util/notifications.py
+++ b/aleksis/core/util/notifications.py
@@ -1,6 +1,6 @@
 """Utility code for notification system."""
 
-from typing import Sequence, Union
+from typing import TYPE_CHECKING, Sequence, Union
 
 from django.apps import apps
 from django.conf import settings
@@ -17,6 +17,9 @@ try:
 except ImportError:
     TwilioClient = None
 
+if TYPE_CHECKING:
+    from ..models import Notification
+
 
 def send_templated_sms(
     template_name: str, from_number: str, recipient_list: Sequence[str], context: dict
@@ -95,7 +98,7 @@ def get_notification_choices() -> list:
     by the administrator (by selecting a subset of these choices).
     """
     choices = []
-    for channel, (name, check, send) in _CHANNELS_MAP.items():
+    for channel, (name, check, send) in _CHANNELS_MAP.items():  # noqa: B007
         if check():
             choices.append((channel, name))
     return choices
diff --git a/aleksis/core/util/predicates.py b/aleksis/core/util/predicates.py
index bdf3a03a3944325ab957e8586fce6fa24a693836..3d91751938c785baf694f379b4126fb879e6a0fc 100644
--- a/aleksis/core/util/predicates.py
+++ b/aleksis/core/util/predicates.py
@@ -12,9 +12,8 @@ from rules import predicate
 
 from ..mixins import ExtensibleModel
 from ..models import Group, PersonalEvent
-from .core_helpers import get_content_type_by_perm, get_site_preferences
+from .core_helpers import get_content_type_by_perm, get_site_preferences, queryset_rules_filter
 from .core_helpers import has_person as has_person_helper
-from .core_helpers import queryset_rules_filter
 
 
 def permission_validator(request: HttpRequest, perm: str) -> bool:
diff --git a/aleksis/core/util/spdx.py b/aleksis/core/util/spdx.py
index 76452ba4a3c5a03b2f065350b13c3adde7a90d57..39a02d95349ae72876090c9d575edbde6b805d92 100644
--- a/aleksis/core/util/spdx.py
+++ b/aleksis/core/util/spdx.py
@@ -1,5 +1,5 @@
 import json
 import os
 
-with open(os.path.join(os.path.dirname(__file__), "licenses.json"), "r") as f:
+with open(os.path.join(os.path.dirname(__file__), "licenses.json")) as f:
     LICENSES = json.load(f)
diff --git a/aleksis/core/views.py b/aleksis/core/views.py
index f9e21e5b57472664e96bded0ebb471a11aff014b..f9e87eca9634022de2e86c8c64225ed29b9e8d35 100644
--- a/aleksis/core/views.py
+++ b/aleksis/core/views.py
@@ -8,7 +8,7 @@ from django.contrib.auth.mixins import LoginRequiredMixin
 from django.contrib.auth.models import Group as DjangoGroup
 from django.contrib.auth.models import Permission, User
 from django.contrib.contenttypes.models import ContentType
-from django.core.exceptions import PermissionDenied, ValidationError
+from django.core.exceptions import BadRequest, PermissionDenied, ValidationError
 from django.core.paginator import Paginator
 from django.db.models import QuerySet
 from django.forms.models import BaseModelForm, modelform_factory
@@ -151,11 +151,8 @@ from .util.pdf import render_pdf
 class LogoView(View):
     def get(self, request: HttpRequest, *args, **kwargs) -> HttpResponse:
         image = request.site.preferences["theme__logo"]
-        if image:
-            image = image.url
-        else:
-            image = static("img/aleksis-banner.svg")
-        return redirect(image)
+        image_url = image.url if image else static("img/aleksis-banner.svg")
+        return redirect(image_url)
 
 
 class RenderPDFView(TemplateView):
@@ -178,9 +175,8 @@ class ServiceWorkerView(View):
     """
 
     def get(self, request: HttpRequest, *args, **kwargs) -> HttpResponse:
-        return HttpResponse(
-            open(settings.SERVICE_WORKER_PATH, "rt"), content_type="application/javascript"
-        )
+        with open(settings.SERVICE_WORKER_PATH) as f:
+            return HttpResponse(f, content_type="application/javascript")
 
 
 class ManifestView(View):
@@ -232,23 +228,13 @@ def index(request: HttpRequest) -> HttpResponse:
     if has_person(request.user):
         person = request.user.person
         widgets = person.dashboard_widgets
-        activities = person.activities.all().order_by("-created")[:5]
-        notifications = person.notifications.filter(send_at__lte=timezone.now()).order_by(
-            "-created"
-        )[:5]
-        unread_notifications = person.notifications.filter(
-            send_at__lte=timezone.now(), read=False
-        ).order_by("-created")
         announcements = Announcement.objects.at_time().for_person(person)
         activities = person.activities.all().order_by("-created")[:5]
-
     else:
         person = None
-        activities = []
-        notifications = []
-        unread_notifications = []
         widgets = []
         announcements = []
+        activities = []
 
     if len(widgets) == 0:
         # Use default dashboard if there are no widgets
@@ -441,15 +427,14 @@ def edit_group(request: HttpRequest, id_: Optional[int] = None) -> HttpResponse:
         else:
             raise PermissionDenied()
 
-    if request.method == "POST":
-        if edit_group_form.is_valid():
-            with reversion.create_revision():
-                set_user(request.user)
-                group = edit_group_form.save(commit=True)
+    if edit_group_form.is_valid():
+        with reversion.create_revision():
+            set_user(request.user)
+            group = edit_group_form.save(commit=True)
 
-            messages.success(request, _("The group has been saved."))
+        messages.success(request, _("The group has been saved."))
 
-            return redirect("group_by_id", group.pk)
+        return redirect("group_by_id", group.pk)
 
     context["edit_group_form"] = edit_group_form
 
@@ -551,12 +536,11 @@ def announcement_form(request: HttpRequest, id_: Optional[int] = None) -> HttpRe
         form = AnnouncementForm(request.POST or None)
         context["mode"] = "add"
 
-    if request.method == "POST":
-        if form.is_valid():
-            form.save()
+    if form.is_valid():
+        form.save()
 
-            messages.success(request, _("The announcement has been saved."))
-            return redirect("announcements")
+        messages.success(request, _("The announcement has been saved."))
+        return redirect("announcements")
 
     context["form"] = form
 
@@ -725,13 +709,12 @@ def edit_additional_field(request: HttpRequest, id_: Optional[int] = None) -> Ht
         else:
             raise PermissionDenied()
 
-    if request.method == "POST":
-        if edit_additional_field_form.is_valid():
-            edit_additional_field_form.save(commit=True)
+    if edit_additional_field_form.is_valid():
+        edit_additional_field_form.save(commit=True)
 
-            messages.success(request, _("The additional field has been saved."))
+        messages.success(request, _("The additional field has been saved."))
 
-            return redirect("additional_fields")
+        return redirect("additional_fields")
 
     context["edit_additional_field_form"] = edit_additional_field_form
 
@@ -785,13 +768,12 @@ def edit_group_type(request: HttpRequest, id_: Optional[int] = None) -> HttpResp
         # Empty form to create a new group_type
         edit_group_type_form = EditGroupTypeForm(request.POST or None)
 
-    if request.method == "POST":
-        if edit_group_type_form.is_valid():
-            edit_group_type_form.save(commit=True)
+    if edit_group_type_form.is_valid():
+        edit_group_type_form.save(commit=True)
 
-            messages.success(request, _("The group type has been saved."))
+        messages.success(request, _("The group type has been saved."))
 
-            return redirect("group_types")
+        return redirect("group_types")
 
     context["edit_group_type_form"] = edit_group_type_form
 
@@ -1388,10 +1370,10 @@ class AccountRegisterView(SignupView):
             and not request.session.get("invitation_code")
         ):
             raise PermissionDenied()
-        return super(AccountRegisterView, self).dispatch(request, *args, **kwargs)
+        return super().dispatch(request, *args, **kwargs)
 
     def get_form_kwargs(self):
-        kwargs = super(AccountRegisterView, self).get_form_kwargs()
+        kwargs = super().get_form_kwargs()
         kwargs["request"] = self.request
         return kwargs
 
@@ -1564,8 +1546,8 @@ class ObjectRepresentationView(View):
         """Get the model by app label and model name."""
         try:
             return apps.get_model(app_label, model)
-        except LookupError:
-            raise Http404()
+        except LookupError as exc:
+            raise Http404 from exc
 
     def get_object(self, request: HttpRequest, app_label: str, model: str, pk: int):
         """Get the object by app label, model name and primary key."""
@@ -1574,8 +1556,8 @@ class ObjectRepresentationView(View):
 
         try:
             return self.model.objects.get(pk=pk)
-        except self.model.DoesNotExist:
-            raise Http404()
+        except self.model.DoesNotExist as exc:
+            raise Http404 from exc
 
     def get(
         self,
diff --git a/conftest.py b/conftest.py
index 2aa286cdb37550088652b10cced7bfc3d3b56aa9..e0de0cc90762c0d5230d0aaa7c296d0b7d121b24 100644
--- a/conftest.py
+++ b/conftest.py
@@ -1 +1 @@
-pytest_plugins = ("celery.contrib.pytest", )
+pytest_plugins = ("celery.contrib.pytest",)
diff --git a/docs/conf.py b/docs/conf.py
index 4f14591b931a77ed87e0e2cc7af56b48805e820b..8dbdcee464e976d14ea50a117c447c1062e78fe3 100644
--- a/docs/conf.py
+++ b/docs/conf.py
@@ -117,9 +117,7 @@ html_static_path = ["_static"]
 # default: ``['localtoc.html', 'relations.html', 'sourcelink.html',
 # 'searchbox.html']``.
 #
-html_sidebars = {
-    "**": ["logo-text.html", "globaltoc.html", "localtoc.html", "searchbox.html"]
-}
+html_sidebars = {"**": ["logo-text.html", "globaltoc.html", "localtoc.html", "searchbox.html"]}
 
 
 # -- Options for HTMLHelp output ---------------------------------------------