From 2b7f93149f58fe9e4a8de09891eb3d840f0d33d3 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Benjamin=20H=C3=A4ttasch?=
 <benjamin.haettasch@fachschaft.informatik.tu-darmstadt.de>
Date: Thu, 10 Aug 2023 14:41:35 +0200
Subject: [PATCH] Improve AKModel

Add or complete docstrings
Remove code smells
Disable irrelevant warnings
Update translations to changed line numbers and line breaks
Move duplicated code for event field pre-population and event timezone adaption to mixins
---
 AKModel/admin.py                            | 318 ++++++++----
 AKModel/apps.py                             |   7 +
 AKModel/availability/forms.py               |  72 ++-
 AKModel/availability/models.py              |  29 +-
 AKModel/availability/serializers.py         |  24 +-
 AKModel/environment.py                      |  15 +-
 AKModel/forms.py                            |  83 ++-
 AKModel/locale/de_DE/LC_MESSAGES/django.po  | 526 ++++++++++----------
 AKModel/management/commands/makemessages.py |  22 +-
 AKModel/metaviews/__init__.py               |   2 +
 AKModel/metaviews/admin.py                  | 109 +++-
 AKModel/metaviews/status.py                 |  47 +-
 AKModel/models.py                           | 260 ++++++++--
 AKModel/serializers.py                      |  21 +
 AKModel/site.py                             |  11 +-
 AKModel/templatetags/tags_AKModel.py        |  49 +-
 AKModel/tests.py                            | 115 ++++-
 AKModel/urls.py                             |  22 +-
 AKModel/views/ak.py                         |  34 +-
 AKModel/views/api.py                        |  24 +
 AKModel/views/event_wizard.py               |  55 +-
 AKModel/views/manage.py                     |  53 +-
 AKModel/views/room.py                       |  32 +-
 AKModel/views/status.py                     |  45 +-
 24 files changed, 1479 insertions(+), 496 deletions(-)

diff --git a/AKModel/admin.py b/AKModel/admin.py
index 0d48bc6f..29c14a05 100644
--- a/AKModel/admin.py
+++ b/AKModel/admin.py
@@ -18,12 +18,15 @@ from AKModel.models import Event, AKOwner, AKCategory, AKTrack, AKRequirement, A
     ConstraintViolation, DefaultSlot
 from AKModel.urls import get_admin_urls_event_wizard, get_admin_urls_event
 from AKModel.views.ak import AKResetInterestView, AKResetInterestCounterView
-from AKModel.views.manage import PlanPublishView, PlanUnpublishView, DefaultSlotEditorView, CVMarkResolvedView, \
-    CVSetLevelViolationView, CVSetLevelWarningView
-from AKModel.views.room import RoomBatchCreationView
+from AKModel.views.manage import CVMarkResolvedView, CVSetLevelViolationView, CVSetLevelWarningView
 
 
 class EventRelatedFieldListFilter(RelatedFieldListFilter):
+    """
+    Reusable filter to restrict the possible choices of a field to those belonging to a certain event
+    as specified in the event__id__exact GET parameter.
+    The choices are only restricted if this parameter is present, otherwise all choices are used/returned
+    """
     def field_choices(self, field, request, model_admin):
         ordering = self.field_admin_ordering(field, request, model_admin)
         limit_choices = {}
@@ -34,6 +37,17 @@ class EventRelatedFieldListFilter(RelatedFieldListFilter):
 
 @admin.register(Event)
 class EventAdmin(admin.ModelAdmin):
+    """
+    Admin interface for Event
+
+    This allows to edit most fields of an event, some can only be changed by admin actions, since they have side effects
+
+    This admin interface registers additional views as defined in urls.py, the wizard, and the full scheduling
+    functionality if the AKScheduling app is active.
+
+    The interface overrides the built-in creation interface for a new event and replaces it with the event creation
+    wizard.
+    """
     model = Event
     list_display = ['name', 'status_url', 'place', 'start', 'end', 'active', 'plan_hidden']
     list_filter = ['active']
@@ -43,32 +57,54 @@ class EventAdmin(admin.ModelAdmin):
     actions = ['publish', 'unpublish']
 
     def add_view(self, request, form_url='', extra_context=None):
-        # Always use wizard to create new events (the built-in form wouldn't work anyways since the timezone cannot
+        # Override
+        # Always use wizard to create new events (the built-in form wouldn't work anyway since the timezone cannot
         # be specified before starting to fill the form)
         return redirect("admin:new_event_wizard_start")
 
     def get_urls(self):
+        """
+        Get all event-related URLs
+        This will be both the built-in URLs and additional views providing additional functionality
+        :return: list of all relevant URLs
+        :rtype: List[path]
+        """
+        # Load wizard URLs and the additional URLs defined in urls.py
+        # (first, to have the highest priority when overriding views)
         urls = get_admin_urls_event_wizard(self.admin_site)
         urls.extend(get_admin_urls_event(self.admin_site))
+
+        # Make scheduling admin views available if app is active
         if apps.is_installed("AKScheduling"):
-            from AKScheduling.urls import get_admin_urls_scheduling
+            from AKScheduling.urls import get_admin_urls_scheduling  # pylint: disable=import-outside-toplevel
             urls.extend(get_admin_urls_scheduling(self.admin_site))
-        urls.extend([
-            path('plan/publish/', self.admin_site.admin_view(PlanPublishView.as_view()), name="plan-publish"),
-            path('plan/unpublish/', self.admin_site.admin_view(PlanUnpublishView.as_view()), name="plan-unpublish"),
-            path('<slug:event_slug>/defaultSlots/', self.admin_site.admin_view(DefaultSlotEditorView.as_view()), name="default-slots-editor"),
-            path('<slug:event_slug>/importRooms/', self.admin_site.admin_view(RoomBatchCreationView.as_view()), name="room-import"),
-        ])
+
+        # Make sure built-in URLs are available as well
         urls.extend(super().get_urls())
         return urls
 
     @display(description=_("Status"))
     def status_url(self, obj):
+        """
+        Define a read-only field to go to the status page of the event
+
+        :param obj: the event to link
+        :return: status page link (HTML)
+        :rtype: str
+        """
         return format_html("<a href='{url}'>{text}</a>",
                            url=reverse_lazy('admin:event_status', kwargs={'event_slug': obj.slug}), text=_("Status"))
 
     @display(description=_("Toggle plan visibility"))
     def toggle_plan_visibility(self, obj):
+        """
+        Define a read-only field to toggle the visibility of the plan of this event
+        This will choose from two different link targets/views depending on the current visibility status
+
+        :param obj: event to change the visibility of the plan for
+        :return: toggling link (HTML)
+        :rtype: str
+        """
         if obj.plan_hidden:
             url = f"{reverse_lazy('admin:plan-publish')}?pks={obj.pk}"
             text = _('Publish plan')
@@ -78,78 +114,97 @@ class EventAdmin(admin.ModelAdmin):
         return format_html("<a href='{url}'>{text}</a>", url=url, text=text)
 
     def get_form(self, request, obj=None, change=False, **kwargs):
-        # Use timezone of event
+        # Override (update) form rendering to make sure the timezone of the event is used
         timezone.activate(obj.timezone)
         return super().get_form(request, obj, change, **kwargs)
 
     @action(description=_('Publish plan'))
     def publish(self, request, queryset):
+        """
+        Admin action to publish the plan
+        """
         selected = queryset.values_list('pk', flat=True)
         return HttpResponseRedirect(f"{reverse_lazy('admin:plan-publish')}?pks={','.join(str(pk) for pk in selected)}")
 
     @action(description=_('Unpublish plan'))
     def unpublish(self, request, queryset):
+        """
+        Admin action to hide the plan
+        """
         selected = queryset.values_list('pk', flat=True)
-        return HttpResponseRedirect(f"{reverse_lazy('admin:plan-unpublish')}?pks={','.join(str(pk) for pk in selected)}")
+        return HttpResponseRedirect(
+            f"{reverse_lazy('admin:plan-unpublish')}?pks={','.join(str(pk) for pk in selected)}")
+
+
+class PrepopulateWithNextActiveEventMixin:
+    """
+    Mixin for automated pre-population of the event field
+    """
+    # pylint: disable=too-few-public-methods
+
+    def formfield_for_foreignkey(self, db_field, request, **kwargs):
+        """
+        Override field generation for foreign key fields to introduce special handling for event fields:
+        Pre-populate the event field with the next active event (since that is the most likeliest event to be worked
+        on in the admin interface) to make creation of new owners easier
+        """
+        if db_field.name == 'event':
+            kwargs['initial'] = Event.get_next_active()
+        return super().formfield_for_foreignkey(db_field, request, **kwargs)
 
 
 @admin.register(AKOwner)
-class AKOwnerAdmin(admin.ModelAdmin):
+class AKOwnerAdmin(PrepopulateWithNextActiveEventMixin, admin.ModelAdmin):
+    """
+    Admin interface for AKOwner
+    """
     model = AKOwner
     list_display = ['name', 'institution', 'event']
     list_filter = ['event', 'institution']
     list_editable = []
     ordering = ['name']
 
-    def formfield_for_foreignkey(self, db_field, request, **kwargs):
-        if db_field.name == 'event':
-            kwargs['initial'] = Event.get_next_active()
-        return super(AKOwnerAdmin, self).formfield_for_foreignkey(db_field, request, **kwargs)
-
 
 @admin.register(AKCategory)
-class AKCategoryAdmin(admin.ModelAdmin):
+class AKCategoryAdmin(PrepopulateWithNextActiveEventMixin, admin.ModelAdmin):
+    """
+    Admin interface for AKCategory
+    """
     model = AKCategory
     list_display = ['name', 'color', 'event']
     list_filter = ['event']
     list_editable = ['color']
     ordering = ['name']
 
-    def formfield_for_foreignkey(self, db_field, request, **kwargs):
-        if db_field.name == 'event':
-            kwargs['initial'] = Event.get_next_active()
-        return super(AKCategoryAdmin, self).formfield_for_foreignkey(db_field, request, **kwargs)
-
 
 @admin.register(AKTrack)
-class AKTrackAdmin(admin.ModelAdmin):
+class AKTrackAdmin(PrepopulateWithNextActiveEventMixin, admin.ModelAdmin):
+    """
+    Admin interface for AKTrack
+    """
     model = AKTrack
     list_display = ['name', 'color', 'event']
     list_filter = ['event']
     list_editable = ['color']
     ordering = ['name']
 
-    def formfield_for_foreignkey(self, db_field, request, **kwargs):
-        if db_field.name == 'event':
-            kwargs['initial'] = Event.get_next_active()
-        return super(AKTrackAdmin, self).formfield_for_foreignkey(db_field, request, **kwargs)
-
 
 @admin.register(AKRequirement)
-class AKRequirementAdmin(admin.ModelAdmin):
+class AKRequirementAdmin(PrepopulateWithNextActiveEventMixin, admin.ModelAdmin):
+    """
+    Admin interface for AKRequirements
+    """
     model = AKRequirement
     list_display = ['name', 'event']
     list_filter = ['event']
     list_editable = []
     ordering = ['name']
 
-    def formfield_for_foreignkey(self, db_field, request, **kwargs):
-        if db_field.name == 'event':
-            kwargs['initial'] = Event.get_next_active()
-        return super(AKRequirementAdmin, self).formfield_for_foreignkey(db_field, request, **kwargs)
-
 
 class WishFilter(SimpleListFilter):
+    """
+    Re-usable filter for wishes
+    """
     title = _("Wish")  # a label for our filter
     parameter_name = 'wishes'  # you can put anything here
 
@@ -170,6 +225,9 @@ class WishFilter(SimpleListFilter):
 
 
 class AKAdminForm(forms.ModelForm):
+    """
+    Modified admin form for AKs, to be used in :class:`AKAdmin`
+    """
     class Meta:
         widgets = {
             'requirements': forms.CheckboxSelectMultiple,
@@ -188,10 +246,19 @@ class AKAdminForm(forms.ModelForm):
 
 
 @admin.register(AK)
-class AKAdmin(SimpleHistoryAdmin):
+class AKAdmin(PrepopulateWithNextActiveEventMixin, SimpleHistoryAdmin):
+    """
+    Admin interface for AKs
+
+    Uses a modified form (see :class:`AKAdminForm`)
+    """
     model = AK
     list_display = ['name', 'short_name', 'category', 'track', 'is_wish', 'interest', 'interest_counter', 'event']
-    list_filter = ['event', WishFilter, ('category', EventRelatedFieldListFilter), ('requirements', EventRelatedFieldListFilter)]
+    list_filter = ['event',
+                   WishFilter,
+                   ('category', EventRelatedFieldListFilter),
+                   ('requirements', EventRelatedFieldListFilter)
+                   ]
     list_editable = ['short_name', 'track', 'interest_counter']
     ordering = ['pk']
     actions = ['wiki_export', 'reset_interest', 'reset_interest_counter']
@@ -199,25 +266,36 @@ class AKAdmin(SimpleHistoryAdmin):
 
     @display(boolean=True)
     def is_wish(self, obj):
+        """
+        Property: Is this AK a wish?
+        """
         return obj.wish
 
     @action(description=_("Export to wiki syntax"))
     def wiki_export(self, request, queryset):
+        """
+        Action: Export to wiki syntax
+        This will use the wiki export view (therefore, all AKs have to have the same event to correclty handle the
+        categories and to prevent accidentially merging AKs from different events in the wiki)
+        but restrict the AKs to the ones explicitly selected here.
+        """
         # Only export when all AKs belong to the same event
         if queryset.values("event").distinct().count() == 1:
             event = queryset.first().event
             pks = set(ak.pk for ak in queryset.all())
-            categories_with_aks = event.get_categories_with_aks(wishes_seperately=False, filter=lambda ak: ak.pk in pks,
+            categories_with_aks = event.get_categories_with_aks(wishes_seperately=False,
+                                                                filter_func=lambda ak: ak.pk in pks,
                                                                 hide_empty_categories=True)
-            return render(request, 'admin/AKModel/wiki_export.html', context={"categories_with_aks": categories_with_aks})
+            return render(request, 'admin/AKModel/wiki_export.html',
+                          context={"categories_with_aks": categories_with_aks})
         self.message_user(request, _("Cannot export AKs from more than one event at the same time."), messages.ERROR)
-
-    def formfield_for_foreignkey(self, db_field, request, **kwargs):
-        if db_field.name == 'event':
-            kwargs['initial'] = Event.get_next_active()
-        return super(AKAdmin, self).formfield_for_foreignkey(db_field, request, **kwargs)
+        return redirect('admin:AKModel_ak_changelist')
 
     def get_urls(self):
+        """
+        Add additional URLs/views
+        Currently used to reset the interest field and interest counter field
+        """
         urls = [
             path('reset-interest/', AKResetInterestView.as_view(), name="ak-reset-interest"),
             path('reset-interest-counter/', AKResetInterestCounterView.as_view(), name="ak-reset-interest-counter"),
@@ -227,17 +305,30 @@ class AKAdmin(SimpleHistoryAdmin):
 
     @action(description=_("Reset interest in AKs"))
     def reset_interest(self, request, queryset):
+        """
+        Action: Reset interest field for the given AKs
+        Will use a typical admin confirmation view flow
+        """
         selected = queryset.values_list('pk', flat=True)
-        return HttpResponseRedirect(f"{reverse_lazy('admin:ak-reset-interest')}?pks={','.join(str(pk) for pk in selected)}")
+        return HttpResponseRedirect(
+            f"{reverse_lazy('admin:ak-reset-interest')}?pks={','.join(str(pk) for pk in selected)}")
 
     @action(description=_("Reset AKs' interest counters"))
     def reset_interest_counter(self, request, queryset):
+        """
+        Action: Reset interest counter field for the given AKs
+        Will use a typical admin confirmation view flow
+        """
         selected = queryset.values_list('pk', flat=True)
-        return HttpResponseRedirect(f"{reverse_lazy('admin:ak-reset-interest-counter')}?pks={','.join(str(pk) for pk in selected)}")
+        return HttpResponseRedirect(
+            f"{reverse_lazy('admin:ak-reset-interest-counter')}?pks={','.join(str(pk) for pk in selected)}")
 
 
 @admin.register(Room)
-class RoomAdmin(admin.ModelAdmin):
+class RoomAdmin(PrepopulateWithNextActiveEventMixin, admin.ModelAdmin):
+    """
+    Admin interface for Rooms
+    """
     model = Room
     list_display = ['name', 'location', 'capacity', 'event']
     list_filter = ['event', ('properties', EventRelatedFieldListFilter), 'location']
@@ -246,26 +337,29 @@ class RoomAdmin(admin.ModelAdmin):
     change_form_template = "admin/AKModel/room_change_form.html"
 
     def add_view(self, request, form_url='', extra_context=None):
+        # Override creation view
         # Use custom view for room creation (either room form or combined form if virtual rooms are supported)
         return redirect("admin:room-new")
 
     def get_form(self, request, obj=None, change=False, **kwargs):
+        # Override form creation to use a form that allows to specify availabilites of the room once this room is
+        # associated with an event (so not before the first saving) since the timezone information and event start
+        # and end are needed to correclty render the calendar
         if obj is not None:
             return RoomFormWithAvailabilities
         return super().get_form(request, obj, change, **kwargs)
 
-    def formfield_for_foreignkey(self, db_field, request, **kwargs):
-        if db_field.name == 'event':
-            kwargs['initial'] = Event.get_next_active()
-        return super(RoomAdmin, self).formfield_for_foreignkey(
-            db_field, request, **kwargs
-        )
-
     def get_urls(self):
+        """
+        Add additional URLs/views
+        This is currently used to adapt the creation form behavior, to allow the creation of virtual rooms in-place
+        when the support for virtual rooms is turned on (AKOnline app active)
+        """
+        # pylint: disable=import-outside-toplevel
         if apps.is_installed("AKOnline"):
             from AKOnline.views import RoomCreationWithVirtualView as RoomCreationView
         else:
-            from .views import RoomCreationView
+            from .views.room import RoomCreationView
 
         urls = [
             path('new/', self.admin_site.admin_view(RoomCreationView.as_view()), name="room-new"),
@@ -274,7 +368,28 @@ class RoomAdmin(admin.ModelAdmin):
         return urls
 
 
+class EventTimezoneFormMixin:
+    """
+    Mixin to enforce the usage of the timezone of the associated event in forms
+    """
+    # pylint: disable=too-few-public-methods
+
+    def get_form(self, request, obj=None, change=False, **kwargs):
+        """
+        Override form creation, use timezone of associated event
+        """
+        if obj is not None and obj.event.timezone:
+            timezone.activate(obj.event.timezone)
+        # No timezone available? Use UTC
+        else:
+            timezone.activate("UTC")
+        return super().get_form(request, obj, change, **kwargs)
+
+
 class AKSlotAdminForm(forms.ModelForm):
+    """
+    Modified admin form for AKSlots, to be used in :class:`AKSlotAdmin`
+    """
     def __init__(self, *args, **kwargs):
         super().__init__(*args, **kwargs)
         # Filter possible values for foreign keys when event is specified
@@ -284,7 +399,12 @@ class AKSlotAdminForm(forms.ModelForm):
 
 
 @admin.register(AKSlot)
-class AKSlotAdmin(admin.ModelAdmin):
+class AKSlotAdmin(EventTimezoneFormMixin, PrepopulateWithNextActiveEventMixin, admin.ModelAdmin):
+    """
+    Admin interface for AKSlots
+
+    Uses a modified form (see :class:`AKSlotAdminForm`)
+    """
     model = AKSlot
     list_display = ['id', 'ak', 'room', 'start', 'duration', 'event']
     list_filter = ['event', ('room', EventRelatedFieldListFilter)]
@@ -292,22 +412,15 @@ class AKSlotAdmin(admin.ModelAdmin):
     readonly_fields = ['ak_details_link', 'updated']
     form = AKSlotAdminForm
 
-    def get_form(self, request, obj=None, change=False, **kwargs):
-        # Use timezone of associated event
-        if obj is not None and obj.event.timezone:
-            timezone.activate(obj.event.timezone)
-        # No timezone available? Use UTC
-        else:
-            timezone.activate("UTC")
-        return super().get_form(request, obj, change, **kwargs)
-
-    def formfield_for_foreignkey(self, db_field, request, **kwargs):
-        if db_field.name == 'event':
-            kwargs['initial'] = Event.get_next_active()
-        return super(AKSlotAdmin, self).formfield_for_foreignkey(db_field, request, **kwargs)
-
     @display(description=_('AK Details'))
     def ak_details_link(self, akslot):
+        """
+        Define a read-only field to link the details of the associated AK
+
+        :param obj: the AK to link
+        :return: AK detail page page link (HTML)
+        :rtype: str
+        """
         if apps.is_installed("AKSubmission") and akslot.ak is not None:
             link = f"<a href={{ akslot.detail_url }}>{str(akslot.ak)}</a>"
             return mark_safe(link)
@@ -317,25 +430,28 @@ class AKSlotAdmin(admin.ModelAdmin):
 
 
 @admin.register(Availability)
-class AvailabilityAdmin(admin.ModelAdmin):
-    def get_form(self, request, obj=None, change=False, **kwargs):
-        # Use timezone of associated event
-        if obj is not None and obj.event.timezone:
-            timezone.activate(obj.event.timezone)
-        # No timezone available? Use UTC
-        else:
-            timezone.activate("UTC")
-        return super().get_form(request, obj, change, **kwargs)
+class AvailabilityAdmin(EventTimezoneFormMixin, admin.ModelAdmin):
+    """
+    Admin interface for Availabilities
+    """
+    list_display = ['__str__', 'event']
+    list_filter = ['event']
 
 
 @admin.register(AKOrgaMessage)
 class AKOrgaMessageAdmin(admin.ModelAdmin):
+    """
+    Admin interface for AKOrgaMessages
+    """
     list_display = ['timestamp', 'ak', 'text']
     list_filter = ['ak__event']
     readonly_fields = ['timestamp', 'ak', 'text']
 
 
 class ConstraintViolationAdminForm(forms.ModelForm):
+    """
+    Adapted admin form for constraint violations for usage in :class:`ConstraintViolationAdmin`)
+    """
     def __init__(self, *args, **kwargs):
         super().__init__(*args, **kwargs)
         # Filter possible values for foreign keys & m2m when event is specified
@@ -350,6 +466,10 @@ class ConstraintViolationAdminForm(forms.ModelForm):
 
 @admin.register(ConstraintViolation)
 class ConstraintViolationAdmin(admin.ModelAdmin):
+    """
+    Admin interface for constraint violations
+    Uses an adapted form (see :class:`ConstraintViolationAdminForm`)
+    """
     list_display = ['type', 'level', 'get_details', 'manually_resolved']
     list_filter = ['event']
     readonly_fields = ['timestamp']
@@ -357,6 +477,9 @@ class ConstraintViolationAdmin(admin.ModelAdmin):
     actions = ['mark_resolved', 'set_violation', 'set_warning']
 
     def get_urls(self):
+        """
+        Add additional URLs/views to change status and severity of CVs
+        """
         urls = [
             path('mark-resolved/', CVMarkResolvedView.as_view(), name="cv-mark-resolved"),
             path('set-violation/', CVSetLevelViolationView.as_view(), name="cv-set-violation"),
@@ -367,21 +490,36 @@ class ConstraintViolationAdmin(admin.ModelAdmin):
 
     @action(description=_("Mark Constraint Violations as manually resolved"))
     def mark_resolved(self, request, queryset):
+        """
+        Action: Mark CV as resolved
+        """
         selected = queryset.values_list('pk', flat=True)
-        return HttpResponseRedirect(f"{reverse_lazy('admin:cv-mark-resolved')}?pks={','.join(str(pk) for pk in selected)}")
+        return HttpResponseRedirect(
+            f"{reverse_lazy('admin:cv-mark-resolved')}?pks={','.join(str(pk) for pk in selected)}")
 
     @action(description=_('Set Constraint Violations to level "violation"'))
     def set_violation(self, request, queryset):
+        """
+        Action: Promote CV to level violation
+        """
         selected = queryset.values_list('pk', flat=True)
-        return HttpResponseRedirect(f"{reverse_lazy('admin:cv-set-violation')}?pks={','.join(str(pk) for pk in selected)}")
+        return HttpResponseRedirect(
+            f"{reverse_lazy('admin:cv-set-violation')}?pks={','.join(str(pk) for pk in selected)}")
 
     @action(description=_('Set Constraint Violations to level "warning"'))
     def set_warning(self, request, queryset):
+        """
+        Action: Set CV to level warning
+        """
         selected = queryset.values_list('pk', flat=True)
-        return HttpResponseRedirect(f"{reverse_lazy('admin:cv-set-warning')}?pks={','.join(str(pk) for pk in selected)}")
+        return HttpResponseRedirect(
+            f"{reverse_lazy('admin:cv-set-warning')}?pks={','.join(str(pk) for pk in selected)}")
 
 
 class DefaultSlotAdminForm(forms.ModelForm):
+    """
+    Adapted admin form for DefaultSlot for usage in :class:`DefaultSlotAdmin`
+    """
     class Meta:
         widgets = {
             'primary_categories': forms.CheckboxSelectMultiple
@@ -395,13 +533,11 @@ class DefaultSlotAdminForm(forms.ModelForm):
 
 
 @admin.register(DefaultSlot)
-class DefaultSlotAdmin(admin.ModelAdmin):
+class DefaultSlotAdmin(EventTimezoneFormMixin, admin.ModelAdmin):
+    """
+    Admin interface for default slots
+    Uses an adapted form (see :class:`DefaultSlotAdminForm`)
+    """
     list_display = ['start_simplified', 'end_simplified', 'event']
     list_filter = ['event']
     form = DefaultSlotAdminForm
-
-    def get_form(self, request, obj=None, change=False, **kwargs):
-        # Use timezone of event
-        if obj is not None:
-            timezone.activate(obj.event.timezone)
-        return super().get_form(request, obj, change, **kwargs)
diff --git a/AKModel/apps.py b/AKModel/apps.py
index 5af2af0d..8b90f381 100644
--- a/AKModel/apps.py
+++ b/AKModel/apps.py
@@ -3,8 +3,15 @@ from django.contrib.admin.apps import AdminConfig
 
 
 class AkmodelConfig(AppConfig):
+    """
+    App configuration (default, only specifies name of the app)
+    """
     name = 'AKModel'
 
 
 class AKAdminConfig(AdminConfig):
+    """
+    App configuration for admin
+    Loading a custom version here allows to add additional contex and further adapt the behavior of the admin interface
+    """
     default_site = 'AKModel.site.AKAdminSite'
diff --git a/AKModel/availability/forms.py b/AKModel/availability/forms.py
index c6abc863..994949a8 100644
--- a/AKModel/availability/forms.py
+++ b/AKModel/availability/forms.py
@@ -1,7 +1,7 @@
 # This part of the code was adapted from pretalx (https://github.com/pretalx/pretalx)
 # Copyright 2017-2019, Tobias Kunze
 # Original Copyrights licensed under the Apache License, Version 2.0 http://www.apache.org/licenses/LICENSE-2.0
-# Changes are marked in the code
+# Documentation was mainly added by us, other changes are marked in the code
 import datetime
 import json
 
@@ -17,6 +17,10 @@ from AKModel.models import Event
 
 
 class AvailabilitiesFormMixin(forms.Form):
+    """
+    Mixin for forms to add availabilities functionality to it
+    Will handle the rendering and population of an availabilities field
+    """
     availabilities = forms.CharField(
         label=_('Availability'),
         help_text=_(
@@ -28,6 +32,14 @@ class AvailabilitiesFormMixin(forms.Form):
     )
 
     def _serialize(self, event, instance):
+        """
+        Serialize relevant availabilities into a JSON format to populate the text field in the form
+
+        :param event: event the availabilities belong to (relevant for start and end times)
+        :param instance: the entity availabilities in this form should belong to (e.g., an AK, or a Room)
+        :return: JSON serializiation of the relevant availabilities
+        :rtype: str
+        """
         if instance:
             availabilities = AvailabilitySerializer(
                 instance.availabilities.all(), many=True
@@ -48,20 +60,28 @@ class AvailabilitiesFormMixin(forms.Form):
 
     def __init__(self, *args, **kwargs):
         super().__init__(*args, **kwargs)
+        # Load event information and populate availabilities text field
         self.event = self.initial.get('event')
         if isinstance(self.event, int):
             self.event = Event.objects.get(pk=self.event)
-        initial = kwargs.pop('initial', dict())
+        initial = kwargs.pop('initial', {})
         initial['availabilities'] = self._serialize(self.event, kwargs['instance'])
         if not isinstance(self, forms.BaseModelForm):
             kwargs.pop('instance')
         kwargs['initial'] = initial
 
     def _parse_availabilities_json(self, jsonavailabilities):
+        """
+        Turn raw JSON availabilities into a list of model instances
+
+        :param jsonavailabilities: raw json input
+        :return: a list of availability objects corresponding to the raw input
+        :rtype: List[Availability]
+        """
         try:
             rawdata = json.loads(jsonavailabilities)
-        except ValueError:
-            raise forms.ValidationError("Submitted availabilities are not valid json.")
+        except ValueError as exc:
+            raise forms.ValidationError("Submitted availabilities are not valid json.") from exc
         if not isinstance(rawdata, dict):
             raise forms.ValidationError(
                 "Submitted json does not comply with expected format, should be object."
@@ -74,17 +94,32 @@ class AvailabilitiesFormMixin(forms.Form):
         return availabilities
 
     def _parse_datetime(self, strdate):
+        """
+        Parse input date string
+        This will try to correct timezone information if needed
+
+        :param strdate: string representing a timestamp
+        :return: a timestamp object
+        """
         tz = self.event.timezone  # adapt to our event model
 
         obj = parse_datetime(strdate)
         if not obj:
             raise TypeError
         if obj.tzinfo is None:
+            # Adapt to new python timezone interface
             obj = obj.replace(tzinfo=tz)
 
         return obj
 
     def _validate_availability(self, rawavail):
+        """
+        Validate a raw availability instance input by making sure the relevant fields are present and can be parsed
+        The cleaned up values that are produced to test the validity of the input are stored in-place in the input
+        object for later usage in cleaning/parsing to availability objects
+
+        :param rawavail: object to validate/clean
+        """
         message = _("The submitted availability does not comply with the required format.")
         if not isinstance(rawavail, dict):
             raise forms.ValidationError(message)
@@ -96,12 +131,11 @@ class AvailabilitiesFormMixin(forms.Form):
         try:
             rawavail['start'] = self._parse_datetime(rawavail['start'])
             rawavail['end'] = self._parse_datetime(rawavail['end'])
-        except (TypeError, ValueError):
+        # Adapt: Better error handling
+        except (TypeError, ValueError) as exc:
             raise forms.ValidationError(
                 _("The submitted availability contains an invalid date.")
-            )
-
-        tz = self.event.timezone  # adapt to our event model
+            ) from exc
 
         timeframe_start = self.event.start  # adapt to our event model
         if rawavail['start'] < timeframe_start:
@@ -115,6 +149,10 @@ class AvailabilitiesFormMixin(forms.Form):
             rawavail['end'] = timeframe_end
 
     def clean_availabilities(self):
+        """
+        Turn raw availabilities into real availability objects
+        :return:
+        """
         data = self.cleaned_data.get('availabilities')
         required = (
                 'availabilities' in self.fields and self.fields['availabilities'].required
@@ -135,7 +173,8 @@ class AvailabilitiesFormMixin(forms.Form):
         return availabilities
 
     def _set_foreignkeys(self, instance, availabilities):
-        """Set the reference to `instance` in each given availability.
+        """
+        Set the reference to `instance` in each given availability.
         For example, set the availabilitiy.room_id to instance.id, in
         case instance of type Room.
         """
@@ -145,10 +184,20 @@ class AvailabilitiesFormMixin(forms.Form):
             setattr(avail, reference_name, instance.id)
 
     def _replace_availabilities(self, instance, availabilities: [Availability]):
+        """
+        Replace the existing list of availabilities belonging to an entity with a new, updated one
+
+        This will trigger a post_save signal for usage in constraint violation checking
+
+        :param instance: entity the availabilities belong to
+        :param availabilities: list of new availabilities
+        """
         with transaction.atomic():
-            # TODO: do not recreate objects unnecessarily, give the client the IDs, so we can track modifications and leave unchanged objects alone
+            # TODO: do not recreate objects unnecessarily, give the client the IDs, so we can track modifications and
+            #  leave unchanged objects alone
             instance.availabilities.all().delete()
             Availability.objects.bulk_create(availabilities)
+            # Adaption:
             # Trigger post save signal manually to make sure constraints are updated accordingly
             # Doing this one time is sufficient, since this will nevertheless update all availability constraint
             # violations of the corresponding AK
@@ -156,6 +205,9 @@ class AvailabilitiesFormMixin(forms.Form):
                 post_save.send(Availability, instance=availabilities[0], created=True)
 
     def save(self, *args, **kwargs):
+        """
+        Override the saving method of the (model) form
+        """
         instance = super().save(*args, **kwargs)
         availabilities = self.cleaned_data.get('availabilities')
 
diff --git a/AKModel/availability/models.py b/AKModel/availability/models.py
index 4f90ddc2..7ce794dc 100644
--- a/AKModel/availability/models.py
+++ b/AKModel/availability/models.py
@@ -23,6 +23,9 @@ zero_time = datetime.time(0, 0)
 # add meta class
 # enable availabilities for AKs and AKCategories
 # add verbose names and help texts to model attributes
+# adapt or extemd documentation
+
+
 class Availability(models.Model):
     """The Availability class models when people or rooms are available for.
 
@@ -31,6 +34,8 @@ class Availability(models.Model):
     span multiple days, but due to our choice of input widget, it will
     usually only span a single day at most.
     """
+    # pylint: disable=broad-exception-raised
+
     event = models.ForeignKey(
         to=Event,
         related_name='availabilities',
@@ -96,10 +101,10 @@ class Availability(models.Model):
         are the same.
         """
         return all(
-            [
+            (
                 getattr(self, attribute, None) == getattr(other, attribute, None)
                 for attribute in ['event', 'person', 'room', 'ak', 'ak_category', 'start', 'end']
-            ]
+            )
         )
 
     @cached_property
@@ -233,10 +238,28 @@ class Availability(models.Model):
 
     @property
     def simplified(self):
-        return f'{self.start.astimezone(self.event.timezone).strftime("%a %H:%M")}-{self.end.astimezone(self.event.timezone).strftime("%a %H:%M")}'
+        """
+        Get a simplified (only Weekday, hour and minute) string representation of an availability
+        :return: simplified string version
+        :rtype: str
+        """
+        return (f'{self.start.astimezone(self.event.timezone).strftime("%a %H:%M")}-'
+                f'{self.end.astimezone(self.event.timezone).strftime("%a %H:%M")}')
 
     @classmethod
     def with_event_length(cls, event, person=None, room=None, ak=None, ak_category=None):
+        """
+        Create an availability covering exactly the time between event start and event end.
+        Can e.g., be used to create default availabilities.
+
+        :param event: relevant event
+        :param person: person, if availability should be connected to a person
+        :param room: room, if availability should be connected to a room
+        :param ak: ak, if availability should be connected to a ak
+        :param ak_category: ak_category, if availability should be connected to a ak_category
+        :return: availability associated to the entity oder entities selected
+        :rtype: Availability
+        """
         timeframe_start = event.start  # adapt to our event model
         # add 1 day, not 24 hours, https://stackoverflow.com/a/25427822/2486196
         timeframe_end = event.end  # adapt to our event model
diff --git a/AKModel/availability/serializers.py b/AKModel/availability/serializers.py
index 92b3adc6..ae7569db 100644
--- a/AKModel/availability/serializers.py
+++ b/AKModel/availability/serializers.py
@@ -1,7 +1,7 @@
 # This part of the code was adapted from pretalx (https://github.com/pretalx/pretalx)
 # Copyright 2017-2019, Tobias Kunze
 # Original Copyrights licensed under the Apache License, Version 2.0 http://www.apache.org/licenses/LICENSE-2.0
-# Changes are marked in the code
+# Documentation was mainly added by us, other changes are marked in the code
 from django.utils import timezone
 from rest_framework.fields import SerializerMethodField
 from rest_framework.serializers import ModelSerializer
@@ -10,19 +10,35 @@ from AKModel.availability.models import Availability
 
 
 class AvailabilitySerializer(ModelSerializer):
+    """
+    REST Framework Serializer for Availability
+    """
     allDay = SerializerMethodField()
     start = SerializerMethodField()
     end = SerializerMethodField()
 
-    def get_allDay(self, obj):
+    def get_allDay(self, obj):  # pylint: disable=invalid-name
+        """
+        Bridge between naming conventions of python and fullcalendar by providing the all_day field as allDay, too
+        """
         return obj.all_day
 
-    # Use already localized strings in serialized field
-    # (default would be UTC, but that would require heavy timezone calculation on client side)
     def get_start(self, obj):
+        """
+        Get start timestamp
+
+        Use already localized strings in serialized field
+        (default would be UTC, but that would require heavy timezone calculation on client side)
+        """
         return timezone.localtime(obj.start, obj.event.timezone).strftime("%Y-%m-%dT%H:%M:%S")
 
     def get_end(self, obj):
+        """
+        Get end timestamp
+
+        Use already localized strings in serialized field
+        (default would be UTC, but that would require heavy timezone calculation on client side)
+        """
         return timezone.localtime(obj.end, obj.event.timezone).strftime("%Y-%m-%dT%H:%M:%S")
 
     class Meta:
diff --git a/AKModel/environment.py b/AKModel/environment.py
index a6536bee..b476f0d7 100644
--- a/AKModel/environment.py
+++ b/AKModel/environment.py
@@ -1,11 +1,14 @@
-# environment.py
+"""
+Environment definitions
+Needed for tex compilation
+"""
 import re
 
 from django_tex.environment import environment
 
 # Used to filter all very special UTF-8 chars that are probably not contained in the LaTeX fonts
 # and would hence cause compilation errors
-utf8_replace_pattern = re.compile(u'[^\u0000-\u206F]', re.UNICODE)
+utf8_replace_pattern = re.compile('[^\u0000-\u206F]', re.UNICODE)
 
 
 def latex_escape_utf8(value):
@@ -17,12 +20,14 @@ def latex_escape_utf8(value):
     :return: escaped string
     :rtype: str
     """
-    return utf8_replace_pattern.sub('', value).replace('&', '\&').replace('_', '\_').replace('#', '\#').replace('$',
-                                                                                                                '\$').replace(
-        '%', '\%').replace('{', '\{').replace('}', '\}')
+    return (utf8_replace_pattern.sub('', value).replace('&', r'\&').replace('_', r'\_').replace('#', r'\#').
+            replace('$', r'\$').replace('%', r'\%').replace('{', r'\{').replace('}', r'\}'))
 
 
 def improved_tex_environment(**options):
+    """
+    Encapsulate our improved latex environment for usage
+    """
     env = environment(**options)
     env.filters.update({
         'latex_escape_utf8': latex_escape_utf8,
diff --git a/AKModel/forms.py b/AKModel/forms.py
index 606b5753..f5475668 100644
--- a/AKModel/forms.py
+++ b/AKModel/forms.py
@@ -1,3 +1,7 @@
+"""
+Central and admin forms
+"""
+
 import csv
 import io
 
@@ -11,6 +15,17 @@ from AKModel.models import Event, AKCategory, AKRequirement, Room
 
 
 class NewEventWizardStartForm(forms.ModelForm):
+    """
+    Initial view of new event wizard
+
+    This form is a model form for Event, but only with a subset of the required fields.
+    It is therefore not possible to really create an event using this form, but only to enter
+    information, in particular the timezone, that is needed to correctly handle/parse the user
+    inputs for further required fields like start and end of the event.
+
+    The form will be used for this partial input, the input of the remaining required fields
+    will then be handled by :class:`NewEventWizardSettingsForm` (see below).
+    """
     class Meta:
         model = Event
         fields = ['name', 'slug', 'timezone', 'plan_hidden']
@@ -18,13 +33,20 @@ class NewEventWizardStartForm(forms.ModelForm):
             'plan_hidden': forms.HiddenInput(),
         }
 
+    # Special hidden field for wizard state handling
     is_init = forms.BooleanField(initial=True, widget=forms.HiddenInput)
 
 
 class NewEventWizardSettingsForm(forms.ModelForm):
+    """
+    Form for second view of the event creation wizard.
+
+    Will handle the input of the remaining required as well as some optional fields.
+    See also :class:`NewEventWizardStartForm`.
+    """
     class Meta:
         model = Event
-        exclude = []
+        fields = "__all__"
         widgets = {
             'name': forms.HiddenInput(),
             'slug': forms.HiddenInput(),
@@ -38,6 +60,10 @@ class NewEventWizardSettingsForm(forms.ModelForm):
 
 
 class NewEventWizardPrepareImportForm(forms.Form):
+    """
+    Wizard form for choosing an event to import/copy elements (requirements, categories, etc) from.
+    Is used to restrict the list of elements to choose from in the next step (see :class:`NewEventWizardImportForm`).
+    """
     import_event = forms.ModelChoiceField(
         queryset=Event.objects.all(),
         label=_("Copy ak requirements and ak categories of existing event"),
@@ -46,6 +72,12 @@ class NewEventWizardPrepareImportForm(forms.Form):
 
 
 class NewEventWizardImportForm(forms.Form):
+    """
+    Wizard form for excaclty choosing which elemments to copy/import for the newly created event.
+    Possible elements are categories, requirements, and dashboard buttons if AKDashboard is active.
+    The lists are restricted to elements from the event selected in the previous step
+    (see :class:`NewEventWizardPrepareImportForm`).
+    """
     import_categories = forms.ModelMultipleChoiceField(
         queryset=AKCategory.objects.all(),
         widget=forms.CheckboxSelectMultiple,
@@ -60,6 +92,7 @@ class NewEventWizardImportForm(forms.Form):
         required=False,
     )
 
+    # pylint: disable=too-many-arguments
     def __init__(self, data=None, files=None, auto_id='id_%s', prefix=None, initial=None, error_class=ErrorList,
                  label_suffix=None, empty_permitted=False, field_order=None, use_required_attribute=None,
                  renderer=None):
@@ -70,10 +103,12 @@ class NewEventWizardImportForm(forms.Form):
         self.fields["import_requirements"].queryset = self.fields["import_requirements"].queryset.filter(
             event=self.initial["import_event"])
 
+        # pylint: disable=import-outside-toplevel
+        # Local imports used to prevent cyclic imports and to only import when AKDashboard is available
         from django.apps import apps
         if apps.is_installed("AKDashboard"):
+            # If AKDashboard is active, allow to copy dashboard buttons, too
             from AKDashboard.models import DashboardButton
-
             self.fields["import_buttons"] = forms.ModelMultipleChoiceField(
                 queryset=DashboardButton.objects.filter(event=self.initial["import_event"]),
                 widget=forms.CheckboxSelectMultiple,
@@ -83,20 +118,37 @@ class NewEventWizardImportForm(forms.Form):
 
 
 class NewEventWizardActivateForm(forms.ModelForm):
+    """
+    Wizard form to activate the newly created event
+    """
     class Meta:
         fields = ["active"]
         model = Event
 
 
 class AdminIntermediateForm(forms.Form):
-    pass
+    """
+    Base form for admin intermediate views (forms used there should inherit from this,
+    by default, the form is empty since it is only needed for the confirmation button)
+    """
 
 
 class AdminIntermediateActionForm(AdminIntermediateForm):
+    """
+    Form for Admin Action Confirmation views -- has a pks field needed to handle the serialization/deserialization of
+    the IDs of the entities the user selected for the admin action to be performed on
+    """
     pks = forms.CharField(widget=forms.HiddenInput)
 
 
 class SlideExportForm(AdminIntermediateForm):
+    """
+    Form to control the slides generated from the AK list of an event
+
+    The user can select how many upcoming AKs are displayed at the footer to inform people that it is their turn soon,
+    whether the AK list should be restricted to those AKs that where marked for presentation, and whether ther should
+    be a symbol and empty space to take notes on for wishes
+    """
     num_next = forms.IntegerField(
         min_value=0,
         max_value=6,
@@ -121,6 +173,9 @@ class SlideExportForm(AdminIntermediateForm):
 
 
 class DefaultSlotEditorForm(AdminIntermediateForm):
+    """
+    Form for default slot editor
+    """
     availabilities = forms.CharField(
         label=_('Default Slots'),
         help_text=_(
@@ -133,6 +188,12 @@ class DefaultSlotEditorForm(AdminIntermediateForm):
 
 
 class RoomBatchCreationForm(AdminIntermediateForm):
+    """
+    Form for room batch creation
+
+    Allows to input a list of room details and choose whether default availabilities should be generated for these
+    rooms. Will check that the input follows a CSV format with at least a name column present.
+    """
     rooms = forms.CharField(
         label=_('New rooms'),
         help_text=_('Enter room details in CSV format. Required colum is "name", optional colums are "location", '
@@ -147,6 +208,13 @@ class RoomBatchCreationForm(AdminIntermediateForm):
     )
 
     def clean_rooms(self):
+        """
+        Validate and transform the input for the rooms textfield
+        Treat the input as CSV and turn it into a dict containing the relevant information.
+
+        :return: a dict containing the raw room information
+        :rtype: dict[str, str]
+        """
         rooms_raw_text = self.cleaned_data["rooms"]
         rooms_raw_dict = csv.DictReader(io.StringIO(rooms_raw_text), delimiter=";")
 
@@ -157,6 +225,10 @@ class RoomBatchCreationForm(AdminIntermediateForm):
 
 
 class RoomForm(forms.ModelForm):
+    """
+    Room (creation) form (basic), will be extended for handling of availabilities
+    (see :class:`RoomFormWithAvailabilities`) and also for creating hybrid rooms in AKOnline (if active)
+    """
     class Meta:
         model = Room
         fields = ['name',
@@ -167,6 +239,9 @@ class RoomForm(forms.ModelForm):
 
 
 class RoomFormWithAvailabilities(AvailabilitiesFormMixin, RoomForm):
+    """
+    Room (update) form including handling of availabilities, extends :class:`RoomForm`
+    """
     class Meta:
         model = Room
         fields = ['name',
@@ -182,7 +257,7 @@ class RoomFormWithAvailabilities(AvailabilitiesFormMixin, RoomForm):
 
     def __init__(self, *args, **kwargs):
         # Init availability mixin
-        kwargs['initial'] = dict()
+        kwargs['initial'] = {}
         super().__init__(*args, **kwargs)
         self.initial = {**self.initial, **kwargs['initial']}
         # Filter possible values for m2m when event is specified
diff --git a/AKModel/locale/de_DE/LC_MESSAGES/django.po b/AKModel/locale/de_DE/LC_MESSAGES/django.po
index f1f5756b..9041cdcb 100644
--- a/AKModel/locale/de_DE/LC_MESSAGES/django.po
+++ b/AKModel/locale/de_DE/LC_MESSAGES/django.po
@@ -2,7 +2,7 @@ msgid ""
 msgstr ""
 "Project-Id-Version: PACKAGE VERSION\n"
 "Report-Msgid-Bugs-To: \n"
-"POT-Creation-Date: 2023-05-15 20:19+0200\n"
+"POT-Creation-Date: 2023-08-13 11:20+0200\n"
 "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
 "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
 "Language-Team: LANGUAGE <LL@li.org>\n"
@@ -11,7 +11,7 @@ msgstr ""
 "Content-Type: text/plain; charset=UTF-8\n"
 "Content-Transfer-Encoding: 8bit\n"
 
-#: AKModel/admin.py:65 AKModel/admin.py:68
+#: AKModel/admin.py:86 AKModel/admin.py:96
 #: AKModel/templates/admin/AKModel/event_wizard/activate.html:32
 #: AKModel/templates/admin/AKModel/event_wizard/created_prepare_import.html:48
 #: AKModel/templates/admin/AKModel/event_wizard/finish.html:21
@@ -21,63 +21,63 @@ msgstr ""
 msgid "Status"
 msgstr "Status"
 
-#: AKModel/admin.py:70
+#: AKModel/admin.py:98
 msgid "Toggle plan visibility"
 msgstr "Plansichtbarkeit ändern"
 
-#: AKModel/admin.py:74 AKModel/admin.py:85 AKModel/views/manage.py:105
+#: AKModel/admin.py:110 AKModel/admin.py:121 AKModel/views/manage.py:106
 msgid "Publish plan"
 msgstr "Plan veröffentlichen"
 
-#: AKModel/admin.py:77 AKModel/admin.py:90 AKModel/views/manage.py:115
+#: AKModel/admin.py:113 AKModel/admin.py:129 AKModel/views/manage.py:116
 msgid "Unpublish plan"
 msgstr "Plan verbergen"
 
-#: AKModel/admin.py:153
+#: AKModel/admin.py:208
 msgid "Wish"
 msgstr "AK-Wunsch"
 
-#: AKModel/admin.py:159
+#: AKModel/admin.py:214
 msgid "Is wish"
 msgstr "Ist ein Wunsch"
 
-#: AKModel/admin.py:160
+#: AKModel/admin.py:215
 msgid "Is not a wish"
 msgstr "Ist kein Wunsch"
 
-#: AKModel/admin.py:204
+#: AKModel/admin.py:274
 msgid "Export to wiki syntax"
 msgstr "In Wiki-Syntax exportieren"
 
-#: AKModel/admin.py:213
+#: AKModel/admin.py:291
 msgid "Cannot export AKs from more than one event at the same time."
 msgstr "Kann nicht AKs von mehreren Events zur selben Zeit exportieren."
 
-#: AKModel/admin.py:228 AKModel/views/ak.py:80
+#: AKModel/admin.py:306 AKModel/views/ak.py:96
 msgid "Reset interest in AKs"
 msgstr "Interesse an AKs zurücksetzen"
 
-#: AKModel/admin.py:233 AKModel/views/ak.py:90
+#: AKModel/admin.py:316 AKModel/views/ak.py:111
 msgid "Reset AKs' interest counters"
 msgstr "Interessenszähler der AKs zurücksetzen"
 
-#: AKModel/admin.py:309 AKModel/admin.py:316
+#: AKModel/admin.py:415 AKModel/admin.py:429
 msgid "AK Details"
 msgstr "AK-Details"
 
-#: AKModel/admin.py:368 AKModel/views/manage.py:75
+#: AKModel/admin.py:491 AKModel/views/manage.py:76
 msgid "Mark Constraint Violations as manually resolved"
 msgstr "Markiere Constraintverletzungen als manuell behoben"
 
-#: AKModel/admin.py:373 AKModel/views/manage.py:85
+#: AKModel/admin.py:500 AKModel/views/manage.py:86
 msgid "Set Constraint Violations to level \"violation\""
 msgstr "Constraintverletzungen auf Level \"Violation\" setzen"
 
-#: AKModel/admin.py:378 AKModel/views/manage.py:95
+#: AKModel/admin.py:509 AKModel/views/manage.py:96
 msgid "Set Constraint Violations to level \"warning\""
 msgstr "Constraintverletzungen auf Level \"Warning\" setzen"
 
-#: AKModel/availability/forms.py:21 AKModel/availability/models.py:248
+#: AKModel/availability/forms.py:21 AKModel/availability/models.py:251
 msgid "Availability"
 msgstr "Verfügbarkeit"
 
@@ -98,113 +98,113 @@ msgstr "Die eingetragenen Verfügbarkeit haben nicht das notwendige Format."
 msgid "The submitted availability contains an invalid date."
 msgstr "Die eingegebene Verfügbarkeit enthält ein ungültiges Datum."
 
-#: AKModel/availability/forms.py:124 AKModel/availability/forms.py:134
+#: AKModel/availability/forms.py:122 AKModel/availability/forms.py:132
 msgid "Please fill in your availabilities!"
 msgstr "Bitte Verfügbarkeiten eintragen!"
 
-#: AKModel/availability/models.py:38 AKModel/models.py:57 AKModel/models.py:129
-#: AKModel/models.py:184 AKModel/models.py:203 AKModel/models.py:224
-#: AKModel/models.py:277 AKModel/models.py:354 AKModel/models.py:387
-#: AKModel/models.py:458 AKModel/models.py:499 AKModel/models.py:664
+#: AKModel/availability/models.py:40 AKModel/models.py:58 AKModel/models.py:135
+#: AKModel/models.py:190 AKModel/models.py:209 AKModel/models.py:230
+#: AKModel/models.py:284 AKModel/models.py:366 AKModel/models.py:399
+#: AKModel/models.py:471 AKModel/models.py:512 AKModel/models.py:677
 msgid "Event"
 msgstr "Event"
 
-#: AKModel/availability/models.py:39 AKModel/models.py:130
-#: AKModel/models.py:185 AKModel/models.py:204 AKModel/models.py:225
-#: AKModel/models.py:278 AKModel/models.py:355 AKModel/models.py:388
-#: AKModel/models.py:459 AKModel/models.py:500 AKModel/models.py:665
+#: AKModel/availability/models.py:41 AKModel/models.py:136
+#: AKModel/models.py:191 AKModel/models.py:210 AKModel/models.py:231
+#: AKModel/models.py:285 AKModel/models.py:367 AKModel/models.py:400
+#: AKModel/models.py:472 AKModel/models.py:513 AKModel/models.py:678
 msgid "Associated event"
 msgstr "Zugehöriges Event"
 
-#: AKModel/availability/models.py:47
+#: AKModel/availability/models.py:49
 msgid "Person"
 msgstr "Person"
 
-#: AKModel/availability/models.py:48
+#: AKModel/availability/models.py:50
 msgid "Person whose availability this is"
 msgstr "Person deren Verfügbarkeit hier abgebildet wird"
 
-#: AKModel/availability/models.py:56 AKModel/models.py:358
-#: AKModel/models.py:377 AKModel/models.py:508
+#: AKModel/availability/models.py:58 AKModel/models.py:370
+#: AKModel/models.py:389 AKModel/models.py:521
 msgid "Room"
 msgstr "Raum"
 
-#: AKModel/availability/models.py:57
+#: AKModel/availability/models.py:59
 msgid "Room whose availability this is"
 msgstr "Raum dessen Verfügbarkeit hier abgebildet wird"
 
-#: AKModel/availability/models.py:65 AKModel/models.py:286
-#: AKModel/models.py:376 AKModel/models.py:453
+#: AKModel/availability/models.py:67 AKModel/models.py:293
+#: AKModel/models.py:388 AKModel/models.py:466
 msgid "AK"
 msgstr "AK"
 
-#: AKModel/availability/models.py:66
+#: AKModel/availability/models.py:68
 msgid "AK whose availability this is"
 msgstr "Verfügbarkeiten"
 
-#: AKModel/availability/models.py:74 AKModel/models.py:188
-#: AKModel/models.py:514
+#: AKModel/availability/models.py:76 AKModel/models.py:194
+#: AKModel/models.py:527
 msgid "AK Category"
 msgstr "AK-Kategorie"
 
-#: AKModel/availability/models.py:75
+#: AKModel/availability/models.py:77
 msgid "AK Category whose availability this is"
 msgstr "AK-Kategorie, deren Verfügbarkeit hier abgebildet wird"
 
-#: AKModel/availability/models.py:249
+#: AKModel/availability/models.py:252
 msgid "Availabilities"
 msgstr "Verfügbarkeiten"
 
-#: AKModel/forms.py:43
+#: AKModel/forms.py:65
 msgid "Copy ak requirements and ak categories of existing event"
 msgstr "AK-Anforderungen und AK-Kategorien eines existierenden Events kopieren"
 
-#: AKModel/forms.py:44
+#: AKModel/forms.py:66
 msgid "You can choose what to copy in the next step"
 msgstr ""
 "Im nächsten Schritt kann ausgewählt werden, was genau kopiert werden soll"
 
-#: AKModel/forms.py:52
+#: AKModel/forms.py:74
 msgid "Copy ak categories"
 msgstr "AK-Kategorien kopieren"
 
-#: AKModel/forms.py:59
+#: AKModel/forms.py:81
 msgid "Copy ak requirements"
 msgstr "AK-Anforderungen kopieren"
 
-#: AKModel/forms.py:80
+#: AKModel/forms.py:105
 msgid "Copy dashboard buttons"
 msgstr "Dashboard-Buttons kopieren"
 
-#: AKModel/forms.py:104
+#: AKModel/forms.py:129
 msgid "# next AKs"
 msgstr "# nächste AKs"
 
-#: AKModel/forms.py:105
+#: AKModel/forms.py:130
 msgid "How many next AKs should be shown on a slide?"
 msgstr "Wie viele nächste AKs sollen auf einer Folie angezeigt werden?"
 
-#: AKModel/forms.py:108
+#: AKModel/forms.py:133
 msgid "Presentation only?"
 msgstr "Nur Vorstellung?"
 
-#: AKModel/forms.py:110 AKModel/forms.py:117
+#: AKModel/forms.py:135 AKModel/forms.py:142
 msgid "Yes"
 msgstr "Ja"
 
-#: AKModel/forms.py:110 AKModel/forms.py:117
+#: AKModel/forms.py:135 AKModel/forms.py:142
 msgid "No"
 msgstr "Nein"
 
-#: AKModel/forms.py:112
+#: AKModel/forms.py:137
 msgid "Restrict AKs to those that asked for chance to be presented?"
 msgstr "AKs auf solche, die um eine Vorstellung gebeten haben, einschränken?"
 
-#: AKModel/forms.py:115
+#: AKModel/forms.py:140
 msgid "Space for notes in wishes?"
 msgstr "Platz für Notizen bei den Wünschen?"
 
-#: AKModel/forms.py:119
+#: AKModel/forms.py:144
 msgid ""
 "Create symbols indicating space to note down owners and timeslots for "
 "wishes, e.g., to be filled out on a touch screen while presenting?"
@@ -213,11 +213,11 @@ msgstr ""
 "fürWünsche markieren, z.B. um während der Präsentation auf einem Touchscreen "
 "ausgefüllt zu werden?"
 
-#: AKModel/forms.py:125 AKModel/models.py:658
+#: AKModel/forms.py:150 AKModel/models.py:671
 msgid "Default Slots"
 msgstr "Standardslots"
 
-#: AKModel/forms.py:127
+#: AKModel/forms.py:152
 msgid ""
 "Click and drag to add default slots, double-click to delete. Or use the "
 "start and end inputs to add entries to the calendar view."
@@ -226,11 +226,11 @@ msgstr ""
 "Einträge zu löschen. Oder Start- und End-Eingabe verwenden, um der "
 "Kalenderansicht neue Einträge hinzuzufügen."
 
-#: AKModel/forms.py:137
+#: AKModel/forms.py:162
 msgid "New rooms"
 msgstr "Neue Räume"
 
-#: AKModel/forms.py:138
+#: AKModel/forms.py:163
 msgid ""
 "Enter room details in CSV format. Required colum is \"name\", optional "
 "colums are \"location\", \"capacity\", and \"url\" for online/hybrid rooms. "
@@ -240,167 +240,167 @@ msgstr ""
 "Spalten sind \"location\", \"capacity\", und \"url\" for Online-/"
 "HybridräumeTrennzeichen: Semikolon"
 
-#: AKModel/forms.py:144
+#: AKModel/forms.py:169
 msgid "Default availabilities?"
 msgstr "Standardverfügbarkeiten?"
 
-#: AKModel/forms.py:145
+#: AKModel/forms.py:170
 msgid "Create default availabilities for all rooms?"
 msgstr "Standardverfügbarkeiten für alle Räume anlegen?"
 
-#: AKModel/forms.py:154
+#: AKModel/forms.py:179
 msgid "CSV must contain a name column"
 msgstr "CSV muss eine name-Spalte enthalten"
 
-#: AKModel/metaviews/admin.py:97 AKModel/models.py:28
+#: AKModel/metaviews/admin.py:146 AKModel/models.py:29
 msgid "Start"
 msgstr "Start"
 
-#: AKModel/metaviews/admin.py:98
+#: AKModel/metaviews/admin.py:147
 msgid "Settings"
 msgstr "Einstellungen"
 
-#: AKModel/metaviews/admin.py:99
+#: AKModel/metaviews/admin.py:148
 msgid "Event created, Prepare Import"
 msgstr "Event angelegt, Import vorbereiten"
 
-#: AKModel/metaviews/admin.py:100
+#: AKModel/metaviews/admin.py:149
 msgid "Import categories & requirements"
 msgstr "Kategorien & Anforderungen kopieren"
 
-#: AKModel/metaviews/admin.py:101
+#: AKModel/metaviews/admin.py:150
 msgid "Activate?"
 msgstr "Aktivieren?"
 
-#: AKModel/metaviews/admin.py:102
+#: AKModel/metaviews/admin.py:151
 #: AKModel/templates/admin/AKModel/event_wizard/activate.html:27
 msgid "Finish"
 msgstr "Abschluss"
 
-#: AKModel/models.py:19 AKModel/models.py:176 AKModel/models.py:200
-#: AKModel/models.py:222 AKModel/models.py:240 AKModel/models.py:346
+#: AKModel/models.py:20 AKModel/models.py:182 AKModel/models.py:206
+#: AKModel/models.py:228 AKModel/models.py:246 AKModel/models.py:358
 msgid "Name"
 msgstr "Name"
 
-#: AKModel/models.py:20
+#: AKModel/models.py:21
 msgid "Name or iteration of the event"
 msgstr "Name oder Iteration des Events"
 
-#: AKModel/models.py:21
+#: AKModel/models.py:22
 msgid "Short Form"
 msgstr "Kurzer Name"
 
-#: AKModel/models.py:22
+#: AKModel/models.py:23
 msgid "Short name of letters/numbers/dots/dashes/underscores used in URLs."
 msgstr ""
 "Kurzname bestehend aus Buchstaben, Nummern, Punkten und Unterstrichen zur "
 "Nutzung in URLs"
 
-#: AKModel/models.py:24
+#: AKModel/models.py:25
 msgid "Place"
 msgstr "Ort"
 
-#: AKModel/models.py:25
+#: AKModel/models.py:26
 msgid "City etc. the event takes place in"
 msgstr "Stadt o.ä. in der das Event stattfindet"
 
-#: AKModel/models.py:27
+#: AKModel/models.py:28
 msgid "Time Zone"
 msgstr "Zeitzone"
 
-#: AKModel/models.py:27
+#: AKModel/models.py:28
 msgid "Time Zone where this event takes place in"
 msgstr "Zeitzone in der das Event stattfindet"
 
-#: AKModel/models.py:28
+#: AKModel/models.py:29
 msgid "Time the event begins"
 msgstr "Zeit zu der das Event beginnt"
 
-#: AKModel/models.py:29
+#: AKModel/models.py:30
 msgid "End"
 msgstr "Ende"
 
-#: AKModel/models.py:29
+#: AKModel/models.py:30
 msgid "Time the event ends"
 msgstr "Zeit zu der das Event endet"
 
-#: AKModel/models.py:30
+#: AKModel/models.py:31
 msgid "Resolution Deadline"
 msgstr "Resolutionsdeadline"
 
-#: AKModel/models.py:31
+#: AKModel/models.py:32
 msgid "When should AKs with intention to submit a resolution be done?"
 msgstr "Wann sollen AKs mit Resolutionsabsicht stattgefunden haben?"
 
-#: AKModel/models.py:33
+#: AKModel/models.py:34
 msgid "Interest Window Start"
 msgstr "Beginn Interessensbekundung"
 
-#: AKModel/models.py:34
+#: AKModel/models.py:35
 msgid "Opening time for expression of interest."
 msgstr "Öffnungszeitpunkt für die Angabe von Interesse an AKs."
 
-#: AKModel/models.py:35
+#: AKModel/models.py:36
 msgid "Interest Window End"
 msgstr "Ende Interessensbekundung"
 
-#: AKModel/models.py:36
+#: AKModel/models.py:37
 msgid "Closing time for expression of interest."
 msgstr "Öffnungszeitpunkt für die Angabe von Interesse an AKs."
 
-#: AKModel/models.py:38
+#: AKModel/models.py:39
 msgid "Public event"
 msgstr "Öffentliches Event"
 
-#: AKModel/models.py:39
+#: AKModel/models.py:40
 msgid "Show this event on overview page."
 msgstr "Zeige dieses Event auf der Ãœbersichtseite an"
 
-#: AKModel/models.py:41
+#: AKModel/models.py:42
 msgid "Active State"
 msgstr "Aktiver Status"
 
-#: AKModel/models.py:41
+#: AKModel/models.py:42
 msgid "Marks currently active events"
 msgstr "Markiert aktuell aktive Events"
 
-#: AKModel/models.py:42
+#: AKModel/models.py:43
 msgid "Plan Hidden"
 msgstr "Plan verborgen"
 
-#: AKModel/models.py:42
+#: AKModel/models.py:43
 msgid "Hides plan for non-staff users"
 msgstr "Verbirgt den Plan für Nutzer*innen ohne erweiterte Rechte"
 
-#: AKModel/models.py:44
+#: AKModel/models.py:45
 msgid "Plan published at"
 msgstr "Plan veröffentlicht am/um"
 
-#: AKModel/models.py:45
+#: AKModel/models.py:46
 msgid "Timestamp at which the plan was published"
 msgstr "Zeitpunkt, zu dem der Plan veröffentlicht wurde"
 
-#: AKModel/models.py:47
+#: AKModel/models.py:48
 msgid "Base URL"
 msgstr "URL-Prefix"
 
-#: AKModel/models.py:47
+#: AKModel/models.py:48
 msgid "Prefix for wiki link construction"
 msgstr "Prefix für die automatische Generierung von Wiki-Links"
 
-#: AKModel/models.py:48
+#: AKModel/models.py:49
 msgid "Wiki Export Template Name"
 msgstr "Wiki-Export Templatename"
 
-#: AKModel/models.py:49
+#: AKModel/models.py:50
 msgid "Default Slot Length"
 msgstr "Standardslotlänge"
 
-#: AKModel/models.py:50
+#: AKModel/models.py:51
 msgid "Default length in hours that is assumed for AKs in this event."
 msgstr "Standardlänge von Slots (in Stunden) für dieses Event"
 
-#: AKModel/models.py:52
+#: AKModel/models.py:53
 msgid "Contact email address"
 msgstr "E-Mail Kontaktadresse"
 
@@ -412,75 +412,75 @@ msgstr ""
 "Eine Mailadresse die auf jeder Seite angezeigt wird und für alle Arten von "
 "Fragen genutzt werden kann"
 
-#: AKModel/models.py:58
+#: AKModel/models.py:59
 msgid "Events"
 msgstr "Events"
 
-#: AKModel/models.py:124
+#: AKModel/models.py:130
 msgid "Nickname"
 msgstr "Spitzname"
 
-#: AKModel/models.py:124
+#: AKModel/models.py:130
 msgid "Name to identify an AK owner by"
 msgstr "Name, durch den eine AK-Leitung identifiziert wird"
 
-#: AKModel/models.py:125
+#: AKModel/models.py:131
 msgid "Slug"
 msgstr "Slug"
 
-#: AKModel/models.py:125
+#: AKModel/models.py:131
 msgid "Slug for URL generation"
 msgstr "Slug für URL-Generierung"
 
-#: AKModel/models.py:126
+#: AKModel/models.py:132
 msgid "Institution"
 msgstr "Instutution"
 
-#: AKModel/models.py:126
+#: AKModel/models.py:132
 msgid "Uni etc."
 msgstr "Universität o.ä."
 
-#: AKModel/models.py:127 AKModel/models.py:249
+#: AKModel/models.py:133 AKModel/models.py:255
 msgid "Web Link"
 msgstr "Internet Link"
 
-#: AKModel/models.py:127
+#: AKModel/models.py:133
 msgid "Link to Homepage"
 msgstr "Link zu Homepage oder Webseite"
 
-#: AKModel/models.py:133 AKModel/models.py:507
+#: AKModel/models.py:139 AKModel/models.py:520
 msgid "AK Owner"
 msgstr "AK-Leitung"
 
-#: AKModel/models.py:134
+#: AKModel/models.py:140
 msgid "AK Owners"
 msgstr "AK-Leitungen"
 
-#: AKModel/models.py:176
+#: AKModel/models.py:182
 msgid "Name of the AK Category"
 msgstr "Name der AK-Kategorie"
 
-#: AKModel/models.py:177 AKModel/models.py:201
+#: AKModel/models.py:183 AKModel/models.py:207
 msgid "Color"
 msgstr "Farbe"
 
-#: AKModel/models.py:177 AKModel/models.py:201
+#: AKModel/models.py:183 AKModel/models.py:207
 msgid "Color for displaying"
 msgstr "Farbe für die Anzeige"
 
-#: AKModel/models.py:178 AKModel/models.py:243
+#: AKModel/models.py:184 AKModel/models.py:249
 msgid "Description"
 msgstr "Beschreibung"
 
-#: AKModel/models.py:179
+#: AKModel/models.py:185
 msgid "Short description of this AK Category"
 msgstr "Beschreibung der AK-Kategorie"
 
-#: AKModel/models.py:180
+#: AKModel/models.py:186
 msgid "Present by default"
 msgstr "Defaultmäßig präsentieren"
 
-#: AKModel/models.py:182
+#: AKModel/models.py:187
 msgid ""
 "Present AKs of this category by default if AK owner did not specify whether "
 "this AK should be presented?"
@@ -488,132 +488,132 @@ msgstr ""
 "AKs dieser Kategorie standardmäßig vorstellen, wenn die Leitungen das für "
 "ihren AK nicht explizit spezifiziert haben?"
 
-#: AKModel/models.py:189
+#: AKModel/models.py:195
 msgid "AK Categories"
 msgstr "AK-Kategorien"
 
-#: AKModel/models.py:200
+#: AKModel/models.py:206
 msgid "Name of the AK Track"
 msgstr "Name des AK-Tracks"
 
-#: AKModel/models.py:207
+#: AKModel/models.py:213
 msgid "AK Track"
 msgstr "AK-Track"
 
-#: AKModel/models.py:208
+#: AKModel/models.py:214
 msgid "AK Tracks"
 msgstr "AK-Tracks"
 
-#: AKModel/models.py:222
+#: AKModel/models.py:228
 msgid "Name of the Requirement"
 msgstr "Name der Anforderung"
 
-#: AKModel/models.py:228 AKModel/models.py:511
+#: AKModel/models.py:234 AKModel/models.py:524
 msgid "AK Requirement"
 msgstr "AK-Anforderung"
 
-#: AKModel/models.py:229
+#: AKModel/models.py:235
 msgid "AK Requirements"
 msgstr "AK-Anforderungen"
 
-#: AKModel/models.py:240
+#: AKModel/models.py:246
 msgid "Name of the AK"
 msgstr "Name des AKs"
 
-#: AKModel/models.py:241
+#: AKModel/models.py:247
 msgid "Short Name"
 msgstr "Kurzer Name"
 
-#: AKModel/models.py:242
+#: AKModel/models.py:248
 msgid "Name displayed in the schedule"
 msgstr "Name zur Anzeige im AK-Plan"
 
-#: AKModel/models.py:243
+#: AKModel/models.py:249
 msgid "Description of the AK"
 msgstr "Beschreibung des AKs"
 
-#: AKModel/models.py:245
+#: AKModel/models.py:251
 msgid "Owners"
 msgstr "Leitungen"
 
-#: AKModel/models.py:246
+#: AKModel/models.py:252
 msgid "Those organizing the AK"
 msgstr "Menschen, die den AK organisieren und halten"
 
-#: AKModel/models.py:249
+#: AKModel/models.py:255
 msgid "Link to wiki page"
 msgstr "Link zur Wiki Seite"
 
-#: AKModel/models.py:250
+#: AKModel/models.py:256
 msgid "Protocol Link"
 msgstr "Protokolllink"
 
-#: AKModel/models.py:250
+#: AKModel/models.py:256
 msgid "Link to protocol"
 msgstr "Link zum Protokoll"
 
-#: AKModel/models.py:252
+#: AKModel/models.py:258
 msgid "Category"
 msgstr "Kategorie"
 
-#: AKModel/models.py:253
+#: AKModel/models.py:259
 msgid "Category of the AK"
 msgstr "Kategorie des AKs"
 
-#: AKModel/models.py:254
+#: AKModel/models.py:260
 msgid "Track"
 msgstr "Track"
 
-#: AKModel/models.py:255
+#: AKModel/models.py:261
 msgid "Track the AK belongs to"
 msgstr "Track zu dem der AK gehört"
 
-#: AKModel/models.py:257
+#: AKModel/models.py:263
 msgid "Resolution Intention"
 msgstr "Resolutionsabsicht"
 
-#: AKModel/models.py:258
+#: AKModel/models.py:264
 msgid "Intends to submit a resolution"
 msgstr "Beabsichtigt eine Resolution einzureichen"
 
-#: AKModel/models.py:259
+#: AKModel/models.py:265
 msgid "Present this AK"
 msgstr "AK präsentieren"
 
-#: AKModel/models.py:260
+#: AKModel/models.py:266
 msgid "Present results of this AK"
 msgstr "Die Ergebnisse dieses AKs vorstellen"
 
-#: AKModel/models.py:262 AKModel/views/status.py:136
+#: AKModel/models.py:268 AKModel/views/status.py:140
 msgid "Requirements"
 msgstr "Anforderungen"
 
-#: AKModel/models.py:263
+#: AKModel/models.py:269
 msgid "AK's Requirements"
 msgstr "Anforderungen des AKs"
 
-#: AKModel/models.py:265
+#: AKModel/models.py:271
 msgid "Conflicting AKs"
 msgstr "AK-Konflikte"
 
-#: AKModel/models.py:266
+#: AKModel/models.py:272
 msgid "AKs that conflict and thus must not take place at the same time"
 msgstr ""
 "AKs, die Konflikte haben und deshalb nicht gleichzeitig stattfinden dürfen"
 
-#: AKModel/models.py:267
+#: AKModel/models.py:273
 msgid "Prerequisite AKs"
 msgstr "Vorausgesetzte AKs"
 
-#: AKModel/models.py:268
+#: AKModel/models.py:274
 msgid "AKs that should precede this AK in the schedule"
 msgstr "AKs die im AK-Plan vor diesem AK stattfinden müssen"
 
-#: AKModel/models.py:270
+#: AKModel/models.py:276
 msgid "Organizational Notes"
 msgstr "Notizen zur Organisation"
 
-#: AKModel/models.py:271
+#: AKModel/models.py:277
 msgid ""
 "Notes to organizers. These are public. For private notes, please use the "
 "button for private messages on the detail page of this AK (after creation/"
@@ -623,291 +623,291 @@ msgstr ""
 "Anmerkungen bitte den Button für Direktnachrichten verwenden (nach dem "
 "Anlegen/Bearbeiten)."
 
-#: AKModel/models.py:273
+#: AKModel/models.py:280
 msgid "Interest"
 msgstr "Interesse"
 
-#: AKModel/models.py:273
+#: AKModel/models.py:280
 msgid "Expected number of people"
 msgstr "Erwartete Personenzahl"
 
-#: AKModel/models.py:274
+#: AKModel/models.py:281
 msgid "Interest Counter"
 msgstr "Interessenszähler"
 
-#: AKModel/models.py:275
+#: AKModel/models.py:282
 msgid "People who have indicated interest online"
 msgstr "Anzahl Personen, die online Interesse bekundet haben"
 
-#: AKModel/models.py:280
+#: AKModel/models.py:287
 msgid "Export?"
 msgstr "Export?"
 
-#: AKModel/models.py:281
+#: AKModel/models.py:288
 msgid "Include AK in wiki export?"
 msgstr "AK bei Wiki-Export berücksichtigen?"
 
-#: AKModel/models.py:287 AKModel/models.py:502
+#: AKModel/models.py:294 AKModel/models.py:515
 #: AKModel/templates/admin/AKModel/status/event_aks.html:10
-#: AKModel/views/manage.py:55 AKModel/views/status.py:74
+#: AKModel/views/manage.py:56 AKModel/views/status.py:76
 msgid "AKs"
 msgstr "AKs"
 
-#: AKModel/models.py:346
+#: AKModel/models.py:358
 msgid "Name or number of the room"
 msgstr "Name oder Nummer des Raums"
 
-#: AKModel/models.py:347
+#: AKModel/models.py:359
 msgid "Location"
 msgstr "Ort"
 
-#: AKModel/models.py:348
+#: AKModel/models.py:360
 msgid "Name or number of the location"
 msgstr "Name oder Nummer des Ortes"
 
-#: AKModel/models.py:349
+#: AKModel/models.py:361
 msgid "Capacity"
 msgstr "Kapazität"
 
-#: AKModel/models.py:350
+#: AKModel/models.py:362
 msgid "Maximum number of people (-1 for unlimited)."
 msgstr "Maximale Personenzahl (-1 wenn unbeschränkt)."
 
-#: AKModel/models.py:351
+#: AKModel/models.py:363
 msgid "Properties"
 msgstr "Eigenschaften"
 
-#: AKModel/models.py:352
+#: AKModel/models.py:364
 msgid "AK requirements fulfilled by the room"
 msgstr "AK-Anforderungen, die dieser Raum erfüllt"
 
-#: AKModel/models.py:359 AKModel/views/status.py:44
+#: AKModel/models.py:371 AKModel/views/status.py:45
 msgid "Rooms"
 msgstr "Räume"
 
-#: AKModel/models.py:376
+#: AKModel/models.py:388
 msgid "AK being mapped"
 msgstr "AK, der zugeordnet wird"
 
-#: AKModel/models.py:378
+#: AKModel/models.py:390
 msgid "Room the AK will take place in"
 msgstr "Raum in dem der AK stattfindet"
 
-#: AKModel/models.py:379 AKModel/models.py:661
+#: AKModel/models.py:391 AKModel/models.py:674
 msgid "Slot Begin"
 msgstr "Beginn des Slots"
 
-#: AKModel/models.py:379 AKModel/models.py:661
+#: AKModel/models.py:391 AKModel/models.py:674
 msgid "Time and date the slot begins"
 msgstr "Zeit und Datum zu der der AK beginnt"
 
-#: AKModel/models.py:381
+#: AKModel/models.py:393
 msgid "Duration"
 msgstr "Dauer"
 
-#: AKModel/models.py:382
+#: AKModel/models.py:394
 msgid "Length in hours"
 msgstr "Länge in Stunden"
 
-#: AKModel/models.py:384
+#: AKModel/models.py:396
 msgid "Scheduling fixed"
 msgstr "Planung fix"
 
-#: AKModel/models.py:385
+#: AKModel/models.py:397
 msgid "Length and time of this AK should not be changed"
 msgstr "Dauer und Zeit dieses AKs sollten nicht verändert werden"
 
-#: AKModel/models.py:390
+#: AKModel/models.py:402
 msgid "Last update"
 msgstr "Letzte Aktualisierung"
 
-#: AKModel/models.py:393
+#: AKModel/models.py:405
 msgid "AK Slot"
 msgstr "AK-Slot"
 
-#: AKModel/models.py:394 AKModel/models.py:504
+#: AKModel/models.py:406 AKModel/models.py:517
 msgid "AK Slots"
 msgstr "AK-Slot"
 
-#: AKModel/models.py:416 AKModel/models.py:425
+#: AKModel/models.py:428 AKModel/models.py:437
 msgid "Not scheduled yet"
 msgstr "Noch nicht geplant"
 
-#: AKModel/models.py:454
+#: AKModel/models.py:467
 msgid "AK this message belongs to"
 msgstr "AK zu dem die Nachricht gehört"
 
-#: AKModel/models.py:455
+#: AKModel/models.py:468
 msgid "Message text"
 msgstr "Nachrichtentext"
 
-#: AKModel/models.py:456
+#: AKModel/models.py:469
 msgid "Message to the organizers. This is not publicly visible."
 msgstr ""
 "Nachricht an die Organisator*innen. Diese ist nicht öffentlich sichtbar."
 
-#: AKModel/models.py:462
+#: AKModel/models.py:475
 msgid "AK Orga Message"
 msgstr "AK-Organachricht"
 
-#: AKModel/models.py:463
+#: AKModel/models.py:476
 msgid "AK Orga Messages"
 msgstr "AK-Organachrichten"
 
-#: AKModel/models.py:472
+#: AKModel/models.py:485
 msgid "Constraint Violation"
 msgstr "Constraintverletzung"
 
-#: AKModel/models.py:473 AKModel/views/status.py:93
+#: AKModel/models.py:486 AKModel/views/status.py:95
 msgid "Constraint Violations"
 msgstr "Constraintverletzungen"
 
-#: AKModel/models.py:477
+#: AKModel/models.py:490
 msgid "Owner has two parallel slots"
 msgstr "Leitung hat zwei Slots parallel"
 
-#: AKModel/models.py:478
+#: AKModel/models.py:491
 msgid "AK Slot was scheduled outside the AK's availabilities"
 msgstr "AK Slot wurde außerhalb der Verfügbarkeit des AKs platziert"
 
-#: AKModel/models.py:479
+#: AKModel/models.py:492
 msgid "Room has two AK slots scheduled at the same time"
 msgstr "Raum hat zwei AK Slots gleichzeitig"
 
-#: AKModel/models.py:480
+#: AKModel/models.py:493
 msgid "Room does not satisfy the requirement of the scheduled AK"
 msgstr "Room erfüllt die Anforderungen des platzierten AKs nicht"
 
-#: AKModel/models.py:481
+#: AKModel/models.py:494
 msgid "AK Slot is scheduled at the same time as an AK listed as a conflict"
 msgstr ""
 "AK Slot wurde wurde zur gleichen Zeit wie ein Konflikt des AKs platziert"
 
-#: AKModel/models.py:482
+#: AKModel/models.py:495
 msgid "AK Slot is scheduled before an AK listed as a prerequisite"
 msgstr "AK Slot wurde vor einem als Voraussetzung gelisteten AK platziert"
 
-#: AKModel/models.py:484
+#: AKModel/models.py:497
 msgid ""
 "AK Slot for AK with intention to submit a resolution is scheduled after "
 "resolution deadline"
 msgstr ""
 "AK Slot eines AKs mit Resoabsicht wurde nach der Resodeadline platziert"
 
-#: AKModel/models.py:485
+#: AKModel/models.py:498
 msgid "AK Slot in a category is outside that categories availabilities"
 msgstr "AK Slot wurde außerhalb der Verfügbarkeiten seiner Kategorie"
 
-#: AKModel/models.py:486
+#: AKModel/models.py:499
 msgid "Two AK Slots for the same AK scheduled at the same time"
 msgstr "Zwei AK Slots eines AKs wurden zur selben Zeit platziert"
 
-#: AKModel/models.py:487
+#: AKModel/models.py:500
 msgid "Room does not have enough space for interest in scheduled AK Slot"
 msgstr "Room hat nicht genug Platz für das Interesse am geplanten AK-Slot"
 
-#: AKModel/models.py:488
+#: AKModel/models.py:501
 msgid "AK Slot is scheduled outside the event's availabilities"
 msgstr "AK Slot wurde außerhalb der Verfügbarkeit des Events platziert"
 
-#: AKModel/models.py:491
+#: AKModel/models.py:504
 msgid "Warning"
 msgstr "Warnung"
 
-#: AKModel/models.py:492
+#: AKModel/models.py:505
 msgid "Violation"
 msgstr "Verletzung"
 
-#: AKModel/models.py:494
+#: AKModel/models.py:507
 msgid "Type"
 msgstr "Art"
 
-#: AKModel/models.py:495
+#: AKModel/models.py:508
 msgid "Type of violation, i.e. what kind of constraint was violated"
 msgstr "Art der Verletzung, gibt an welche Art Constraint verletzt wurde"
 
-#: AKModel/models.py:496
+#: AKModel/models.py:509
 msgid "Level"
 msgstr "Level"
 
-#: AKModel/models.py:497
+#: AKModel/models.py:510
 msgid "Severity level of the violation"
 msgstr "Schweregrad der Verletzung"
 
-#: AKModel/models.py:503
+#: AKModel/models.py:516
 msgid "AK(s) belonging to this constraint"
 msgstr "AK(s), die zu diesem Constraint gehören"
 
-#: AKModel/models.py:505
+#: AKModel/models.py:518
 msgid "AK Slot(s) belonging to this constraint"
 msgstr "AK Slot(s), die zu diesem Constraint gehören"
 
-#: AKModel/models.py:507
+#: AKModel/models.py:520
 msgid "AK Owner belonging to this constraint"
 msgstr "AK Leitung(en), die zu diesem Constraint gehören"
 
-#: AKModel/models.py:509
+#: AKModel/models.py:522
 msgid "Room belonging to this constraint"
 msgstr "Raum, der zu diesem Constraint gehört"
 
-#: AKModel/models.py:512
+#: AKModel/models.py:525
 msgid "AK Requirement belonging to this constraint"
 msgstr "AK Anforderung, die zu diesem Constraint gehört"
 
-#: AKModel/models.py:514
+#: AKModel/models.py:527
 msgid "AK Category belonging to this constraint"
 msgstr "AK Kategorie, di zu diesem Constraint gehört"
 
-#: AKModel/models.py:516
+#: AKModel/models.py:529
 msgid "Comment"
 msgstr "Kommentar"
 
-#: AKModel/models.py:516
+#: AKModel/models.py:529
 msgid "Comment or further details for this violation"
 msgstr "Kommentar oder weitere Details zu dieser Vereletzung"
 
-#: AKModel/models.py:519
+#: AKModel/models.py:532
 msgid "Timestamp"
 msgstr "Timestamp"
 
-#: AKModel/models.py:519
+#: AKModel/models.py:532
 msgid "Time of creation"
 msgstr "Zeitpunkt der ERstellung"
 
-#: AKModel/models.py:520
+#: AKModel/models.py:533
 msgid "Manually Resolved"
 msgstr "Manuell behoben"
 
-#: AKModel/models.py:521
+#: AKModel/models.py:534
 msgid "Mark this violation manually as resolved"
 msgstr "Markiere diese Verletzung manuell als behoben"
 
-#: AKModel/models.py:548
+#: AKModel/models.py:561
 #: AKModel/templates/admin/AKModel/requirements_overview.html:27
 msgid "Details"
 msgstr "Details"
 
-#: AKModel/models.py:657
+#: AKModel/models.py:670
 msgid "Default Slot"
 msgstr "Standardslot"
 
-#: AKModel/models.py:662
+#: AKModel/models.py:675
 msgid "Slot End"
 msgstr "Ende des Slots"
 
-#: AKModel/models.py:662
+#: AKModel/models.py:675
 msgid "Time and date the slot ends"
 msgstr "Zeit und Datum zu der der Slot endet"
 
-#: AKModel/models.py:667
+#: AKModel/models.py:680
 msgid "Primary categories"
 msgstr "Primäre Kategorien"
 
-#: AKModel/models.py:668
+#: AKModel/models.py:681
 msgid "Categories that should be assigned to this slot primarily"
 msgstr "Kategorieren, die diesem Slot primär zugewiesen werden sollen"
 
-#: AKModel/site.py:10
+#: AKModel/site.py:11
 msgid "Administration"
 msgstr "Verwaltung"
 
@@ -1015,7 +1015,7 @@ msgid "No AKs with this requirement"
 msgstr "Kein AK mit dieser Anforderung"
 
 #: AKModel/templates/admin/AKModel/requirements_overview.html:45
-#: AKModel/views/status.py:150
+#: AKModel/views/status.py:156
 msgid "Add Requirement"
 msgstr "Anforderung hinzufügen"
 
@@ -1068,7 +1068,7 @@ msgstr "Bisher keine Räume"
 msgid "Active Events"
 msgstr "Aktive Events"
 
-#: AKModel/templates/admin/ak_index.html:16 AKModel/views/status.py:85
+#: AKModel/templates/admin/ak_index.html:16 AKModel/views/status.py:87
 msgid "Scheduling"
 msgstr "Scheduling"
 
@@ -1101,160 +1101,160 @@ msgstr "Login"
 msgid "Register"
 msgstr "Registrieren"
 
-#: AKModel/views/ak.py:14
+#: AKModel/views/ak.py:17
 msgid "Requirements for Event"
 msgstr "Anforderungen für das Event"
 
-#: AKModel/views/ak.py:28
+#: AKModel/views/ak.py:34
 msgid "AK CSV Export"
 msgstr "AK-CSV-Export"
 
-#: AKModel/views/ak.py:42
+#: AKModel/views/ak.py:48
 msgid "AK Wiki Export"
 msgstr "AK-Wiki-Export"
 
-#: AKModel/views/ak.py:53 AKModel/views/manage.py:41
+#: AKModel/views/ak.py:59 AKModel/views/manage.py:42
 msgid "Wishes"
 msgstr "Wünsche"
 
-#: AKModel/views/ak.py:60
+#: AKModel/views/ak.py:71
 msgid "Delete AK Orga Messages"
 msgstr "AK-Organachrichten löschen"
 
-#: AKModel/views/ak.py:75
+#: AKModel/views/ak.py:86
 msgid "AK Orga Messages successfully deleted"
 msgstr "AK-Organachrichten erfolgreich gelöscht"
 
-#: AKModel/views/ak.py:82
+#: AKModel/views/ak.py:98
 msgid "Interest of the following AKs will be set to not filled (-1):"
 msgstr "Interesse an den folgenden AKs wird auf nicht ausgefüllt (-1) gesetzt:"
 
-#: AKModel/views/ak.py:83
+#: AKModel/views/ak.py:99
 msgid "Reset of interest in AKs successful."
 msgstr "Interesse an AKs erfolgreich zurückgesetzt."
 
-#: AKModel/views/ak.py:92
+#: AKModel/views/ak.py:113
 msgid "Interest counter of the following AKs will be set to 0:"
 msgstr "Interessensbekundungszähler der folgenden AKs wird auf 0 gesetzt:"
 
-#: AKModel/views/ak.py:93
+#: AKModel/views/ak.py:114
 msgid "AKs' interest counters set back to 0."
 msgstr "Interessenszähler der AKs zurückgesetzt"
 
-#: AKModel/views/event_wizard.py:69
+#: AKModel/views/event_wizard.py:70
 #, python-format
 msgid "Copied '%(obj)s'"
 msgstr "'%(obj)s' kopiert"
 
-#: AKModel/views/event_wizard.py:72
+#: AKModel/views/event_wizard.py:73
 #, python-format
 msgid "Could not copy '%(obj)s' (%(error)s)"
 msgstr "'%(obj)s' konnte nicht kopiert werden (%(error)s)"
 
-#: AKModel/views/manage.py:25 AKModel/views/status.py:125
+#: AKModel/views/manage.py:25 AKModel/views/status.py:129
 msgid "Export AK Slides"
 msgstr "AK-Folien exportieren"
 
-#: AKModel/views/manage.py:36
+#: AKModel/views/manage.py:37
 msgid "Symbols"
 msgstr "Symbole"
 
-#: AKModel/views/manage.py:37
+#: AKModel/views/manage.py:38
 msgid "Who?"
 msgstr "Wer?"
 
-#: AKModel/views/manage.py:38
+#: AKModel/views/manage.py:39
 msgid "Duration(s)"
 msgstr "Dauer(n)"
 
-#: AKModel/views/manage.py:39
+#: AKModel/views/manage.py:40
 msgid "Reso intention?"
 msgstr "Resolutionsabsicht?"
 
-#: AKModel/views/manage.py:40
+#: AKModel/views/manage.py:41
 msgid "Category (for Wishes)"
 msgstr "Kategorie (für Wünsche)"
 
-#: AKModel/views/manage.py:77
+#: AKModel/views/manage.py:78
 msgid "The following Constraint Violations will be marked as manually resolved"
 msgstr ""
 "Die folgenden Constraintverletzungen werden als manuell behoben markiert."
 
-#: AKModel/views/manage.py:78
+#: AKModel/views/manage.py:79
 msgid "Constraint Violations marked as resolved"
 msgstr "Constraintverletzungen als manuell behoben markiert"
 
-#: AKModel/views/manage.py:87
+#: AKModel/views/manage.py:88
 msgid "The following Constraint Violations will be set to level 'violation'"
 msgstr ""
 "Die folgenden Constraintverletzungen werden auf das Level \"Violation\" "
 "gesetzt."
 
-#: AKModel/views/manage.py:88
+#: AKModel/views/manage.py:89
 msgid "Constraint Violations set to level 'violation'"
 msgstr "Constraintverletzungen auf Level \"Violation\" gesetzt"
 
-#: AKModel/views/manage.py:97
+#: AKModel/views/manage.py:98
 msgid "The following Constraint Violations will be set to level 'warning'"
 msgstr ""
 "Die folgenden Constraintverletzungen werden auf das Level 'warning' gesetzt."
 
-#: AKModel/views/manage.py:98
+#: AKModel/views/manage.py:99
 msgid "Constraint Violations set to level 'warning'"
 msgstr "Constraintverletzungen auf Level \"Warning\" gesetzt"
 
-#: AKModel/views/manage.py:107
+#: AKModel/views/manage.py:108
 msgid "Publish the plan(s) of:"
 msgstr "Den Plan/die Pläne veröffentlichen von:"
 
-#: AKModel/views/manage.py:108
+#: AKModel/views/manage.py:109
 msgid "Plan published"
 msgstr "Plan veröffentlicht"
 
-#: AKModel/views/manage.py:117
+#: AKModel/views/manage.py:118
 msgid "Unpublish the plan(s) of:"
 msgstr "Den Plan/die Pläne verbergen von:"
 
-#: AKModel/views/manage.py:118
+#: AKModel/views/manage.py:119
 msgid "Plan unpublished"
 msgstr "Plan verborgen"
 
-#: AKModel/views/manage.py:127 AKModel/views/status.py:109
+#: AKModel/views/manage.py:128 AKModel/views/status.py:113
 msgid "Edit Default Slots"
 msgstr "Standardslots bearbeiten"
 
-#: AKModel/views/manage.py:164
+#: AKModel/views/manage.py:165
 #, python-brace-format
 msgid "Could not update slot {id} since it does not belong to {event}"
 msgstr ""
 "Konnte  Slot {id} nicht bearbeiten, da er nicht zum Event {event} gehört"
 
-#: AKModel/views/manage.py:194
+#: AKModel/views/manage.py:195
 #, python-brace-format
 msgid "Updated {u} slot(s). created {c} new slot(s) and deleted {d} slot(s)"
 msgstr ""
 "{u} Slot(s) aktualisiert, {c} Slot(s) hinzugefügt und {d} Slot(s) gelöscht"
 
-#: AKModel/views/room.py:32
+#: AKModel/views/room.py:34
 #, python-format
 msgid "Created Room '%(room)s'"
 msgstr "Raum '%(room)s' angelegt"
 
-#: AKModel/views/room.py:38 AKModel/views/status.py:64
+#: AKModel/views/room.py:41 AKModel/views/status.py:66
 msgid "Import Rooms from CSV"
 msgstr "Räume aus CSV importieren"
 
-#: AKModel/views/room.py:73
+#: AKModel/views/room.py:77
 #, python-brace-format
 msgid "Could not import room {name}: {e}"
 msgstr "Konnte Raum {name} nicht importieren: {e}"
 
-#: AKModel/views/room.py:77
+#: AKModel/views/room.py:81
 #, python-brace-format
 msgid "Imported {count} room(s)"
 msgstr "{count} Raum/Räume importiert"
 
-#: AKModel/views/room.py:79
+#: AKModel/views/room.py:83
 msgid "No rooms imported"
 msgstr "Keine Räume importiert"
 
@@ -1270,35 +1270,35 @@ msgstr "Kategorien"
 msgid "Add category"
 msgstr "Kategorie hinzufügen"
 
-#: AKModel/views/status.py:48
+#: AKModel/views/status.py:49
 msgid "Add Room"
 msgstr "Raum hinzufügen"
 
-#: AKModel/views/status.py:98
+#: AKModel/views/status.py:100
 msgid "AKs requiring special attention"
 msgstr "AKs, die besondere Aufmerksamkeit benötigen"
 
-#: AKModel/views/status.py:102
+#: AKModel/views/status.py:104
 msgid "Enter Interest"
 msgstr "Interesse erfassen"
 
-#: AKModel/views/status.py:113
+#: AKModel/views/status.py:117
 msgid "Manage ak tracks"
 msgstr "AK-Tracks verwalten"
 
-#: AKModel/views/status.py:117
+#: AKModel/views/status.py:121
 msgid "Export AKs as CSV"
 msgstr "AKs als CSV exportieren"
 
-#: AKModel/views/status.py:121
+#: AKModel/views/status.py:125
 msgid "Export AKs for Wiki"
 msgstr "AKs im Wiki-Format exportieren"
 
-#: AKModel/views/status.py:146
+#: AKModel/views/status.py:152
 msgid "Show AKs for requirements"
 msgstr "Zu Anforderungen gehörige AKs anzeigen"
 
-#: AKModel/views/status.py:157
+#: AKModel/views/status.py:163
 msgid "Event Status"
 msgstr "Eventstatus"
 
diff --git a/AKModel/management/commands/makemessages.py b/AKModel/management/commands/makemessages.py
index d3e9149e..bd9f89e1 100644
--- a/AKModel/management/commands/makemessages.py
+++ b/AKModel/management/commands/makemessages.py
@@ -1,13 +1,15 @@
-"""
-Ensure PO files are generated using forward slashes in the location comments on all operating systems
-"""
 import os
 
 from django.core.management.commands.makemessages import Command as MakeMessagesCommand
 
 
 class Command(MakeMessagesCommand):
+    """
+    Adapted version of the :class:`MakeMessagesCommand`
+    Ensure PO files are generated using forward slashes in the location comments on all operating systems
+    """
     def find_files(self, root):
+        # Replace backward slashes with forward slashes if necessary in file list
         all_files = super().find_files(root)
         if os.sep != "\\":
             return all_files
@@ -21,17 +23,19 @@ class Command(MakeMessagesCommand):
         return all_files
 
     def build_potfiles(self):
+        # Replace backward slashes with forward slashes if necessary in the files itself
         pot_files = super().build_potfiles()
         if os.sep != "\\":
             return pot_files
 
         for filename in pot_files:
-            lines = open(filename, "r", encoding="utf-8").readlines()
-            fixed_lines = []
-            for line in lines:
-                if line.startswith("#: "):
-                    line = line.replace("\\", "/")
-                fixed_lines.append(line)
+            with open(filename, "r", encoding="utf-8") as f:
+                lines = f.readlines()
+                fixed_lines = []
+                for line in lines:
+                    if line.startswith("#: "):
+                        line = line.replace("\\", "/")
+                    fixed_lines.append(line)
 
             with open(filename, "w", encoding="utf-8") as f:
                 f.writelines(fixed_lines)
diff --git a/AKModel/metaviews/__init__.py b/AKModel/metaviews/__init__.py
index f5a3ee68..3956af08 100644
--- a/AKModel/metaviews/__init__.py
+++ b/AKModel/metaviews/__init__.py
@@ -1,3 +1,5 @@
 from AKModel.metaviews.status import StatusManager
 
+# create on instance of the :class:`AKModel.metaviews.status.StatusManager`
+# that can then be accessed everywhere (singleton pattern)
 status_manager = StatusManager()
diff --git a/AKModel/metaviews/admin.py b/AKModel/metaviews/admin.py
index 9c6f9027..fb0ff606 100644
--- a/AKModel/metaviews/admin.py
+++ b/AKModel/metaviews/admin.py
@@ -13,36 +13,61 @@ from AKModel.models import Event
 class EventSlugMixin:
     """
     Mixin to handle views with event slugs
+
+    This will make the relevant event available as self.event in all kind types of requests
+    (generic GET and POST views, list views, dispatching, create views)
     """
+    # pylint: disable=no-member
     event = None
 
     def _load_event(self):
+        """
+        Perform the real loading of the event data (as specified by event_slug in the URL) into self.event
+        """
         # Find event based on event slug
         if self.event is None:
             self.event = get_object_or_404(Event, slug=self.kwargs.get("event_slug", None))
 
     def get(self, request, *args, **kwargs):
+        """
+        Override GET request handling to perform loading event first
+        """
         self._load_event()
         return super().get(request, *args, **kwargs)
 
     def post(self, request, *args, **kwargs):
+        """
+        Override POST request handling to perform loading event first
+        """
         self._load_event()
         return super().post(request, *args, **kwargs)
 
     def list(self, request, *args, **kwargs):
+        """
+        Override list view request handling to perform loading event first
+        """
         self._load_event()
         return super().list(request, *args, **kwargs)
 
     def create(self, request, *args, **kwargs):
+        """
+        Override create view request handling to perform loading event first
+        """
         self._load_event()
         return super().create(request, *args, **kwargs)
 
     def dispatch(self, request, *args, **kwargs):
+        """
+        Override dispatch which is called in many generic views to perform loading event first
+        """
         if self.event is None:
             self._load_event()
         return super().dispatch(request, *args, **kwargs)
 
     def get_context_data(self, *, object_list=None, **kwargs):
+        """
+        Override `get_context_data` to make the event information available in the rendering context as `event`. too
+        """
         context = super().get_context_data(object_list=object_list, **kwargs)
         # Add event to context (to make it accessible in templates)
         context["event"] = self.event
@@ -55,15 +80,29 @@ class FilterByEventSlugMixin(EventSlugMixin):
     """
 
     def get_queryset(self):
-        # Filter current queryset based on url event slug or return 404 if event slug is invalid
+        """
+        Get adapted queryset:
+        Filter current queryset based on url event slug or return 404 if event slug is invalid
+        :return: Queryset
+        """
         return super().get_queryset().filter(event=self.event)
 
 
 class AdminViewMixin:
+    """
+    Mixin to provide context information needed in custom admin views
+
+    Will either use default information for `site_url` and `title` or allows to set own values for that
+    """
+    # pylint: disable=too-few-public-methods
+
     site_url = ''
     title = ''
 
     def get_context_data(self, **kwargs):
+        """
+        Extend context
+        """
         extra = admin.site.each_context(self.request)
         extra.update(super().get_context_data(**kwargs))
 
@@ -76,10 +115,19 @@ class AdminViewMixin:
 
 
 class IntermediateAdminView(AdminViewMixin, FormView):
+    """
+    Metaview: Handle typical "action but with preview and confirmation step before" workflow
+    """
     template_name = "admin/AKModel/action_intermediate.html"
     form_class = AdminIntermediateForm
 
     def get_preview(self):
+        """
+        Render a preview of the action to be performed.
+        Default is empty
+        :return: preview (html)
+        :rtype: str
+        """
         return ""
 
     def get_context_data(self, **kwargs):
@@ -90,7 +138,18 @@ class IntermediateAdminView(AdminViewMixin, FormView):
 
 
 class WizardViewMixin:
+    """
+    Mixin to create wizard-like views.
+    This visualizes the progress of the user in the creation process and provides the interlinking to the next step
+
+    In the current implementation, the steps of the wizard are hardcoded here,
+    hence this mixin can only be used for the event creation wizard
+    """
+    # pylint: disable=too-few-public-methods
     def get_context_data(self, **kwargs):
+        """
+        Extend context
+        """
         context = super().get_context_data(**kwargs)
         context["wizard_step"] = self.wizard_step
         context["wizard_steps"] = [
@@ -107,10 +166,23 @@ class WizardViewMixin:
 
 
 class IntermediateAdminActionView(IntermediateAdminView, ABC):
+    """
+    Abstract base view: Intermediate action view (preview & confirmation see :class:`IntermediateAdminView`)
+    for custom admin actions (marking multiple objects in a django admin model instances list with a checkmark and then
+    choosing an action from the dropdown).
+
+    This will automatically handle the decoding of the URL encoding of the list of primary keys django does to select
+    which objects the action should be run on, then display a preview, perform the action after confirmation and
+    redirect again to the object list including display of a confirmation message
+    """
+    # pylint: disable=no-member
     form_class = AdminIntermediateActionForm
     entities = None
 
     def get_queryset(self, pks=None):
+        """
+        Get the queryset of objects to perform the action on
+        """
         if pks is None:
             pks = self.request.GET['pks']
         return self.model.objects.filter(pk__in=pks.split(","))
@@ -130,7 +202,10 @@ class IntermediateAdminActionView(IntermediateAdminView, ABC):
 
     @abstractmethod
     def action(self, form):
-        pass
+        """
+        The real action to perform
+        :param form: form holding the data probably needed for the action
+        """
 
     def form_valid(self, form):
         self.entities = self.get_queryset(pks=form.cleaned_data['pks'])
@@ -140,7 +215,21 @@ class IntermediateAdminActionView(IntermediateAdminView, ABC):
 
 
 class LoopActionMixin(ABC):
-    def action(self, form):
+    """
+    Mixin for the typical kind of action where one needs to loop over all elements
+    and perform a certain function on each of them
+
+    The action is performed by overriding `perform_action(self, entity)`
+    further customization can be reached with the two callbacks `pre_action()` and `post_action()`
+    that are called before and after performing the action loop
+    """
+    def action(self, form):  # pylint: disable=unused-argument
+        """
+        The real action to perform.
+        Will perform the loop, perform the action on each aelement and call the callbacks
+
+        :param form: form holding the data probably needed for the action
+        """
         self.pre_action()
         for entity in self.entities:
             self.perform_action(entity)
@@ -149,10 +238,18 @@ class LoopActionMixin(ABC):
 
     @abstractmethod
     def perform_action(self, entity):
-        pass
+        """
+        Action to perform on each entity
+
+        :param entity: entity to perform the action on
+        """
 
     def pre_action(self):
-        pass
+        """
+        Callback for custom action before loop starts
+        """
 
     def post_action(self):
-        pass
+        """
+        Callback for custom action after loop finished
+        """
diff --git a/AKModel/metaviews/status.py b/AKModel/metaviews/status.py
index 2e1c3777..5579a745 100644
--- a/AKModel/metaviews/status.py
+++ b/AKModel/metaviews/status.py
@@ -8,6 +8,9 @@ from AKModel.metaviews.admin import AdminViewMixin
 
 
 class StatusWidget(ABC):
+    """
+    Abstract parent for status page widgets
+    """
     title = "Status Widget"
     actions = []
     status = "primary"
@@ -18,7 +21,6 @@ class StatusWidget(ABC):
         """
         Which model/context is needed to render this widget?
         """
-        pass
 
     def get_context_data(self, context) -> dict:
         """
@@ -32,6 +34,7 @@ class StatusWidget(ABC):
         Render widget based on context
 
         :param context: Context for rendering
+        :param request: HTTP request, needed for rendering
         :return: Dictionary containing the rendered/prepared information
         """
         context = self.get_context_data(context)
@@ -42,7 +45,7 @@ class StatusWidget(ABC):
             "status": self.render_status(context),
         }
 
-    def render_title(self, context: {}) -> str:
+    def render_title(self, context: {}) -> str:  # pylint: disable=unused-argument
         """
         Render title for widget based on context
 
@@ -52,7 +55,7 @@ class StatusWidget(ABC):
         """
         return self.title
 
-    def render_status(self, context: {}) -> str:
+    def render_status(self, context: {}) -> str:  # pylint: disable=unused-argument
         """
         Render status for widget based on context
 
@@ -63,16 +66,16 @@ class StatusWidget(ABC):
         return self.status
 
     @abstractmethod
-    def render_body(self, context: {}, request) -> str:
+    def render_body(self, context: {}, request) -> str:  # pylint: disable=unused-argument
         """
         Render body for widget based on context
 
         :param context: Context for rendering
+        :param request: HTTP request (needed for rendering)
         :return: Rendered widget body (HTML)
         """
-        pass
 
-    def render_actions(self, context: {}) -> list[dict]:
+    def render_actions(self, context: {}) -> list[dict]:  # pylint: disable=unused-argument
         """
         Render actions for widget based on context
 
@@ -81,16 +84,30 @@ class StatusWidget(ABC):
         :param context: Context for rendering
         :return: List of actions
         """
-        return [a for a in self.actions]
+        return self.actions
 
 
 class TemplateStatusWidget(StatusWidget):
+    """
+    A :class:`StatusWidget` that produces its content by rendering a given html template
+    """
     @property
     @abstractmethod
     def template_name(self) -> str:
-        pass
+        """
+        Configure the template to use
+        :return: name of the template to use
+        """
 
     def render_body(self, context: {}, request) -> str:
+        """
+        Render the body of the widget using the template rendering method from django
+        (load and render template using the provided context)
+
+        :param context: context to use for rendering
+        :param request: HTTP request (needed by django)
+        :return: rendered content (HTML)
+        """
         template = loader.get_template(self.template_name)
         return template.render(context, request)
 
@@ -98,6 +115,8 @@ class TemplateStatusWidget(StatusWidget):
 class StatusManager:
     """
     Registry for all status widgets
+
+    Allows to register status widgets using the `@status_manager.register(name="xyz")` decorator
     """
     widgets = {}
     widgets_by_context_type = defaultdict(list)
@@ -131,6 +150,9 @@ class StatusManager:
 
 
 class StatusView(ABC, AdminViewMixin, TemplateView):
+    """
+    Abstract view: A generic base view to create a status page holding multiple widgets
+    """
     template_name = "admin/AKModel/status/status.html"
 
     @property
@@ -139,12 +161,15 @@ class StatusView(ABC, AdminViewMixin, TemplateView):
         """
         Which model/context is provided by this status view?
         """
-        pass
 
     def get(self, request, *args, **kwargs):
         context = self.get_context_data(**kwargs)
 
-        from AKModel.metaviews import status_manager
-        context['widgets'] = [w.render(context, self.request) for w in status_manager.get_by_context_type(self.provided_context_type)]
+        # Load status manager (local import to prevent cyclic import)
+        from AKModel.metaviews import status_manager  # pylint: disable=import-outside-toplevel
+
+        # Render all widgets and provide them as part of the context
+        context['widgets'] = [w.render(context, self.request)
+                              for w in status_manager.get_by_context_type(self.provided_context_type)]
 
         return self.render_to_response(context)
diff --git a/AKModel/models.py b/AKModel/models.py
index 3b1a9da9..926b7e0e 100644
--- a/AKModel/models.py
+++ b/AKModel/models.py
@@ -14,7 +14,8 @@ from timezone_field import TimeZoneField
 
 
 class Event(models.Model):
-    """ An event supplies the frame for all Aks.
+    """
+    An event supplies the frame for all Aks.
     """
     name = models.CharField(max_length=64, unique=True, verbose_name=_('Name'),
                             help_text=_('Name or iteration of the event'))
@@ -50,8 +51,8 @@ class Event(models.Model):
                                        help_text=_('Default length in hours that is assumed for AKs in this event.'))
 
     contact_email = models.EmailField(verbose_name=_("Contact email address"), blank=True,
-                                      help_text=_(
-                                          "An email address that is displayed on every page and can be used for all kinds of questions"))
+                                        help_text=_("An email address that is displayed on every page "
+                                                    "and can be used for all kinds of questions"))
 
     class Meta:
         verbose_name = _('Event')
@@ -63,25 +64,37 @@ class Event(models.Model):
 
     @staticmethod
     def get_by_slug(slug):
+        """
+        Get event by its slug
+
+        :param slug: slug of the event
+        :return: event identified by the slug
+        :rtype: Event
+        """
         return Event.objects.get(slug=slug)
 
     @staticmethod
     def get_next_active():
-        # Get first active event taking place
+        """
+        Get first active event taking place
+        :return: matching event (if any) or None
+        :rtype: Event
+        """
         event = Event.objects.filter(active=True).order_by('start').first()
         # No active event? Return the next event taking place
         if event is None:
             event = Event.objects.order_by('start').filter(start__gt=datetime.now()).first()
         return event
 
-    def get_categories_with_aks(self, wishes_seperately=False, filter=lambda ak: True, hide_empty_categories=False):
+    def get_categories_with_aks(self, wishes_seperately=False,
+                                filter_func=lambda ak: True, hide_empty_categories=False):
         """
         Get AKCategories as well as a list of AKs belonging to the category for this event
 
         :param wishes_seperately: Return wishes as individual list.
         :type wishes_seperately: bool
-        :param filter: Optional filter predicate, only include AK in list if filter returns True
-        :type filter: (AK)->bool
+        :param filter_func: Optional filter predicate, only include AK in list if filter returns True
+        :type filter_func: (AK)->bool
         :return: list of category-AK-list-tuples, optionally the additional list of AK wishes
         :rtype: list[(AKCategory, list[AK])] [, list[AK]]
         """
@@ -89,11 +102,26 @@ class Event(models.Model):
         categories_with_aks = []
         ak_wishes = []
 
+        # Fill lists by iterating
+        # A different behavior is needed depending on whether wishes should show up inside their categories
+        # or as a separate category
+
+        def _get_category_aks(category):
+            """
+            Get all AKs belonging to a category
+            Use joining and prefetching to reduce the number of necessary SQL queries
+
+            :param category: category the AKs should belong to
+            :return: QuerySet over AKs
+            :return: QuerySet[AK]
+            """
+            return category.ak_set.select_related('event').prefetch_related('owners', 'akslot_set').all()
+
         if wishes_seperately:
             for category in categories:
                 ak_list = []
-                for ak in category.ak_set.select_related('event').prefetch_related('owners', 'akslot_set').all():
-                    if filter(ak):
+                for ak in _get_category_aks(category):
+                    if filter_func(ak):
                         if ak.wish:
                             ak_wishes.append(ak)
                         else:
@@ -101,21 +129,36 @@ class Event(models.Model):
                 if not hide_empty_categories or len(ak_list) > 0:
                     categories_with_aks.append((category, ak_list))
             return categories_with_aks, ak_wishes
-        else:
-            for category in categories:
-                ak_list = []
-                for ak in category.ak_set.all():
-                    if filter(ak):
-                        ak_list.append(ak)
-                if not hide_empty_categories or len(ak_list) > 0:
-                    categories_with_aks.append((category, ak_list))
-            return categories_with_aks
+
+        for category in categories:
+            ak_list = []
+            for ak in _get_category_aks(category):
+                if filter_func(ak):
+                    ak_list.append(ak)
+            if not hide_empty_categories or len(ak_list) > 0:
+                categories_with_aks.append((category, ak_list))
+        return categories_with_aks
 
     def get_unscheduled_wish_slots(self):
+        """
+        Get all slots of wishes that are currently not scheduled
+        :return: queryset of theses slots
+        :rtype: QuerySet[AKSlot]
+        """
         return self.akslot_set.filter(start__isnull=True).annotate(Count('ak__owners')).filter(ak__owners__count=0)
 
     def get_aks_without_availabilities(self):
-        return self.ak_set.annotate(Count('availabilities', distinct=True)).annotate(Count('owners', distinct=True)).filter(availabilities__count=0, owners__count__gt=0)
+        """
+        Gt all AKs that don't have any availability at all
+
+        :return: generator over these AKs
+        :rtype: Generator[AK]
+        """
+        return (self.ak_set
+                .annotate(Count('availabilities', distinct=True))
+                .annotate(Count('owners', distinct=True))
+                .filter(availabilities__count=0, owners__count__gt=0)
+                )
 
 
 class AKOwner(models.Model):
@@ -141,21 +184,34 @@ class AKOwner(models.Model):
         return self.name
 
     def _generate_slug(self):
+        """
+        Auto-generate a slug for an owner
+        This will start with a very simple slug (the name truncated to a maximum length) and then gradually produce
+        more complicated slugs when the previous candidates are already used
+
+        :return: the slug
+        :rtype: str
+        """
         max_length = self._meta.get_field('slug').max_length
 
+        # Try name alone (truncated if necessary)
         slug_candidate = slugify(self.name)[:max_length]
         if not AKOwner.objects.filter(event=self.event, slug=slug_candidate).exists():
             self.slug = slug_candidate
             return
+
+        # Try name and institution separated by '_' (truncated if necessary)
         slug_candidate = slugify(slug_candidate + '_' + self.institution)[:max_length]
         if not AKOwner.objects.filter(event=self.event, slug=slug_candidate).exists():
             self.slug = slug_candidate
             return
+
+        # Try name + institution + an incrementing digit
         for i in itertools.count(1):
             if not AKOwner.objects.filter(event=self.event, slug=slug_candidate).exists():
                 break
             digits = len(str(i))
-            slug_candidate = '{}-{}'.format(slug_candidate[:-(digits + 1)], i)
+            slug_candidate = f'{slug_candidate[:-(digits + 1)]}-{i}'
 
         self.slug = slug_candidate
 
@@ -167,6 +223,15 @@ class AKOwner(models.Model):
 
     @staticmethod
     def get_by_slug(event, slug):
+        """
+        Get owner by slug
+        Will be identified by the combination of event slug and owner slug which is unique
+
+        :param event: event
+        :param slug: slug of the owner
+        :return: owner identified by slugs
+        :rtype: AKOwner
+        """
         return AKOwner.objects.get(event=event, slug=slug)
 
 
@@ -178,8 +243,8 @@ class AKCategory(models.Model):
     description = models.TextField(blank=True, verbose_name=_("Description"),
                                    help_text=_("Short description of this AK Category"))
     present_by_default = models.BooleanField(blank=True, default=True, verbose_name=_("Present by default"),
-                                             help_text=_(
-                                                 "Present AKs of this category by default if AK owner did not specify whether this AK should be presented?"))
+                                             help_text=_("Present AKs of this category by default "
+                                                 "if AK owner did not specify whether this AK should be presented?"))
 
     event = models.ForeignKey(to=Event, on_delete=models.CASCADE, verbose_name=_('Event'),
                               help_text=_('Associated event'))
@@ -213,6 +278,11 @@ class AKTrack(models.Model):
         return self.name
 
     def aks_with_category(self):
+        """
+        Get all AKs that belong to this track with category already joined to prevent additional SQL queries
+        :return: queryset over the AKs
+        :rtype: QuerySet[AK]
+        """
         return self.ak_set.select_related('category').all()
 
 
@@ -268,7 +338,8 @@ class AK(models.Model):
                                            help_text=_('AKs that should precede this AK in the schedule'))
 
     notes = models.TextField(blank=True, verbose_name=_('Organizational Notes'), help_text=_(
-        'Notes to organizers. These are public. For private notes, please use the button for private messages on the detail page of this AK (after creation/editing).'))
+        'Notes to organizers. These are public. For private notes, please use the button for private messages '
+        'on the detail page of this AK (after creation/editing).'))
 
     interest = models.IntegerField(default=-1, verbose_name=_('Interest'), help_text=_('Expected number of people'))
     interest_counter = models.IntegerField(default=0, verbose_name=_('Interest Counter'),
@@ -295,8 +366,16 @@ class AK(models.Model):
 
     @property
     def details(self):
+        """
+        Generate a detailled string representation, e.g., for usage in scheduling
+        :return: string representation of that AK with all details
+        :rtype: str
+        """
+        # local import to prevent cyclic import
+        # pylint: disable=import-outside-toplevel
         from AKModel.availability.models import Availability
-        availabilities = ', \n'.join(f'{a.simplified}' for a in Availability.objects.select_related('event').filter(ak=self))
+        availabilities = ', \n'.join(f'{a.simplified}' for a in Availability.objects.select_related('event')
+                                     .filter(ak=self))
         return f"""{self.name}{" (R)" if self.reso else ""}:
         
         {self.owners_list}
@@ -309,34 +388,74 @@ class AK(models.Model):
 
     @property
     def owners_list(self):
+        """
+        Get a list of stringified representations of all owners
+
+        :return: list of owners
+        :rtype: List[str]
+        """
         return ", ".join(str(owner) for owner in self.owners.all())
 
     @property
     def durations_list(self):
+        """
+        Get a list of stringified representations of all durations of associated slots
+
+        :return: list of durations
+        :rtype: List[str]
+        """
         return ", ".join(str(slot.duration_simplified) for slot in self.akslot_set.select_related('event').all())
 
     @property
     def wish(self):
+        """
+        Is the AK a wish?
+        :return: true if wish, false if not
+        :rtype: bool
+        """
         return self.owners.count() == 0
 
     def increment_interest(self):
+        """
+        Increment the interest counter for this AK by one
+        without tracking that change to prevent an unreadable and large history
+        """
         self.interest_counter += 1
-        self.skip_history_when_saving = True
+        self.skip_history_when_saving = True  # pylint: disable=attribute-defined-outside-init
         self.save()
         del self.skip_history_when_saving
 
     @property
     def availabilities(self):
+        """
+        Get all availabilities associated to this AK
+        :return: availabilities
+        :rtype: QuerySet[Availability]
+        """
         return "Availability".objects.filter(ak=self)
 
     @property
     def edit_url(self):
+        """
+        Get edit URL for this AK
+        Will link to frontend if AKSubmission is active, otherwise to the edit view for this object in admin interface
+
+        :return: URL
+        :rtype: str
+        """
         if apps.is_installed("AKSubmission"):
             return reverse_lazy('submit:ak_edit', kwargs={'event_slug': self.event.slug, 'pk': self.id})
         return reverse_lazy('admin:AKModel_ak_change', kwargs={'object_id': self.id})
 
     @property
     def detail_url(self):
+        """
+        Get detail URL for this AK
+        Will link to frontend if AKSubmission is active, otherwise to the edit view for this object in admin interface
+
+        :return: URL
+        :rtype: str
+        """
         if apps.is_installed("AKSubmission"):
             return reverse_lazy('submit:ak_detail', kwargs={'event_slug': self.event.slug, 'pk': self.id})
         return self.edit_url
@@ -364,6 +483,12 @@ class Room(models.Model):
 
     @property
     def title(self):
+        """
+        Get title of a room, which consists of location and name if location is set, otherwise only the name
+
+        :return: title
+        :rtype: str
+        """
         if self.location:
             return f"{self.location} {self.name}"
         return self.name
@@ -429,7 +554,8 @@ class AKSlot(models.Model):
         start = self.start.astimezone(self.event.timezone)
         end = self.end.astimezone(self.event.timezone)
 
-        return f"{start.strftime('%a %H:%M')} - {end.strftime('%H:%M') if start.day == end.day else end.strftime('%a %H:%M')}"
+        return (f"{start.strftime('%a %H:%M')} - "
+                f"{end.strftime('%H:%M') if start.day == end.day else end.strftime('%a %H:%M')}")
 
     @property
     def end(self):
@@ -448,10 +574,20 @@ class AKSlot(models.Model):
         return (timezone.now() - self.updated).total_seconds()
 
     def overlaps(self, other: "AKSlot"):
+        """
+        Check wether two slots overlap
+
+        :param other: second slot to compare with
+        :return: true if they overlap, false if not:
+        :rtype: bool
+        """
         return self.start < other.end <= self.end or self.start <= other.start < self.end
 
 
 class AKOrgaMessage(models.Model):
+    """
+    Model representing confidential messages to the organizers/scheduling people, belonging to a certain AK
+    """
     ak = models.ForeignKey(to=AK, on_delete=models.CASCADE, verbose_name=_('AK'),
                            help_text=_('AK this message belongs to'))
     text = models.TextField(verbose_name=_("Message text"),
@@ -470,12 +606,23 @@ class AKOrgaMessage(models.Model):
 
 
 class ConstraintViolation(models.Model):
+    """
+    Model to represent any kind of constraint violation
+
+    Can have two different severities: violation and warning
+    The list of possible types is defined in :class:`ViolationType`
+    Depending on the type, different fields (references to other models) will be filled. Each violation should always
+    be related to an event and at least on other instance of a causing entity
+    """
     class Meta:
         verbose_name = _('Constraint Violation')
         verbose_name_plural = _('Constraint Violations')
         ordering = ['-timestamp']
 
     class ViolationType(models.TextChoices):
+        """
+        Possible types of violations with their text representation
+        """
         OWNER_TWO_SLOTS = 'ots', _('Owner has two parallel slots')
         SLOT_OUTSIDE_AVAIL = 'soa', _('AK Slot was scheduled outside the AK\'s availabilities')
         ROOM_TWO_SLOTS = 'rts', _('Room has two AK slots scheduled at the same time')
@@ -490,6 +637,9 @@ class ConstraintViolation(models.Model):
         SLOT_OUTSIDE_EVENT = 'soe', _('AK Slot is scheduled outside the event\'s availabilities')
 
     class ViolationLevel(models.IntegerChoices):
+        """
+        Possible severities/levels of a CV
+        """
         WARNING = 1, _('Warning')
         VIOLATION = 10, _('Violation')
 
@@ -501,6 +651,7 @@ class ConstraintViolation(models.Model):
     event = models.ForeignKey(to=Event, on_delete=models.CASCADE, verbose_name=_('Event'),
                               help_text=_('Associated event'))
 
+    # Possible "causes":
     aks = models.ManyToManyField(to=AK, blank=True, verbose_name=_('AKs'),
                                  help_text=_('AK(s) belonging to this constraint'))
     ak_slots = models.ManyToManyField(to=AKSlot, blank=True, verbose_name=_('AK Slots'),
@@ -551,22 +702,37 @@ class ConstraintViolation(models.Model):
 
     @property
     def details(self):
+        """
+        Property: Details
+        """
         return self.get_details()
 
     @property
-    def edit_url(self):
+    def edit_url(self) -> str:
+        """
+        Property: Edit URL for this CV
+        """
         return reverse_lazy('admin:AKModel_constraintviolation_change', kwargs={'object_id': self.pk})
 
     @property
-    def level_display(self):
+    def level_display(self) -> str:
+        """
+        Property: Severity as string
+        """
         return self.get_level_display()
 
     @property
-    def type_display(self):
+    def type_display(self) -> str:
+        """
+        Property: Type as string
+        """
         return self.get_type_display()
 
     @property
-    def timestamp_display(self):
+    def timestamp_display(self) -> str:
+        """
+        Property: Creation timestamp as string
+        """
         return self.timestamp.astimezone(self.event.timezone).strftime('%d.%m.%y %H:%M')
 
     @property
@@ -585,7 +751,10 @@ class ConstraintViolation(models.Model):
         return self.aks_tmp
 
     @property
-    def _aks_str(self):
+    def _aks_str(self) -> str:
+        """
+        Property: AKs as string
+        """
         if self.pk and self.pk > 0:
             return ', '.join(str(a) for a in self.aks.all())
         return ', '.join(str(a) for a in self.aks_tmp)
@@ -606,7 +775,10 @@ class ConstraintViolation(models.Model):
         return self.ak_slots_tmp
 
     @property
-    def _ak_slots_str(self):
+    def _ak_slots_str(self) -> str:
+        """
+        Property: Slots as string
+        """
         if self.pk and self.pk > 0:
             return ', '.join(str(a) for a in self.ak_slots.select_related('event').all())
         return ', '.join(str(a) for a in self.ak_slots_tmp)
@@ -655,6 +827,10 @@ class ConstraintViolation(models.Model):
 
 
 class DefaultSlot(models.Model):
+    """
+    Model representing a default slot,
+    i.e., a prefered slot to use for typical AKs in the schedule to guarantee enough breaks etc.
+    """
     class Meta:
         verbose_name = _('Default Slot')
         verbose_name_plural = _('Default Slots')
@@ -670,19 +846,31 @@ class DefaultSlot(models.Model):
                                             help_text=_('Categories that should be assigned to this slot primarily'))
 
     @property
-    def start_simplified(self):
+    def start_simplified(self) -> str:
+        """
+        Property: Simplified version of the start timetstamp (weekday, hour, minute) as string
+        """
         return self.start.astimezone(self.event.timezone).strftime('%a %H:%M')
 
     @property
-    def start_iso(self):
+    def start_iso(self) -> str:
+        """
+        Property: Start timestamp as ISO timestamp for usage in calendar views
+        """
         return timezone.localtime(self.start, self.event.timezone).strftime("%Y-%m-%dT%H:%M:%S")
 
     @property
-    def end_simplified(self):
+    def end_simplified(self) -> str:
+        """
+        Property: Simplified version of the end timetstamp (weekday, hour, minute) as string
+        """
         return self.end.astimezone(self.event.timezone).strftime('%a %H:%M')
 
     @property
-    def end_iso(self):
+    def end_iso(self) -> str:
+        """
+        Property: End timestamp as ISO timestamp for usage in calendar views
+        """
         return timezone.localtime(self.end, self.event.timezone).strftime("%Y-%m-%dT%H:%M:%S")
 
     def __str__(self):
diff --git a/AKModel/serializers.py b/AKModel/serializers.py
index 8dbbb4ea..5d9ad603 100644
--- a/AKModel/serializers.py
+++ b/AKModel/serializers.py
@@ -4,36 +4,54 @@ from AKModel.models import AK, Room, AKSlot, AKTrack, AKCategory, AKOwner
 
 
 class AKOwnerSerializer(serializers.ModelSerializer):
+    """
+    REST Framework Serializer for AKOwner
+    """
     class Meta:
         model = AKOwner
         fields = '__all__'
 
 
 class AKCategorySerializer(serializers.ModelSerializer):
+    """
+    REST Framework Serializer for AKCategory
+    """
     class Meta:
         model = AKCategory
         fields = '__all__'
 
 
 class AKTrackSerializer(serializers.ModelSerializer):
+    """
+    REST Framework Serializer for AKTrack
+    """
     class Meta:
         model = AKTrack
         fields = '__all__'
 
 
 class AKSerializer(serializers.ModelSerializer):
+    """
+    REST Framework Serializer for AK
+    """
     class Meta:
         model = AK
         fields = '__all__'
 
 
 class RoomSerializer(serializers.ModelSerializer):
+    """
+    REST Framework Serializer for Room
+    """
     class Meta:
         model = Room
         fields = '__all__'
 
 
 class AKSlotSerializer(serializers.ModelSerializer):
+    """
+    REST Framework Serializer for AKSlot
+    """
     class Meta:
         model = AKSlot
         fields = '__all__'
@@ -41,6 +59,9 @@ class AKSlotSerializer(serializers.ModelSerializer):
     treat_as_local = serializers.BooleanField(required=False, default=False, write_only=True)
 
     def create(self, validated_data:dict):
+        # Handle timezone adaption based upon the control field "treat_as_local":
+        # If it is set, ignore timezone submitted from the browser (will always be UTC)
+        # and treat it as input in the events timezone instead
         if validated_data['treat_as_local']:
             validated_data['start'] = validated_data['start'].replace(tzinfo=None).astimezone(
                 validated_data['event'].timezone)
diff --git a/AKModel/site.py b/AKModel/site.py
index 480689ef..bff49850 100644
--- a/AKModel/site.py
+++ b/AKModel/site.py
@@ -1,17 +1,22 @@
 from django.contrib.admin import AdminSite
 from django.utils.translation import gettext_lazy as _
+# from django.urls import path
 
 from AKModel.models import Event
 
 
 class AKAdminSite(AdminSite):
+    """
+    Custom admin interface definition (extend the admin functionality of Django)
+    """
     index_template = "admin/ak_index.html"
     site_header = f"AKPlanning - {_('Administration')}"
     index_title = _('Administration')
 
     def get_urls(self):
-        from django.urls import path
-
+        """
+        Get URLs -- add further views that are not related to a certain model here if needed
+        """
         urls = super().get_urls()
         urls += [
             # path('...', self.admin_view(...)),
@@ -19,6 +24,8 @@ class AKAdminSite(AdminSite):
         return urls
 
     def index(self, request, extra_context=None):
+        # Override index page rendering to provide extra context (the list of active events)
+        # to be used in the adapted template
         if extra_context is None:
             extra_context = {}
         extra_context["active_events"] = Event.objects.filter(active=True)
diff --git a/AKModel/templatetags/tags_AKModel.py b/AKModel/templatetags/tags_AKModel.py
index 06560dc7..9ca14812 100644
--- a/AKModel/templatetags/tags_AKModel.py
+++ b/AKModel/templatetags/tags_AKModel.py
@@ -8,30 +8,58 @@ from fontawesome_6.app_settings import get_css
 register = template.Library()
 
 
-# Get Footer Info from settings
 @register.simple_tag
 def footer_info():
+    """
+    Get Footer Info from settings
+
+    :return: a dict of several strings like the impress URL to use in the footer
+    :rtype: Dict[str, str]
+    """
     return settings.FOOTER_INFO
 
 
 @register.filter
 def check_app_installed(name):
+    """
+    Check whether the app with the given name is active in this instance
+
+    :param name: name of the app to check for
+    :return: true if app is installed
+    :rtype: bool
+    """
     return apps.is_installed(name)
 
 
 @register.filter
 def message_bootstrap_class(tag):
+    """
+    Turn message severity classes into corresponding bootstrap css classes
+
+    :param tag: severity of the message
+    :return: matching bootstrap class
+    """
     if tag == "error":
         return "alert-danger"
-    elif tag == "success":
+    if tag == "success":
         return "alert-success"
-    elif tag == "warning":
+    if tag == "warning":
         return "alert-warning"
     return "alert-info"
 
 
 @register.filter
 def wiki_owners_export(owners, event):
+    """
+    Preserve owner link information for wiki export by using internal links if possible
+    but external links when owner specified a non-wikilink. This is applied to the full list of owners
+
+    :param owners: list of owners
+    :param event: event this owner belongs to and that is currently exported
+    (specifying this directly prevents unnecesary database lookups)
+    :return: linkified owners list in wiki syntax
+    :rtype: str
+    """
     def to_link(owner):
         if owner.link != '':
             event_link_prefix, _ = event.base_url.rsplit("/", 1)
@@ -44,17 +72,30 @@ def wiki_owners_export(owners, event):
     return ", ".join(to_link(owner) for owner in owners.all())
 
 
+# get list of relevant css fontawesome css files for this instance
 css = get_css()
 
 
 @register.simple_tag
 def fontawesome_6_css():
+    """
+    Create html code to load all required fontawesome css files
+
+    :return: HTML code to load css
+    :rtype: str
+    """
     return mark_safe(conditional_escape('\n').join(format_html(
             '<link href="{}" rel="stylesheet" media="all">', stylesheet) for stylesheet in css))
 
 
 @register.simple_tag
 def fontawesome_6_js():
+    """
+    Create html code to load all required fontawesome javascript files
+
+    :return: HTML code to load js
+    :rtype: str
+    """
     return mark_safe(format_html(
         '<script type="text/javascript" src="{}"></script>', static('fontawesome_6/js/django-fontawesome.js')
-    ))
\ No newline at end of file
+    ))
diff --git a/AKModel/tests.py b/AKModel/tests.py
index d676992f..5b353c30 100644
--- a/AKModel/tests.py
+++ b/AKModel/tests.py
@@ -1,7 +1,7 @@
 import traceback
 from typing import List
 
-from django.contrib.auth.models import User
+from django.contrib.auth import get_user_model
 from django.contrib.messages import get_messages
 from django.contrib.messages.storage.base import Message
 from django.test import TestCase
@@ -12,21 +12,43 @@ from AKModel.models import Event, AKOwner, AKCategory, AKTrack, AKRequirement, A
 
 
 class BasicViewTests:
+    """
+    Parent class for "standard" tests of views
+
+    Provided with a list of views and arguments (if necessary), this will test that views
+    - render correctly without errors
+    - are only reachable with the correct rights (neither too freely nor too restricted)
+
+    To do this, the test creates sample users, fixtures are loaded automatically by the django test framework.
+    It also provides helper functions, e.g., to check for correct messages to the user or more simply generate
+    the URLs to test
+
+    In this class, methods from :class:`TestCase` will be called at multiple places event though TestCase is not a
+    parent of this class but has to be included as parent in concrete implementations of this class seperately.
+    It however still makes sense to treat this class as some kind of mixin and not implement it as a child of TestCase,
+    since the test framework does not understand the concept of abstract test definitions and would handle this class
+    as real test case otherwise, distorting the test results.
+    """
+    # pylint: disable=no-member
     VIEWS = []
     APP_NAME = ''
     VIEWS_STAFF_ONLY = []
     EDIT_TESTCASES = []
 
-    def setUp(self):
-        self.staff_user = User.objects.create(
+    def setUp(self):  # pylint: disable=invalid-name
+        """
+        Setup testing by creating sample users
+        """
+        user_model = get_user_model()
+        self.staff_user = user_model.objects.create(
             username='Test Staff User', email='teststaff@example.com', password='staffpw',
             is_staff=True, is_active=True
         )
-        self.admin_user = User.objects.create(
+        self.admin_user = user_model.objects.create(
             username='Test Admin User', email='testadmin@example.com', password='adminpw',
             is_staff=True, is_superuser=True, is_active=True
         )
-        self.deactivated_user = User.objects.create(
+        self.deactivated_user = user_model.objects.create(
             username='Test Deactivated User', email='testdeactivated@example.com', password='deactivatedpw',
             is_staff=True, is_active=False
         )
@@ -45,6 +67,13 @@ class BasicViewTests:
         return view_name_with_prefix, url
 
     def _assert_message(self, response, expected_message, msg_prefix=""):
+        """
+        Assert that the correct message is shown and cause test to fail if not
+
+        :param response: response to check
+        :param expected_message: message that should be shown
+        :param msg_prefix: prefix for the error message when test fails
+        """
         messages:List[Message] = list(get_messages(response.wsgi_request))
 
         msg_count = "No message shown to user"
@@ -59,21 +88,30 @@ class BasicViewTests:
         self.assertEqual(messages[-1].message, expected_message, msg=msg_content)
 
     def test_views_for_200(self):
+        """
+        Test the list of public views (as specified in "VIEWS") for error-free rendering
+        """
         for view_name in self.VIEWS:
             view_name_with_prefix, url = self._name_and_url(view_name)
             try:
                 response = self.client.get(url)
                 self.assertEqual(response.status_code, 200, msg=f"{view_name_with_prefix} ({url}) broken")
-            except Exception as e:
-                self.fail(f"An error occurred during rendering of {view_name_with_prefix} ({url}):\n\n{traceback.format_exc()}")
+            except Exception: # pylint: disable=broad-exception-caught
+                self.fail(f"An error occurred during rendering of {view_name_with_prefix} ({url}):"
+                          f"\n\n{traceback.format_exc()}")
 
     def test_access_control_staff_only(self):
+        """
+        Test whether internal views (as specified in "VIEWS_STAFF_ONLY" are visible to staff users and staff users only
+        """
+        # Not logged in? Views should not be visible
         self.client.logout()
         for view_name in self.VIEWS_STAFF_ONLY:
             view_name_with_prefix, url = self._name_and_url(view_name)
             response = self.client.get(url)
             self.assertEqual(response.status_code, 302, msg=f"{view_name_with_prefix} ({url}) accessible by non-staff")
 
+        # Logged in? Views should be visible
         self.client.force_login(self.staff_user)
         for view_name in self.VIEWS_STAFF_ONLY:
             view_name_with_prefix, url = self._name_and_url(view_name)
@@ -81,9 +119,11 @@ class BasicViewTests:
                 response = self.client.get(url)
                 self.assertEqual(response.status_code, 200,
                                  msg=f"{view_name_with_prefix} ({url}) should be accessible for staff (but isn't)")
-            except Exception as e:
-                self.fail(f"An error occurred during rendering of {view_name_with_prefix} ({url}):\n\n{traceback.format_exc()}")
+            except Exception:  # pylint: disable=broad-exception-caught
+                self.fail(f"An error occurred during rendering of {view_name_with_prefix} ({url}):"
+                          f"\n\n{traceback.format_exc()}")
 
+        # Disabled user? Views should not be visible
         self.client.force_login(self.deactivated_user)
         for view_name in self.VIEWS_STAFF_ONLY:
             view_name_with_prefix, url = self._name_and_url(view_name)
@@ -91,28 +131,37 @@ class BasicViewTests:
             self.assertEqual(response.status_code, 302,
                              msg=f"{view_name_with_prefix} ({url}) still accessible for deactivated user")
 
-    def _to_sendable_value(self, v):
+    def _to_sendable_value(self, val):
         """
         Create representation sendable via POST from form data
 
-        :param v: value to prepare
-        :type v: any
+        Needed to automatically check create, update and delete views
+
+        :param val: value to prepare
+        :type val: any
         :return: prepared value (normally either raw value or primary key of complex object)
         """
-        if type(v) == list:
-            return [e.pk for e in v]
-        if type(v) == "RelatedManager":
-            return [e.pk for e in v.all()]
-        return v
+        if isinstance(val, list):
+            return [e.pk for e in val]
+        if type(val) == "RelatedManager":  # pylint: disable=unidiomatic-typecheck
+            return [e.pk for e in val.all()]
+        return val
 
     def test_submit_edit_form(self):
         """
-        Test edit forms in the most simple way (sending them again unchanged)
+        Test edit forms (as specified in "EDIT_TESTCASES") in the most simple way (sending them again unchanged)
         """
         for testcase in self.EDIT_TESTCASES:
             self._test_submit_edit_form(testcase)
 
     def _test_submit_edit_form(self, testcase):
+        """
+        Test a single edit form by rendering and sending it again unchanged
+
+        This will test for correct rendering, dispatching/redirecting, messages and access control handling
+
+        :param testcase: details of the form to test
+        """
         name, url = self._name_and_url((testcase["view"], testcase["kwargs"]))
         form_name = testcase.get("form_name", "form")
         expected_code = testcase.get("expected_code", 302)
@@ -145,6 +194,9 @@ class BasicViewTests:
 
 
 class ModelViewTests(BasicViewTests, TestCase):
+    """
+    Basic view test cases for views from AKModel plus some custom tests
+    """
     fixtures = ['model.json']
 
     ADMIN_MODELS = [
@@ -172,35 +224,48 @@ class ModelViewTests(BasicViewTests, TestCase):
     ]
 
     def test_admin(self):
+        """
+        Test basic admin functionality (displaying and interacting with model instances)
+        """
         self.client.force_login(self.admin_user)
         for model in self.ADMIN_MODELS:
+            # Special treatment for a subset of views (where we exchanged default functionality, e.g., create views)
             if model[1] == "event":
-                view_name_with_prefix, url = self._name_and_url((f'admin:new_event_wizard_start', {}))
+                _, url = self._name_and_url(('admin:new_event_wizard_start', {}))
             elif model[1] == "room":
-                view_name_with_prefix, url = self._name_and_url((f'admin:room-new', {}))
+                _, url = self._name_and_url(('admin:room-new', {}))
+            # Otherwise, just call the creation form view
             else:
-                view_name_with_prefix, url = self._name_and_url((f'admin:AKModel_{model[1]}_add', {}))
+                _, url = self._name_and_url((f'admin:AKModel_{model[1]}_add', {}))
             response = self.client.get(url)
             self.assertEqual(response.status_code, 200, msg=f"Add form for model {model[1]} ({url}) broken")
 
         for model in self.ADMIN_MODELS:
+            # Test the update view using the first existing instance of each model
             m = model[0].objects.first()
             if m is not None:
-                view_name_with_prefix, url = self._name_and_url((f'admin:AKModel_{model[1]}_change', {'object_id': m.pk}))
+                _, url = self._name_and_url(
+                    (f'admin:AKModel_{model[1]}_change', {'object_id': m.pk})
+                )
                 response = self.client.get(url)
                 self.assertEqual(response.status_code, 200, msg=f"Edit form for model {model[1]} ({url}) broken")
 
     def test_wiki_export(self):
+        """
+        Test wiki export
+        This will test whether the view renders at all and whether the export list contains the correct AKs
+        """
         self.client.force_login(self.admin_user)
 
-        export_url = reverse_lazy(f"admin:ak_wiki_export", kwargs={'slug': 'kif42'})
+        export_url = reverse_lazy("admin:ak_wiki_export", kwargs={'slug': 'kif42'})
         response = self.client.get(export_url)
         self.assertEqual(response.status_code, 200, "Export not working at all")
 
         export_count = 0
-        for category, aks in response.context["categories_with_aks"]:
+        for _, aks in response.context["categories_with_aks"]:
             for ak in aks:
-                self.assertEqual(ak.include_in_export, True, f"AK with export flag set to False (pk={ak.pk}) included in export")
+                self.assertEqual(ak.include_in_export, True,
+                                 f"AK with export flag set to False (pk={ak.pk}) included in export")
                 self.assertNotEqual(ak.pk, 1, "AK known to be excluded from export (PK 1) included in export")
                 export_count += 1
 
diff --git a/AKModel/urls.py b/AKModel/urls.py
index 80617814..763cc1a2 100644
--- a/AKModel/urls.py
+++ b/AKModel/urls.py
@@ -4,12 +4,14 @@ from django.urls import include, path
 from rest_framework.routers import DefaultRouter
 
 import AKModel.views.api
-from AKModel.views.manage import ExportSlidesView
+from AKModel.views.manage import ExportSlidesView, PlanPublishView, PlanUnpublishView, DefaultSlotEditorView
 from AKModel.views.ak import AKRequirementOverview, AKCSVExportView, AKWikiExportView, AKMessageDeleteView
 from AKModel.views.event_wizard import NewEventWizardStartView, NewEventWizardPrepareImportView, \
     NewEventWizardImportView, NewEventWizardActivateView, NewEventWizardFinishView, NewEventWizardSettingsView
+from AKModel.views.room import RoomBatchCreationView
 from AKModel.views.status import EventStatusView
 
+# Register basic API views/endpoints
 api_router = DefaultRouter()
 api_router.register('akowner', AKModel.views.api.AKOwnerViewSet, basename='AKOwner')
 api_router.register('akcategory', AKModel.views.api.AKCategoryViewSet, basename='AKCategory')
@@ -18,7 +20,9 @@ api_router.register('ak', AKModel.views.api.AKViewSet, basename='AK')
 api_router.register('room', AKModel.views.api.RoomViewSet, basename='Room')
 api_router.register('akslot', AKModel.views.api.AKSlotViewSet, basename='AKSlot')
 
+# TODO Can we move this functionality to the individual apps instead?
 extra_paths = []
+# If AKScheduling is active, register additional API endpoints
 if apps.is_installed("AKScheduling"):
     from AKScheduling.api import ResourcesViewSet, RoomAvailabilitiesView, EventsView, EventsViewSet, \
         ConstraintViolationsViewSet, DefaultSlotsView
@@ -33,9 +37,10 @@ if apps.is_installed("AKScheduling"):
              name='scheduling-room-availabilities')),
     extra_paths.append(path('api/scheduling-default-slots/', DefaultSlotsView.as_view(),
              name='scheduling-default-slots'))
+
+#If AKSubmission is active, register an additional API endpoint for increasing the interest counter
 if apps.is_installed("AKSubmission"):
     from AKSubmission.api import increment_interest_counter
-
     extra_paths.append(path('api/ak/<pk>/indicate-interest/', increment_interest_counter, name='submission-ak-indicate-interest'))
 
 event_specific_paths = [
@@ -45,6 +50,7 @@ event_specific_paths.extend(extra_paths)
 
 app_name = 'model'
 
+# Included all these extra view paths at a path starting with the event slug
 urlpatterns = [
     path(
         '<slug:event_slug>/',
@@ -55,6 +61,9 @@ urlpatterns = [
 
 
 def get_admin_urls_event_wizard(admin_site):
+    """
+    Defines all additional URLs for the event creation wizard
+    """
     return [
         path('add/wizard/start/', admin_site.admin_view(NewEventWizardStartView.as_view()),
              name="new_event_wizard_start"),
@@ -75,6 +84,9 @@ def get_admin_urls_event_wizard(admin_site):
 
 
 def get_admin_urls_event(admin_site):
+    """
+    Defines all additional event-related view URLs that will be included in the event admin interface
+    """
     return [
         path('<slug:event_slug>/status/', admin_site.admin_view(EventStatusView.as_view()), name="event_status"),
         path('<slug:event_slug>/requirements/', admin_site.admin_view(AKRequirementOverview.as_view()),
@@ -86,4 +98,10 @@ def get_admin_urls_event(admin_site):
         path('<slug:event_slug>/delete-orga-messages/', admin_site.admin_view(AKMessageDeleteView.as_view()),
              name="ak_delete_orga_messages"),
         path('<slug:event_slug>/ak-slide-export/', admin_site.admin_view(ExportSlidesView.as_view()), name="ak_slide_export"),
+        path('plan/publish/', admin_site.admin_view(PlanPublishView.as_view()), name="plan-publish"),
+        path('plan/unpublish/', admin_site.admin_view(PlanUnpublishView.as_view()), name="plan-unpublish"),
+        path('<slug:event_slug>/defaultSlots/', admin_site.admin_view(DefaultSlotEditorView.as_view()),
+             name="default-slots-editor"),
+        path('<slug:event_slug>/importRooms/', admin_site.admin_view(RoomBatchCreationView.as_view()),
+             name="room-import"),
     ]
diff --git a/AKModel/views/ak.py b/AKModel/views/ak.py
index 640d398e..3afec5ac 100644
--- a/AKModel/views/ak.py
+++ b/AKModel/views/ak.py
@@ -9,6 +9,9 @@ from AKModel.models import AKRequirement, AKSlot, Event, AKOrgaMessage, AK
 
 
 class AKRequirementOverview(AdminViewMixin, FilterByEventSlugMixin, ListView):
+    """
+    View: Display requirements for the given event
+    """
     model = AKRequirement
     context_object_name = "requirements"
     title = _("Requirements for Event")
@@ -22,6 +25,9 @@ class AKRequirementOverview(AdminViewMixin, FilterByEventSlugMixin, ListView):
 
 
 class AKCSVExportView(AdminViewMixin, FilterByEventSlugMixin, ListView):
+    """
+    View: Export all AK slots of this event in CSV format ordered by tracks
+    """
     template_name = "admin/AKModel/ak_csv_export.html"
     model = AKSlot
     context_object_name = "slots"
@@ -30,12 +36,12 @@ class AKCSVExportView(AdminViewMixin, FilterByEventSlugMixin, ListView):
     def get_queryset(self):
         return super().get_queryset().order_by("ak__track")
 
-    def get_context_data(self, **kwargs):
-        context = super().get_context_data(**kwargs)
-        return context
-
 
 class AKWikiExportView(AdminViewMixin, DetailView):
+    """
+    View: Export AKs of this event in wiki syntax
+    This will show one text field per category, with a separate category/field for wishes
+    """
     template_name = "admin/AKModel/wiki_export.html"
     model = Event
     context_object_name = "event"
@@ -46,7 +52,7 @@ class AKWikiExportView(AdminViewMixin, DetailView):
 
         categories_with_aks, ak_wishes = context["event"].get_categories_with_aks(
             wishes_seperately=True,
-            filter=lambda ak: ak.include_in_export
+            filter_func=lambda ak: ak.include_in_export
         )
 
         context["categories_with_aks"] = [(category.name, ak_list) for category, ak_list in categories_with_aks]
@@ -56,10 +62,18 @@ class AKWikiExportView(AdminViewMixin, DetailView):
 
 
 class AKMessageDeleteView(EventSlugMixin, IntermediateAdminView):
+    """
+    View: Confirmation page to delete confidential AK-related messages to orga
+
+    Confirmation functionality provided by :class:`AKModel.metaviews.admin.IntermediateAdminView`
+    """
     template_name = "admin/AKModel/message_delete.html"
     title = _("Delete AK Orga Messages")
 
     def get_orga_messages_for_event(self, event):
+        """
+        Get all orga messages for the given event
+        """
         return AKOrgaMessage.objects.filter(ak__event=event)
 
     def get_success_url(self):
@@ -77,6 +91,11 @@ class AKMessageDeleteView(EventSlugMixin, IntermediateAdminView):
 
 
 class AKResetInterestView(IntermediateAdminActionView):
+    """
+    View: Confirmation page to reset all manually specified interest values
+
+    Confirmation functionality provided by :class:`AKModel.metaviews.admin.IntermediateAdminView`
+    """
     title = _("Reset interest in AKs")
     model = AK
     confirmation_message = _("Interest of the following AKs will be set to not filled (-1):")
@@ -87,6 +106,11 @@ class AKResetInterestView(IntermediateAdminActionView):
 
 
 class AKResetInterestCounterView(IntermediateAdminActionView):
+    """
+    View: Confirmation page to reset all interest counters (online interest indication)
+
+    Confirmation functionality provided by :class:`AKModel.metaviews.admin.IntermediateAdminView`
+    """
     title = _("Reset AKs' interest counters")
     model = AK
     confirmation_message = _("Interest counter of the following AKs will be set to 0:")
diff --git a/AKModel/views/api.py b/AKModel/views/api.py
index abf4c261..06ef5abf 100644
--- a/AKModel/views/api.py
+++ b/AKModel/views/api.py
@@ -7,6 +7,10 @@ from AKModel.serializers import AKOwnerSerializer, AKCategorySerializer, AKTrack
 
 
 class AKOwnerViewSet(EventSlugMixin, mixins.RetrieveModelMixin, mixins.ListModelMixin, viewsets.GenericViewSet):
+    """
+    API View: Owners (restricted to those of the given event)
+    Read-only
+    """
     permission_classes = (permissions.DjangoModelPermissionsOrAnonReadOnly,)
     serializer_class = AKOwnerSerializer
 
@@ -15,6 +19,10 @@ class AKOwnerViewSet(EventSlugMixin, mixins.RetrieveModelMixin, mixins.ListModel
 
 
 class AKCategoryViewSet(EventSlugMixin, mixins.RetrieveModelMixin, mixins.ListModelMixin, viewsets.GenericViewSet):
+    """
+    API View: Categories (restricted to those of the given event)
+    Read-only
+    """
     permission_classes = (permissions.DjangoModelPermissionsOrAnonReadOnly,)
     serializer_class = AKCategorySerializer
 
@@ -24,6 +32,10 @@ class AKCategoryViewSet(EventSlugMixin, mixins.RetrieveModelMixin, mixins.ListMo
 
 class AKTrackViewSet(EventSlugMixin, mixins.RetrieveModelMixin, mixins.CreateModelMixin, mixins.UpdateModelMixin,
                      mixins.DestroyModelMixin, mixins.ListModelMixin, viewsets.GenericViewSet):
+    """
+    API View: Tracks (restricted to those of the given event)
+    Read, Write, Delete
+    """
     permission_classes = (permissions.DjangoModelPermissionsOrAnonReadOnly,)
     serializer_class = AKTrackSerializer
 
@@ -33,6 +45,10 @@ class AKTrackViewSet(EventSlugMixin, mixins.RetrieveModelMixin, mixins.CreateMod
 
 class AKViewSet(EventSlugMixin, mixins.RetrieveModelMixin, mixins.UpdateModelMixin, mixins.ListModelMixin,
                 viewsets.GenericViewSet):
+    """
+    API View: AKs (restricted to those of the given event)
+    Read, Write
+    """
     permission_classes = (permissions.DjangoModelPermissionsOrAnonReadOnly,)
     serializer_class = AKSerializer
 
@@ -41,6 +57,10 @@ class AKViewSet(EventSlugMixin, mixins.RetrieveModelMixin, mixins.UpdateModelMix
 
 
 class RoomViewSet(EventSlugMixin, mixins.RetrieveModelMixin, mixins.ListModelMixin, viewsets.GenericViewSet):
+    """
+    API View: Rooms (restricted to those of the given event)
+    Read-only
+    """
     permission_classes = (permissions.DjangoModelPermissionsOrAnonReadOnly,)
     serializer_class = RoomSerializer
 
@@ -50,6 +70,10 @@ class RoomViewSet(EventSlugMixin, mixins.RetrieveModelMixin, mixins.ListModelMix
 
 class AKSlotViewSet(EventSlugMixin, mixins.RetrieveModelMixin, mixins.CreateModelMixin, mixins.UpdateModelMixin,
                     mixins.ListModelMixin, viewsets.GenericViewSet):
+    """
+    API View: AK slots (restricted to those of the given event)
+    Read, Write
+    """
     permission_classes = (permissions.DjangoModelPermissionsOrAnonReadOnly,)
     serializer_class = AKSlotSerializer
 
diff --git a/AKModel/views/event_wizard.py b/AKModel/views/event_wizard.py
index 2aca3660..76a401a1 100644
--- a/AKModel/views/event_wizard.py
+++ b/AKModel/views/event_wizard.py
@@ -12,6 +12,12 @@ from AKModel.models import Event
 
 
 class NewEventWizardStartView(AdminViewMixin, WizardViewMixin, CreateView):
+    """
+    Wizard view: Entry/Start
+
+    Specify basic settings, especially the timezone for correct time treatment in the next view
+    (:class:`NewEventWizardSettingsView`) where this view will redirect to without saving the new event already
+    """
     model = Event
     form_class = NewEventWizardStartForm
     template_name = "admin/AKModel/event_wizard/start.html"
@@ -19,6 +25,16 @@ class NewEventWizardStartView(AdminViewMixin, WizardViewMixin, CreateView):
 
 
 class NewEventWizardSettingsView(AdminViewMixin, WizardViewMixin, CreateView):
+    """
+    Wizard view: Event settings
+
+    Specify most of the event settings. The user will see that certain fields are required since they were lead here
+    from another form in :class:`NewEventWizardStartView` that did not contain these fields even though they are
+    mandatory for the event model
+
+    Next step will then be :class:`NewEventWizardPrepareImportView` to prepare copy configuration elements
+    from an existing event
+    """
     model = Event
     form_class = NewEventWizardSettingsForm
     template_name = "admin/AKModel/event_wizard/settings.html"
@@ -34,6 +50,14 @@ class NewEventWizardSettingsView(AdminViewMixin, WizardViewMixin, CreateView):
 
 
 class NewEventWizardPrepareImportView(WizardViewMixin, EventSlugMixin, FormView):
+    """
+    Wizard view: Choose event to copy configuration elements from
+
+    The user can here select an existing event to copy elements like requirements, categories and dashboard buttons from
+    The exact subset of elements to copy from can then be selected in the next view (:class:`NewEventWizardImportView`)
+
+    Instead, this step can be skipped by directly continuing with :class:`NewEventWizardActivateView`
+    """
     form_class = NewEventWizardPrepareImportForm
     template_name = "admin/AKModel/event_wizard/created_prepare_import.html"
     wizard_step = 3
@@ -45,29 +69,40 @@ class NewEventWizardPrepareImportView(WizardViewMixin, EventSlugMixin, FormView)
 
 
 class NewEventWizardImportView(EventSlugMixin, WizardViewMixin, FormView):
+    """
+    Wizard view: Select configuration elements to copy
+
+    Displays lists of requirements, categories and dashboard buttons that the user can select entries to be copied from
+
+    Afterwards, the event can be activated in :class:`NewEventWizardActivateView`
+    """
     form_class = NewEventWizardImportForm
     template_name = "admin/AKModel/event_wizard/import.html"
     wizard_step = 4
 
     def get_initial(self):
         initial = super().get_initial()
+        # Remember which event was selected and send it again when submitting the form for validation
         initial["import_event"] = Event.objects.get(slug=self.kwargs["import_slug"])
         return initial
 
     def form_valid(self, form):
+        # pylint: disable=consider-using-f-string
         import_types = ["import_categories", "import_requirements"]
         if apps.is_installed("AKDashboard"):
             import_types.append("import_buttons")
 
+        # Loop over all kinds of configuration elements and then over all selected elements of each type
+        # and try to clone them by requesting a new primary key, adapting the event and then storing the
+        # object in the database
         for import_type in import_types:
             for import_obj in form.cleaned_data.get(import_type):
-                # clone existing entry
                 try:
                     import_obj.event = self.event
                     import_obj.pk = None
                     import_obj.save()
                     messages.add_message(self.request, messages.SUCCESS, _("Copied '%(obj)s'" % {'obj': import_obj}))
-                except BaseException as e:
+                except BaseException as e:  # pylint: disable=broad-exception-caught
                     messages.add_message(self.request, messages.ERROR,
                                          _("Could not copy '%(obj)s' (%(error)s)" % {'obj': import_obj,
                                                                                      "error": str(e)}))
@@ -75,6 +110,17 @@ class NewEventWizardImportView(EventSlugMixin, WizardViewMixin, FormView):
 
 
 class NewEventWizardActivateView(WizardViewMixin, UpdateView):
+    """
+    Wizard view: Allow activating the event
+
+    The user is asked to make the created event active. This is done in this step and not already during the creation
+    in the second step of the wizard to prevent users seeing an unconfigured submission.
+    The event will nevertheless already be visible in the dashboard before, when a public event was created in
+    :class:`NewEventWizardSettingsView`.
+
+    In the following last step (:class:`NewEventWizardFinishView`), a confirmation of the full process and some
+    details of the created event are shown
+    """
     model = Event
     template_name = "admin/AKModel/event_wizard/activate.html"
     form_class = NewEventWizardActivateForm
@@ -85,6 +131,11 @@ class NewEventWizardActivateView(WizardViewMixin, UpdateView):
 
 
 class NewEventWizardFinishView(WizardViewMixin, DetailView):
+    """
+    Wizard view: Confirmation and summary
+
+    Show a confirmation and a summary of the created event
+    """
     model = Event
     template_name = "admin/AKModel/event_wizard/finish.html"
     wizard_step = 6
diff --git a/AKModel/views/manage.py b/AKModel/views/manage.py
index 369395d5..6f01c10d 100644
--- a/AKModel/views/manage.py
+++ b/AKModel/views/manage.py
@@ -18,16 +18,28 @@ from AKModel.models import ConstraintViolation, Event, DefaultSlot
 
 
 class UserView(TemplateView):
+    """
+    View: Start page for logged in user
+
+    Will over a link to backend or inform the user that their account still needs to be confirmed
+    """
     template_name = "AKModel/user.html"
 
 
 class ExportSlidesView(EventSlugMixin, IntermediateAdminView):
+    """
+    View: Export slides to present AKs
+
+    Over a form to choose some settings for the export and then generate the PDF
+    """
     title = _('Export AK Slides')
     form_class = SlideExportForm
 
     def form_valid(self, form):
+        # pylint: disable=invalid-name
         template_name = 'admin/AKModel/export/slides.tex'
 
+        # Settings
         NEXT_AK_LIST_LENGTH = form.cleaned_data['num_next']
         RESULT_PRESENTATION_MODE = form.cleaned_data["presentation_mode"]
         SPACE_FOR_NOTES_IN_WISHES = form.cleaned_data["wish_notes"]
@@ -42,12 +54,18 @@ class ExportSlidesView(EventSlugMixin, IntermediateAdminView):
         }
 
         def build_ak_list_with_next_aks(ak_list):
+            """
+            Create a list of tuples cosisting of an AK and a list of upcoming AKs (list length depending on setting)
+            """
             next_aks_list = zip_longest(*[ak_list[i + 1:] for i in range(NEXT_AK_LIST_LENGTH)], fillvalue=None)
-            return [(ak, next_aks) for ak, next_aks in zip_longest(ak_list, next_aks_list, fillvalue=list())]
+            return [(ak, next_aks) for ak, next_aks in zip_longest(ak_list, next_aks_list, fillvalue=[])]
 
-        categories_with_aks, ak_wishes = self.event.get_categories_with_aks(wishes_seperately=True, filter=lambda
+        # Get all relevant AKs (wishes separately, and either all AKs or only those who should directly or indirectly
+        # be presented when restriction setting was chosen)
+        categories_with_aks, ak_wishes = self.event.get_categories_with_aks(wishes_seperately=True, filter_func=lambda
             ak: not RESULT_PRESENTATION_MODE or (ak.present or (ak.present is None and ak.category.present_by_default)))
 
+        # Create context for LaTeX rendering
         context = {
             'title': self.event.name,
             'categories_with_aks': [(category, build_ak_list_with_next_aks(ak_list)) for category, ak_list in
@@ -67,11 +85,17 @@ class ExportSlidesView(EventSlugMixin, IntermediateAdminView):
             os.remove(f'{tempdir}/texput.tex')
             pdf = run_tex_in_directory(source, tempdir, template_name=self.template_name)
 
+        # Show PDF file to the user (with a filename containing a timestamp to prevent confusions about the right
+        # version to use when generating multiple versions of the slides, e.g., because owners did last-minute changes
+        # to their AKs
         timestamp = datetime.datetime.now(tz=self.event.timezone).strftime("%Y-%m-%d_%H_%M")
         return PDFResponse(pdf, filename=f'{self.event.slug}_ak_slides_{timestamp}.pdf')
 
 
 class CVMarkResolvedView(IntermediateAdminActionView):
+    """
+    Admin action view: Mark one or multitple constraint violation(s) as resolved
+    """
     title = _('Mark Constraint Violations as manually resolved')
     model = ConstraintViolation
     confirmation_message = _("The following Constraint Violations will be marked as manually resolved")
@@ -82,6 +106,9 @@ class CVMarkResolvedView(IntermediateAdminActionView):
 
 
 class CVSetLevelViolationView(IntermediateAdminActionView):
+    """
+    Admin action view: Set one or multitple constraint violation(s) as to level "violation"
+    """
     title = _('Set Constraint Violations to level "violation"')
     model = ConstraintViolation
     confirmation_message = _("The following Constraint Violations will be set to level 'violation'")
@@ -92,6 +119,9 @@ class CVSetLevelViolationView(IntermediateAdminActionView):
 
 
 class CVSetLevelWarningView(IntermediateAdminActionView):
+    """
+    Admin action view: Set one or multitple constraint violation(s) as to level "warning"
+    """
     title = _('Set Constraint Violations to level "warning"')
     model = ConstraintViolation
     confirmation_message = _("The following Constraint Violations will be set to level 'warning'")
@@ -102,6 +132,9 @@ class CVSetLevelWarningView(IntermediateAdminActionView):
 
 
 class PlanPublishView(IntermediateAdminActionView):
+    """
+    Admin action view: Publish the plan of one or multitple event(s)
+    """
     title = _('Publish plan')
     model = Event
     confirmation_message = _('Publish the plan(s) of:')
@@ -112,6 +145,9 @@ class PlanPublishView(IntermediateAdminActionView):
 
 
 class PlanUnpublishView(IntermediateAdminActionView):
+    """
+    Admin action view: Unpublish the plan of one or multitple event(s)
+    """
     title = _('Unpublish plan')
     model = Event
     confirmation_message = _('Unpublish the plan(s) of:')
@@ -122,6 +158,9 @@ class PlanUnpublishView(IntermediateAdminActionView):
 
 
 class DefaultSlotEditorView(EventSlugMixin, IntermediateAdminView):
+    """
+    Admin view: Allow to edit the default slots of an event
+    """
     template_name = "admin/AKModel/default_slot_editor.html"
     form_class = DefaultSlotEditorForm
     title = _("Edit Default Slots")
@@ -149,13 +188,14 @@ class DefaultSlotEditorView(EventSlugMixin, IntermediateAdminView):
 
         previous_slot_ids = set(s.id for s in self.event.defaultslot_set.all())
 
+        # Loop over inputs and update or add slots
         for slot in default_slots_raw:
             start = parse_datetime(slot["start"]).replace(tzinfo=tz)
             end = parse_datetime(slot["end"]).replace(tzinfo=tz)
 
             if slot["id"] != '':
-                id = int(slot["id"])
-                if id not in previous_slot_ids:
+                slot_id = int(slot["id"])
+                if slot_id not in previous_slot_ids:
                     # Make sure only slots (currently) belonging to this event are edited
                     # (user did not manipulate IDs and slots have not been deleted in another session in the meantime)
                     messages.add_message(
@@ -166,8 +206,8 @@ class DefaultSlotEditorView(EventSlugMixin, IntermediateAdminView):
                     )
                 else:
                     # Update existing entries
-                    previous_slot_ids.remove(id)
-                    original_slot = DefaultSlot.objects.get(id=id)
+                    previous_slot_ids.remove(slot_id)
+                    original_slot = DefaultSlot.objects.get(id=slot_id)
                     if original_slot.start != start or original_slot.end != end:
                         original_slot.start = start
                         original_slot.end = end
@@ -187,6 +227,7 @@ class DefaultSlotEditorView(EventSlugMixin, IntermediateAdminView):
         for d_id in previous_slot_ids:
             DefaultSlot.objects.get(id=d_id).delete()
 
+        # Inform user about changes performed
         if created_count + updated_count + deleted_count > 0:
             messages.add_message(
                 self.request,
diff --git a/AKModel/views/room.py b/AKModel/views/room.py
index adead3ba..138a04a0 100644
--- a/AKModel/views/room.py
+++ b/AKModel/views/room.py
@@ -15,6 +15,9 @@ from AKModel.models import Room
 
 
 class RoomCreationView(AdminViewMixin, CreateView):
+    """
+    Admin view: Create a room
+    """
     form_class = RoomForm
     template_name = 'admin/AKModel/room_create.html'
 
@@ -22,18 +25,28 @@ class RoomCreationView(AdminViewMixin, CreateView):
         print(self.request.POST['save_action'])
         if self.request.POST['save_action'] == 'save_add_another':
             return reverse_lazy('admin:room-new')
-        elif self.request.POST['save_action'] == 'save_continue':
+        if self.request.POST['save_action'] == 'save_continue':
             return reverse_lazy('admin:AKModel_room_change', kwargs={'object_id': self.room.pk})
-        else:
-            return reverse_lazy('admin:AKModel_room_changelist')
+        return reverse_lazy('admin:AKModel_room_changelist')
 
     def form_valid(self, form):
-        self.room = form.save()
+        self.room = form.save()  # pylint: disable=attribute-defined-outside-init
+
+        # translatable string with placeholders, no f-string possible
+        # pylint: disable=consider-using-f-string
         messages.success(self.request, _("Created Room '%(room)s'" % {'room': self.room}))
+
         return HttpResponseRedirect(self.get_success_url())
 
 
 class RoomBatchCreationView(EventSlugMixin, IntermediateAdminView):
+    """
+    Admin action: Allow to create rooms in batch by inputing a CSV-formatted list of room details into a textbox
+
+    This offers the input form, supports creation of virtual rooms if AKOnline is active, too,
+    and users can specify that default availabilities (from event start to end) should be created for the rooms
+    automatically
+    """
     form_class = RoomBatchCreationForm
     title = _("Import Rooms from CSV")
 
@@ -47,23 +60,33 @@ class RoomBatchCreationView(EventSlugMixin, IntermediateAdminView):
 
         rooms_raw_dict: csv.DictReader = form.cleaned_data["rooms"]
 
+        # Prepare creation of virtual rooms if there is information (an URL) in the data and the AKOnline app is active
         if apps.is_installed("AKOnline") and "url" in rooms_raw_dict.fieldnames:
             virtual_rooms_support = True
+            # pylint: disable=import-outside-toplevel
             from AKOnline.models import VirtualRoom
 
+        # Loop over all inputs
         for raw_room in rooms_raw_dict:
+            # Gather the relevant information (most fields can be empty)
             name = raw_room["name"]
             location = raw_room["location"] if "location" in rooms_raw_dict.fieldnames else ""
             capacity = raw_room["capacity"] if "capacity" in rooms_raw_dict.fieldnames else -1
 
             try:
+                # Try to create a room (catches cases where the room name contains keywords or symbols that the
+                # database cannot handle (.e.g., special UTF-8 characters)
                 r = Room.objects.create(name=name,
                                     location=location,
                                     capacity=capacity,
                                     event=self.event)
+
+                # and if necessary an associated virtual room, too
                 if virtual_rooms_support and raw_room["url"] != "":
                     VirtualRoom.objects.create(room=r,
                                                url=raw_room["url"])
+
+                # If user requested default availabilities, create them
                 if create_default_availabilities:
                     a = Availability.with_event_length(event=self.event, room=r)
                     a.save()
@@ -72,6 +95,7 @@ class RoomBatchCreationView(EventSlugMixin, IntermediateAdminView):
                 messages.add_message(self.request, messages.WARNING,
                                      _("Could not import room {name}: {e}").format(name=name, e=str(e)))
 
+        # Inform the user about the rooms created
         if created_count > 0:
             messages.add_message(self.request, messages.SUCCESS,
                                  _("Imported {count} room(s)").format(count=created_count))
diff --git a/AKModel/views/status.py b/AKModel/views/status.py
index 460c13af..11173d97 100644
--- a/AKModel/views/status.py
+++ b/AKModel/views/status.py
@@ -4,12 +4,15 @@ from django.utils.html import format_html
 from django.utils.translation import gettext_lazy as _
 
 from AKModel.metaviews import status_manager
-from AKModel.metaviews.admin import EventSlugMixin, AdminViewMixin
+from AKModel.metaviews.admin import EventSlugMixin
 from AKModel.metaviews.status import TemplateStatusWidget, StatusView
 
 
 @status_manager.register(name="event_overview")
 class EventOverviewWidget(TemplateStatusWidget):
+    """
+    Status page widget: Event overview
+    """
     required_context_type = "event"
     title = _("Overview")
     template_name = "admin/AKModel/status/event_overview.html"
@@ -20,6 +23,12 @@ class EventOverviewWidget(TemplateStatusWidget):
 
 @status_manager.register(name="event_categories")
 class EventCategoriesWidget(TemplateStatusWidget):
+    """
+    Status page widget: Category information
+
+    Show all categories of the event together with the number of AKs belonging to this category.
+    Offers an action to add a new category.
+    """
     required_context_type = "event"
     title = _("Categories")
     template_name = "admin/AKModel/status/event_categories.html"
@@ -31,7 +40,8 @@ class EventCategoriesWidget(TemplateStatusWidget):
     ]
 
     def render_title(self, context: {}) -> str:
-        self.category_count = context['event'].akcategory_set.count()
+        # Store category count as instance variable for re-use in body
+        self.category_count = context['event'].akcategory_set.count()  # pylint: disable=attribute-defined-outside-init
         return f"{super().render_title(context)} ({self.category_count})"
 
     def render_status(self, context: {}) -> str:
@@ -40,6 +50,12 @@ class EventCategoriesWidget(TemplateStatusWidget):
 
 @status_manager.register(name="event_rooms")
 class EventRoomsWidget(TemplateStatusWidget):
+    """
+    Status page widget: Category information
+
+    Show all rooms of the event.
+    Offers actions to add a single new room as well as for batch creation.
+    """
     required_context_type = "event"
     title = _("Rooms")
     template_name = "admin/AKModel/status/event_rooms.html"
@@ -51,7 +67,8 @@ class EventRoomsWidget(TemplateStatusWidget):
     ]
 
     def render_title(self, context: {}) -> str:
-        self.room_count = context['event'].room_set.count()
+        # Store room count as instance variable for re-use in body
+        self.room_count = context['event'].room_set.count()  # pylint: disable=attribute-defined-outside-init
         return f"{super().render_title(context)} ({self.room_count})"
 
     def render_status(self, context: {}) -> str:
@@ -59,6 +76,7 @@ class EventRoomsWidget(TemplateStatusWidget):
 
     def render_actions(self, context: {}) -> list[dict]:
         actions = super().render_actions(context)
+        # Action has to be added here since it depends on the event for URL building
         actions.append(
             {
                 "text": _("Import Rooms from CSV"),
@@ -70,6 +88,12 @@ class EventRoomsWidget(TemplateStatusWidget):
 
 @status_manager.register(name="event_aks")
 class EventAKsWidget(TemplateStatusWidget):
+    """
+    Status page widget: AK information
+
+    Show information about the AKs of this event.
+    Offers a long list of AK-related actions and also scheduling actions of AKScheduling is active
+    """
     required_context_type = "event"
     title = _("AKs")
     template_name = "admin/AKModel/status/event_aks.html"
@@ -101,7 +125,9 @@ class EventAKsWidget(TemplateStatusWidget):
                 {
                     "text": _("Enter Interest"),
                     "url": reverse_lazy("admin:enter-interest",
-                            kwargs={"event_slug": context["event"].slug, "pk": context["event"].ak_set.all().first().pk}),
+                                        kwargs={"event_slug": context["event"].slug,
+                                                "pk": context["event"].ak_set.all().first().pk}
+                                        ),
                 },
             ])
         actions.extend([
@@ -132,11 +158,19 @@ class EventAKsWidget(TemplateStatusWidget):
 
 @status_manager.register(name="event_requirements")
 class EventRequirementsWidget(TemplateStatusWidget):
+    """
+    Status page widget: Requirement information information
+
+    Show information about the requirements of this event.
+    Offers actions to add new requirements or to get a list of AKs having a given requirement.
+    """
     required_context_type = "event"
     title = _("Requirements")
     template_name = "admin/AKModel/status/event_requirements.html"
 
     def render_title(self, context: {}) -> str:
+        # Store requirements count as instance variable for re-use in body
+        # pylint: disable=attribute-defined-outside-init
         self.requirements_count = context['event'].akrequirement_set.count()
         return f"{super().render_title(context)} ({self.requirements_count})"
 
@@ -154,6 +188,9 @@ class EventRequirementsWidget(TemplateStatusWidget):
 
 
 class EventStatusView(EventSlugMixin, StatusView):
+    """
+    View: Show a status dashboard for the given event
+    """
     title = _("Event Status")
     provided_context_type = "event"
 
-- 
GitLab