diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index 741d4f59f623b4acc32c6be23e1ddb4710737c8a..53e961a22e2e4f6bfe923d659b269f11bae418bf 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -18,16 +18,16 @@ before_script:
   - python -V  # Print out python version for debugging
   - apt-get -qq update
   - apt-get -qq install -y python3-virtualenv python3 python3-dev python3-pip gettext default-mysql-client default-libmysqlclient-dev
-  - export DJANGO_SETTINGS_MODULE=AKPlanning.settings_ci
-  - ./Utils/setup.sh --prod
+  - ./Utils/setup.sh --ci
+  - mkdir -p public/badges public/lint
+  - echo undefined > public/badges/$CI_JOB_NAME.score
+  - source venv/bin/activate
+  - pip install pylint-gitlab pylint-django
   - mysql --version
 
 check:
   script:
     - ./Utils/check.sh --all
-
-check-migrations:
-  script:
     - source venv/bin/activate
     - ./manage.py makemigrations --dry-run --check
 
@@ -48,3 +48,42 @@ test:
         coverage_format: cobertura
         path: coverage.xml
       junit: unit.xml
+
+lint:
+  stage: test
+  script:
+    - pylint --load-plugins pylint_django --django-settings-module=AKPlanning.settings_ci --rcfile pylintrc --exit-zero --output-format=text AK* | tee /tmp/pylint.txt
+    - sed -n 's/^Your code has been rated at \([-0-9.]*\)\/.*/\1/p' /tmp/pylint.txt > public/badges/$CI_JOB_NAME.score
+    - pylint --load-plugins pylint_django --django-settings-module=AKPlanning.settings_ci --rcfile pylintrc --exit-zero --output-format=pylint_gitlab.GitlabCodeClimateReporter AK* > codeclimate.json
+    - pylint --load-plugins pylint_django --django-settings-module=AKPlanning.settings_ci --rcfile pylintrc --exit-zero --output-format=pylint_gitlab.GitlabPagesHtmlReporter AK* > public/lint/index.html
+  after_script:
+   - |
+      echo "Linting score: $(cat public/badges/$CI_JOB_NAME.score)"
+  artifacts:
+    paths:
+      - public
+    reports:
+      codequality: codeclimate.json
+    when: always
+
+doc:
+  stage: test
+  script:
+    - cd docs
+    - make html
+    - cd ..
+  artifacts:
+    paths:
+      - docs/_build/html
+
+pages:
+  stage: deploy
+  image: alpine:latest
+  script:
+    - echo
+  artifacts:
+    paths:
+      - public
+  only:
+    refs:
+      - main
diff --git a/AKDashboard/admin.py b/AKDashboard/admin.py
index 724ac31cb275d61c92e01cb3b4c0d65e3af288ea..ffc7b41c68ac7a013606f4388dc867fbd47ee4f9 100644
--- a/AKDashboard/admin.py
+++ b/AKDashboard/admin.py
@@ -4,6 +4,9 @@ from AKDashboard.models import DashboardButton
 
 @admin.register(DashboardButton)
 class DashboardButtonAdmin(admin.ModelAdmin):
+    """
+    Admin interface for dashboard buttons
+    """
     list_display = ['text', 'url', 'event']
     list_filter = ['event']
     search_fields = ['text', 'url']
diff --git a/AKDashboard/apps.py b/AKDashboard/apps.py
index ad0d431686f975c681fa583b0fcd1438653c2af0..c71dc24053822f11dd86ad7a1b4a608a8c6a417a 100644
--- a/AKDashboard/apps.py
+++ b/AKDashboard/apps.py
@@ -2,4 +2,7 @@ from django.apps import AppConfig
 
 
 class AkdashboardConfig(AppConfig):
+    """
+    App configuration for dashboard (default)
+    """
     name = 'AKDashboard'
diff --git a/AKDashboard/locale/de_DE/LC_MESSAGES/django.po b/AKDashboard/locale/de_DE/LC_MESSAGES/django.po
index 40921dd1daec106fc37a5e8a1e12bde0fae98607..b630ca873d6607cd947ef5ba8ef67cbec7f3844a 100644
--- a/AKDashboard/locale/de_DE/LC_MESSAGES/django.po
+++ b/AKDashboard/locale/de_DE/LC_MESSAGES/django.po
@@ -8,7 +8,7 @@ msgid ""
 msgstr ""
 "Project-Id-Version: PACKAGE VERSION\n"
 "Report-Msgid-Bugs-To: \n"
-"POT-Creation-Date: 2023-05-15 20:03+0200\n"
+"POT-Creation-Date: 2023-08-16 16:30+0200\n"
 "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
 "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
 "Language-Team: LANGUAGE <LL@li.org>\n"
@@ -17,47 +17,47 @@ msgstr ""
 "Content-Type: text/plain; charset=UTF-8\n"
 "Content-Transfer-Encoding: 8bit\n"
 
-#: AKDashboard/models.py:10
+#: AKDashboard/models.py:21
 msgid "Dashboard Button"
 msgstr "Dashboard-Button"
 
-#: AKDashboard/models.py:11
+#: AKDashboard/models.py:22
 msgid "Dashboard Buttons"
 msgstr "Dashboard-Buttons"
 
-#: AKDashboard/models.py:21
+#: AKDashboard/models.py:32
 msgid "Text"
 msgstr "Text"
 
-#: AKDashboard/models.py:22
+#: AKDashboard/models.py:33
 msgid "Text that will be shown on the button"
 msgstr "Text, der auf dem Button angezeigt wird"
 
-#: AKDashboard/models.py:23
+#: AKDashboard/models.py:34
 msgid "Link URL"
 msgstr "Link-URL"
 
-#: AKDashboard/models.py:23
+#: AKDashboard/models.py:34
 msgid "URL this button links to"
 msgstr "URL auf die der Button verweist"
 
-#: AKDashboard/models.py:24
+#: AKDashboard/models.py:35
 msgid "Icon"
 msgstr "Symbol"
 
-#: AKDashboard/models.py:26
+#: AKDashboard/models.py:37
 msgid "Button Style"
 msgstr "Stil des Buttons"
 
-#: AKDashboard/models.py:26
+#: AKDashboard/models.py:37
 msgid "Style (Color) of this button (bootstrap class)"
 msgstr "Stiel (Farbe) des Buttons (Bootstrap-Klasse)"
 
-#: AKDashboard/models.py:28
+#: AKDashboard/models.py:39
 msgid "Event"
 msgstr "Veranstaltung"
 
-#: AKDashboard/models.py:28
+#: AKDashboard/models.py:39
 msgid "Event this button belongs to"
 msgstr "Veranstaltung, zu der dieser Button gehört"
 
@@ -105,22 +105,22 @@ msgstr "AK-Einreichung"
 msgid "AK History"
 msgstr "AK-Verlauf"
 
-#: AKDashboard/views.py:42
+#: AKDashboard/views.py:59
 #, python-format
 msgid "New AK: %(ak)s."
 msgstr "Neuer AK: %(ak)s."
 
-#: AKDashboard/views.py:45
+#: AKDashboard/views.py:62
 #, python-format
 msgid "AK \"%(ak)s\" edited."
 msgstr "AK \"%(ak)s\" bearbeitet."
 
-#: AKDashboard/views.py:48
+#: AKDashboard/views.py:65
 #, python-format
 msgid "AK \"%(ak)s\" deleted."
 msgstr "AK \"%(ak)s\" gelöscht."
 
-#: AKDashboard/views.py:60
+#: AKDashboard/views.py:80
 #, python-format
 msgid "AK \"%(ak)s\" (re-)scheduled."
 msgstr "AK \"%(ak)s\" (um-)geplant."
diff --git a/AKDashboard/models.py b/AKDashboard/models.py
index 130a8e37889624047d4bda8f945b4bdbf8455506..6691e962ec149aac76667cd8ada71bc16738df68 100644
--- a/AKDashboard/models.py
+++ b/AKDashboard/models.py
@@ -6,6 +6,17 @@ from AKModel.models import Event
 
 
 class DashboardButton(models.Model):
+    """
+    Model for a single dashboard button
+
+    Allows to specify
+    * a text (currently without possibility to translate),
+    * a color (based on predefined design colors)
+    * a url the button should point to (internal or external)
+    * an icon (from the collection of fontawesome)
+
+    Each button is associated with a single event and will be deleted when the event is deleted.
+    """
     class Meta:
         verbose_name = _("Dashboard Button")
         verbose_name_plural = _("Dashboard Buttons")
diff --git a/AKDashboard/tests.py b/AKDashboard/tests.py
index 9610f5a22ad8f68aea735342f1fd18d034ba5c7b..0b03241c95a7461c0f3064acfa1c8083baae0aa8 100644
--- a/AKDashboard/tests.py
+++ b/AKDashboard/tests.py
@@ -10,8 +10,14 @@ from AKModel.tests import BasicViewTests
 
 
 class DashboardTests(TestCase):
+    """
+    Specific Dashboard Tests
+    """
     @classmethod
     def setUpTestData(cls):
+        """
+        Initialize Test database
+        """
         super().setUpTestData()
         cls.event = Event.objects.create(
             name="Dashboard Test Event",
@@ -28,17 +34,30 @@ class DashboardTests(TestCase):
         )
 
     def test_dashboard_view(self):
+        """
+        Check that the main dashboard is reachable
+        (would also be covered by generic view testcase below)
+        """
         url = reverse('dashboard:dashboard_event', kwargs={"slug": self.event.slug})
         response = self.client.get(url)
         self.assertEqual(response.status_code, 200)
 
     def test_nonexistent_dashboard_view(self):
+        """
+        Make sure there is no dashboard for an non-existing event
+        """
         url = reverse('dashboard:dashboard_event', kwargs={"slug": "nonexistent-event"})
         response = self.client.get(url)
         self.assertEqual(response.status_code, 404)
 
     @override_settings(DASHBOARD_SHOW_RECENT=True)
     def test_history(self):
+        """
+        Test displaying of history
+
+        For the sake of that test, the setting to show recent events in dashboard is enforced to be true
+        regardless of the default configuration currently in place
+        """
         url = reverse('dashboard:dashboard_event', kwargs={"slug": self.event.slug})
 
         # History should be empty
@@ -57,6 +76,11 @@ class DashboardTests(TestCase):
         self.assertEqual(response.context["recent_changes"][0]['text'], "New AK: Test AK.")
 
     def test_public(self):
+        """
+        Test handling of public and private events
+        (only public events should be part of the standard dashboard,
+        but there should be an individual dashboard for both public and private events)
+        """
         url_dashboard_index = reverse('dashboard:dashboard')
         url_event_dashboard = reverse('dashboard:dashboard_event', kwargs={"slug": self.event.slug})
 
@@ -79,6 +103,9 @@ class DashboardTests(TestCase):
         self.assertTrue(self.event in response.context["events"])
 
     def test_active(self):
+        """
+        Test existence of buttons with regard to activity status of the given event
+        """
         url_event_dashboard = reverse('dashboard:dashboard_event', kwargs={"slug": self.event.slug})
 
         if apps.is_installed('AKSubmission'):
@@ -95,6 +122,9 @@ class DashboardTests(TestCase):
             self.assertContains(response, "AK Submission")
 
     def test_plan_hidden(self):
+        """
+        Test visibility of plan buttons with regard to plan visibility status for a given event
+        """
         url_event_dashboard = reverse('dashboard:dashboard_event', kwargs={"slug": self.event.slug})
 
         if apps.is_installed('AKPlan'):
@@ -114,6 +144,9 @@ class DashboardTests(TestCase):
             self.assertContains(response, "AK Wall")
 
     def test_dashboard_buttons(self):
+        """
+        Make sure manually added buttons are displayed correctly
+        """
         url_event_dashboard = reverse('dashboard:dashboard_event', kwargs={"slug": self.event.slug})
 
         response = self.client.get(url_event_dashboard)
@@ -129,6 +162,9 @@ class DashboardTests(TestCase):
 
 
 class DashboardViewTests(BasicViewTests, TestCase):
+    """
+    Generic view tests, based on :class:`AKModel.BasicViewTests` as specified in this class in VIEWS
+    """
     fixtures = ['model.json', 'dashboard.json']
 
     APP_NAME = 'dashboard'
diff --git a/AKDashboard/views.py b/AKDashboard/views.py
index 88e3443e3f4c4378f52ff63b002186176d1fe546..601edda274cf0a5d893571d6307d001c49affb63 100644
--- a/AKDashboard/views.py
+++ b/AKDashboard/views.py
@@ -1,5 +1,4 @@
 from django.apps import apps
-from django.urls import reverse_lazy
 from django.utils.decorators import method_decorator
 from django.views.decorators.csrf import ensure_csrf_cookie
 from django.views.generic import TemplateView, DetailView
@@ -10,6 +9,11 @@ from AKPlanning import settings
 
 
 class DashboardView(TemplateView):
+    """
+    Index view of dashboard and therefore the main entry point for AKPlanning
+
+    Displays information and buttons for all public events
+    """
     template_name = 'AKDashboard/dashboard.html'
 
     @method_decorator(ensure_csrf_cookie)
@@ -23,6 +27,14 @@ class DashboardView(TemplateView):
 
 
 class DashboardEventView(DetailView):
+    """
+    Dashboard view for a single event
+
+    In addition to the basic information and the buttons,
+    an overview over recent events (new and changed AKs, moved AKSlots) for the given event is shown.
+
+    The event dashboard also exists for non-public events (one only needs to know the URL/slug of the event).
+    """
     template_name = 'AKDashboard/dashboard_event.html'
     context_object_name = 'event'
     model = Event
@@ -32,11 +44,16 @@ class DashboardEventView(DetailView):
 
         # Show feed of recent changes (if activated)
         if settings.DASHBOARD_SHOW_RECENT:
+            # Create a list of chronically sorted events (both AK and plan changes):
             recent_changes = []
 
-            # Newest AKs
+            # Newest AKs (if AKSubmission is used)
             if apps.is_installed("AKSubmission"):
-                submission_changes = AK.history.filter(event=context['event'])[:int(settings.DASHBOARD_RECENT_MAX)]
+                # Get the latest x changes (if there are that many),
+                # where x corresponds to the entry threshold configured in the settings
+                # (such that the list will be completely filled even if there are no (newer) plan changes)
+                submission_changes = AK.history.filter(event=context['event'])[:int(settings.DASHBOARD_RECENT_MAX)] # pylint: disable=no-member, line-too-long
+                # Create textual representation including icons
                 for s in submission_changes:
                     if s.history_type == '+':
                         text = _('New AK: %(ak)s.') % {'ak': s.name}
@@ -48,18 +65,21 @@ class DashboardEventView(DetailView):
                         text = _('AK "%(ak)s" deleted.') % {'ak': s.name}
                         icon = ('times', 'fas')
 
-                    recent_changes.append({'icon': icon, 'text': text, 'link': s.instance.detail_url, 'timestamp': s.history_date})
-
-            # Changes in plan
-            if apps.is_installed("AKPlan"):
-                if not context['event'].plan_hidden:
-                    last_changed_slots = AKSlot.objects.select_related('ak').filter(event=context['event'], start__isnull=False).order_by('-updated')[
-                                         :int(settings.DASHBOARD_RECENT_MAX)]
-                    for changed_slot in last_changed_slots:
-                        recent_changes.append({'icon': ('clock', 'far'),
-                                               'text': _('AK "%(ak)s" (re-)scheduled.') % {'ak': changed_slot.ak.name},
-                                               'link': changed_slot.ak.detail_url,
-                                               'timestamp': changed_slot.updated})
+                    # Store representation in change list (still unsorted)
+                    recent_changes.append(
+                        {'icon': icon, 'text': text, 'link': s.instance.detail_url, 'timestamp': s.history_date}
+                    )
+
+            # Changes in plan (if AKPlan is used and plan is publicly visible)
+            if apps.is_installed("AKPlan") and not context['event'].plan_hidden:
+                # Get the latest plan changes (again using a threshold, see above)
+                last_changed_slots = AKSlot.objects.select_related('ak').filter(event=context['event'], start__isnull=False).order_by('-updated')[:int(settings.DASHBOARD_RECENT_MAX)] #pylint: disable=line-too-long
+                for changed_slot in last_changed_slots:
+                    # Create textual representation including icons and links and store in list (still unsorted)
+                    recent_changes.append({'icon': ('clock', 'far'),
+                                           'text': _('AK "%(ak)s" (re-)scheduled.') % {'ak': changed_slot.ak.name},
+                                           'link': changed_slot.ak.detail_url,
+                                           'timestamp': changed_slot.updated})
 
             # Sort by change date...
             recent_changes.sort(key=lambda x: x['timestamp'], reverse=True)
diff --git a/AKModel/admin.py b/AKModel/admin.py
index 0d48bc6f269a3a886f78da948f7c4d29df8e63c8..29c14a05bd85bc2d530b41bf4c179880ec4f6fbe 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 5af2af0d5ddb4c45cf30635743e869d3c32a62f1..8b90f381b68e372c89f84837374bda1ab9251fad 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 c6abc863cc06bf327bb44c19635c7dcd91e4c39b..994949a8ef98eadad4eef68b5a94c3abfdf9979b 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 4f90ddc2d3403f1c0052293b98efa26b3ddf5491..7ce794dcda52fcbde462584379e1447ad2b124f2 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 92b3adc61ee6eff6011bbe1a846fcaf89f5aaa51..ae7569db012d8ac1bb0a4f63de37b26eed08d5e6 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 a6536beecbdbb0945b73ad414744d15d8fc87bdb..b476f0d79509edc396ba04d3cc1fec02d5189128 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 606b57539cc406adc67cac537a739e9727d8c29f..f547566817eb55969906c63644119c7294848c80 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 f1f5756bdebe19854d5533ad9ac7b022f5aa1350..ac995f3afa67eb991aaeb41e9d6d8b65cfb378c8 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-16 16:30+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,67 +21,67 @@ 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:138
 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:151
 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:99
 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:114
 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:99
 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:112
 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:125
 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:25 AKModel/availability/models.py:271
 msgid "Availability"
 msgstr "Verfügbarkeit"
 
-#: AKModel/availability/forms.py:23
+#: AKModel/availability/forms.py:27
 msgid ""
 "Click and drag to mark the availability during the event, double-click to "
 "delete. Or use the start and end inputs to add entries to the calendar view."
@@ -90,121 +90,121 @@ msgstr ""
 "Doppelt klicken um Einträge zu löschen. Oder Start- und End-Eingabe "
 "verwenden, um der Kalenderansicht neue Einträge hinzuzufügen."
 
-#: AKModel/availability/forms.py:88
+#: AKModel/availability/forms.py:123
 msgid "The submitted availability does not comply with the required format."
 msgstr "Die eingetragenen Verfügbarkeit haben nicht das notwendige Format."
 
-#: AKModel/availability/forms.py:101
+#: AKModel/availability/forms.py:137
 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:162 AKModel/availability/forms.py:172
 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:43 AKModel/models.py:58 AKModel/models.py:172
+#: AKModel/models.py:249 AKModel/models.py:268 AKModel/models.py:294
+#: AKModel/models.py:348 AKModel/models.py:475 AKModel/models.py:514
+#: AKModel/models.py:596 AKModel/models.py:651 AKModel/models.py:842
 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:44 AKModel/models.py:173
+#: AKModel/models.py:250 AKModel/models.py:269 AKModel/models.py:295
+#: AKModel/models.py:349 AKModel/models.py:476 AKModel/models.py:515
+#: AKModel/models.py:597 AKModel/models.py:652 AKModel/models.py:843
 msgid "Associated event"
 msgstr "Zugehöriges Event"
 
-#: AKModel/availability/models.py:47
+#: AKModel/availability/models.py:52
 msgid "Person"
 msgstr "Person"
 
-#: AKModel/availability/models.py:48
+#: AKModel/availability/models.py:53
 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:61 AKModel/models.py:479
+#: AKModel/models.py:504 AKModel/models.py:661
 msgid "Room"
 msgstr "Raum"
 
-#: AKModel/availability/models.py:57
+#: AKModel/availability/models.py:62
 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:70 AKModel/models.py:357
+#: AKModel/models.py:503 AKModel/models.py:591
 msgid "AK"
 msgstr "AK"
 
-#: AKModel/availability/models.py:66
+#: AKModel/availability/models.py:71
 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:79 AKModel/models.py:253
+#: AKModel/models.py:667
 msgid "AK Category"
 msgstr "AK-Kategorie"
 
-#: AKModel/availability/models.py:75
+#: AKModel/availability/models.py:80
 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:272
 msgid "Availabilities"
 msgstr "Verfügbarkeiten"
 
-#: AKModel/forms.py:43
+#: AKModel/forms.py:69
 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:70
 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:84
 msgid "Copy ak categories"
 msgstr "AK-Kategorien kopieren"
 
-#: AKModel/forms.py:59
+#: AKModel/forms.py:91
 msgid "Copy ak requirements"
 msgstr "AK-Anforderungen kopieren"
 
-#: AKModel/forms.py:80
+#: AKModel/forms.py:115
 msgid "Copy dashboard buttons"
 msgstr "Dashboard-Buttons kopieren"
 
-#: AKModel/forms.py:104
+#: AKModel/forms.py:156
 msgid "# next AKs"
 msgstr "# nächste AKs"
 
-#: AKModel/forms.py:105
+#: AKModel/forms.py:157
 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:160
 msgid "Presentation only?"
 msgstr "Nur Vorstellung?"
 
-#: AKModel/forms.py:110 AKModel/forms.py:117
+#: AKModel/forms.py:162 AKModel/forms.py:169
 msgid "Yes"
 msgstr "Ja"
 
-#: AKModel/forms.py:110 AKModel/forms.py:117
+#: AKModel/forms.py:162 AKModel/forms.py:169
 msgid "No"
 msgstr "Nein"
 
-#: AKModel/forms.py:112
+#: AKModel/forms.py:164
 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:167
 msgid "Space for notes in wishes?"
 msgstr "Platz für Notizen bei den Wünschen?"
 
-#: AKModel/forms.py:119
+#: AKModel/forms.py:171
 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:180 AKModel/models.py:836
 msgid "Default Slots"
 msgstr "Standardslots"
 
-#: AKModel/forms.py:127
+#: AKModel/forms.py:182
 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:198
 msgid "New rooms"
 msgstr "Neue Räume"
 
-#: AKModel/forms.py:138
+#: AKModel/forms.py:199
 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:205
 msgid "Default availabilities?"
 msgstr "Standardverfügbarkeiten?"
 
-#: AKModel/forms.py:145
+#: AKModel/forms.py:206
 msgid "Create default availabilities for all rooms?"
 msgstr "Standardverfügbarkeiten für alle Räume anlegen?"
 
-#: AKModel/forms.py:154
+#: AKModel/forms.py:222
 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:156 AKModel/models.py:29
 msgid "Start"
 msgstr "Start"
 
-#: AKModel/metaviews/admin.py:98
+#: AKModel/metaviews/admin.py:157
 msgid "Settings"
 msgstr "Einstellungen"
 
-#: AKModel/metaviews/admin.py:99
+#: AKModel/metaviews/admin.py:158
 msgid "Event created, Prepare Import"
 msgstr "Event angelegt, Import vorbereiten"
 
-#: AKModel/metaviews/admin.py:100
+#: AKModel/metaviews/admin.py:159
 msgid "Import categories & requirements"
 msgstr "Kategorien & Anforderungen kopieren"
 
-#: AKModel/metaviews/admin.py:101
+#: AKModel/metaviews/admin.py:160
 msgid "Activate?"
 msgstr "Aktivieren?"
 
-#: AKModel/metaviews/admin.py:102
+#: AKModel/metaviews/admin.py:161
 #: 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:241 AKModel/models.py:265
+#: AKModel/models.py:292 AKModel/models.py:310 AKModel/models.py:467
 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:167
 msgid "Nickname"
 msgstr "Spitzname"
 
-#: AKModel/models.py:124
+#: AKModel/models.py:167
 msgid "Name to identify an AK owner by"
 msgstr "Name, durch den eine AK-Leitung identifiziert wird"
 
-#: AKModel/models.py:125
+#: AKModel/models.py:168
 msgid "Slug"
 msgstr "Slug"
 
-#: AKModel/models.py:125
+#: AKModel/models.py:168
 msgid "Slug for URL generation"
 msgstr "Slug für URL-Generierung"
 
-#: AKModel/models.py:126
+#: AKModel/models.py:169
 msgid "Institution"
 msgstr "Instutution"
 
-#: AKModel/models.py:126
+#: AKModel/models.py:169
 msgid "Uni etc."
 msgstr "Universität o.ä."
 
-#: AKModel/models.py:127 AKModel/models.py:249
+#: AKModel/models.py:170 AKModel/models.py:319
 msgid "Web Link"
 msgstr "Internet Link"
 
-#: AKModel/models.py:127
+#: AKModel/models.py:170
 msgid "Link to Homepage"
 msgstr "Link zu Homepage oder Webseite"
 
-#: AKModel/models.py:133 AKModel/models.py:507
+#: AKModel/models.py:176 AKModel/models.py:660
 msgid "AK Owner"
 msgstr "AK-Leitung"
 
-#: AKModel/models.py:134
+#: AKModel/models.py:177
 msgid "AK Owners"
 msgstr "AK-Leitungen"
 
-#: AKModel/models.py:176
+#: AKModel/models.py:241
 msgid "Name of the AK Category"
 msgstr "Name der AK-Kategorie"
 
-#: AKModel/models.py:177 AKModel/models.py:201
+#: AKModel/models.py:242 AKModel/models.py:266
 msgid "Color"
 msgstr "Farbe"
 
-#: AKModel/models.py:177 AKModel/models.py:201
+#: AKModel/models.py:242 AKModel/models.py:266
 msgid "Color for displaying"
 msgstr "Farbe für die Anzeige"
 
-#: AKModel/models.py:178 AKModel/models.py:243
+#: AKModel/models.py:243 AKModel/models.py:313
 msgid "Description"
 msgstr "Beschreibung"
 
-#: AKModel/models.py:179
+#: AKModel/models.py:244
 msgid "Short description of this AK Category"
 msgstr "Beschreibung der AK-Kategorie"
 
-#: AKModel/models.py:180
+#: AKModel/models.py:245
 msgid "Present by default"
 msgstr "Defaultmäßig präsentieren"
 
-#: AKModel/models.py:182
+#: AKModel/models.py:246
 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:254
 msgid "AK Categories"
 msgstr "AK-Kategorien"
 
-#: AKModel/models.py:200
+#: AKModel/models.py:265
 msgid "Name of the AK Track"
 msgstr "Name des AK-Tracks"
 
-#: AKModel/models.py:207
+#: AKModel/models.py:272
 msgid "AK Track"
 msgstr "AK-Track"
 
-#: AKModel/models.py:208
+#: AKModel/models.py:273
 msgid "AK Tracks"
 msgstr "AK-Tracks"
 
-#: AKModel/models.py:222
+#: AKModel/models.py:292
 msgid "Name of the Requirement"
 msgstr "Name der Anforderung"
 
-#: AKModel/models.py:228 AKModel/models.py:511
+#: AKModel/models.py:298 AKModel/models.py:664
 msgid "AK Requirement"
 msgstr "AK-Anforderung"
 
-#: AKModel/models.py:229
+#: AKModel/models.py:299
 msgid "AK Requirements"
 msgstr "AK-Anforderungen"
 
-#: AKModel/models.py:240
+#: AKModel/models.py:310
 msgid "Name of the AK"
 msgstr "Name des AKs"
 
-#: AKModel/models.py:241
+#: AKModel/models.py:311
 msgid "Short Name"
 msgstr "Kurzer Name"
 
-#: AKModel/models.py:242
+#: AKModel/models.py:312
 msgid "Name displayed in the schedule"
 msgstr "Name zur Anzeige im AK-Plan"
 
-#: AKModel/models.py:243
+#: AKModel/models.py:313
 msgid "Description of the AK"
 msgstr "Beschreibung des AKs"
 
-#: AKModel/models.py:245
+#: AKModel/models.py:315
 msgid "Owners"
 msgstr "Leitungen"
 
-#: AKModel/models.py:246
+#: AKModel/models.py:316
 msgid "Those organizing the AK"
 msgstr "Menschen, die den AK organisieren und halten"
 
-#: AKModel/models.py:249
+#: AKModel/models.py:319
 msgid "Link to wiki page"
 msgstr "Link zur Wiki Seite"
 
-#: AKModel/models.py:250
+#: AKModel/models.py:320
 msgid "Protocol Link"
 msgstr "Protokolllink"
 
-#: AKModel/models.py:250
+#: AKModel/models.py:320
 msgid "Link to protocol"
 msgstr "Link zum Protokoll"
 
-#: AKModel/models.py:252
+#: AKModel/models.py:322
 msgid "Category"
 msgstr "Kategorie"
 
-#: AKModel/models.py:253
+#: AKModel/models.py:323
 msgid "Category of the AK"
 msgstr "Kategorie des AKs"
 
-#: AKModel/models.py:254
+#: AKModel/models.py:324
 msgid "Track"
 msgstr "Track"
 
-#: AKModel/models.py:255
+#: AKModel/models.py:325
 msgid "Track the AK belongs to"
 msgstr "Track zu dem der AK gehört"
 
-#: AKModel/models.py:257
+#: AKModel/models.py:327
 msgid "Resolution Intention"
 msgstr "Resolutionsabsicht"
 
-#: AKModel/models.py:258
+#: AKModel/models.py:328
 msgid "Intends to submit a resolution"
 msgstr "Beabsichtigt eine Resolution einzureichen"
 
-#: AKModel/models.py:259
+#: AKModel/models.py:329
 msgid "Present this AK"
 msgstr "AK präsentieren"
 
-#: AKModel/models.py:260
+#: AKModel/models.py:330
 msgid "Present results of this AK"
 msgstr "Die Ergebnisse dieses AKs vorstellen"
 
-#: AKModel/models.py:262 AKModel/views/status.py:136
+#: AKModel/models.py:332 AKModel/views/status.py:168
 msgid "Requirements"
 msgstr "Anforderungen"
 
-#: AKModel/models.py:263
+#: AKModel/models.py:333
 msgid "AK's Requirements"
 msgstr "Anforderungen des AKs"
 
-#: AKModel/models.py:265
+#: AKModel/models.py:335
 msgid "Conflicting AKs"
 msgstr "AK-Konflikte"
 
-#: AKModel/models.py:266
+#: AKModel/models.py:336
 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:337
 msgid "Prerequisite AKs"
 msgstr "Vorausgesetzte AKs"
 
-#: AKModel/models.py:268
+#: AKModel/models.py:338
 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:340
 msgid "Organizational Notes"
 msgstr "Notizen zur Organisation"
 
-#: AKModel/models.py:271
+#: AKModel/models.py:341
 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:344
 msgid "Interest"
 msgstr "Interesse"
 
-#: AKModel/models.py:273
+#: AKModel/models.py:344
 msgid "Expected number of people"
 msgstr "Erwartete Personenzahl"
 
-#: AKModel/models.py:274
+#: AKModel/models.py:345
 msgid "Interest Counter"
 msgstr "Interessenszähler"
 
-#: AKModel/models.py:275
+#: AKModel/models.py:346
 msgid "People who have indicated interest online"
 msgstr "Anzahl Personen, die online Interesse bekundet haben"
 
-#: AKModel/models.py:280
+#: AKModel/models.py:351
 msgid "Export?"
 msgstr "Export?"
 
-#: AKModel/models.py:281
+#: AKModel/models.py:352
 msgid "Include AK in wiki export?"
 msgstr "AK bei Wiki-Export berücksichtigen?"
 
-#: AKModel/models.py:287 AKModel/models.py:502
+#: AKModel/models.py:358 AKModel/models.py:655
 #: AKModel/templates/admin/AKModel/status/event_aks.html:10
-#: AKModel/views/manage.py:55 AKModel/views/status.py:74
+#: AKModel/views/manage.py:73 AKModel/views/status.py:98
 msgid "AKs"
 msgstr "AKs"
 
-#: AKModel/models.py:346
+#: AKModel/models.py:467
 msgid "Name or number of the room"
 msgstr "Name oder Nummer des Raums"
 
-#: AKModel/models.py:347
+#: AKModel/models.py:468
 msgid "Location"
 msgstr "Ort"
 
-#: AKModel/models.py:348
+#: AKModel/models.py:469
 msgid "Name or number of the location"
 msgstr "Name oder Nummer des Ortes"
 
-#: AKModel/models.py:349
+#: AKModel/models.py:470
 msgid "Capacity"
 msgstr "Kapazität"
 
-#: AKModel/models.py:350
+#: AKModel/models.py:471
 msgid "Maximum number of people (-1 for unlimited)."
 msgstr "Maximale Personenzahl (-1 wenn unbeschränkt)."
 
-#: AKModel/models.py:351
+#: AKModel/models.py:472
 msgid "Properties"
 msgstr "Eigenschaften"
 
-#: AKModel/models.py:352
+#: AKModel/models.py:473
 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:480 AKModel/views/status.py:60
 msgid "Rooms"
 msgstr "Räume"
 
-#: AKModel/models.py:376
+#: AKModel/models.py:503
 msgid "AK being mapped"
 msgstr "AK, der zugeordnet wird"
 
-#: AKModel/models.py:378
+#: AKModel/models.py:505
 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:506 AKModel/models.py:839
 msgid "Slot Begin"
 msgstr "Beginn des Slots"
 
-#: AKModel/models.py:379 AKModel/models.py:661
+#: AKModel/models.py:506 AKModel/models.py:839
 msgid "Time and date the slot begins"
 msgstr "Zeit und Datum zu der der AK beginnt"
 
-#: AKModel/models.py:381
+#: AKModel/models.py:508
 msgid "Duration"
 msgstr "Dauer"
 
-#: AKModel/models.py:382
+#: AKModel/models.py:509
 msgid "Length in hours"
 msgstr "Länge in Stunden"
 
-#: AKModel/models.py:384
+#: AKModel/models.py:511
 msgid "Scheduling fixed"
 msgstr "Planung fix"
 
-#: AKModel/models.py:385
+#: AKModel/models.py:512
 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:517
 msgid "Last update"
 msgstr "Letzte Aktualisierung"
 
-#: AKModel/models.py:393
+#: AKModel/models.py:520
 msgid "AK Slot"
 msgstr "AK-Slot"
 
-#: AKModel/models.py:394 AKModel/models.py:504
+#: AKModel/models.py:521 AKModel/models.py:657
 msgid "AK Slots"
 msgstr "AK-Slot"
 
-#: AKModel/models.py:416 AKModel/models.py:425
+#: AKModel/models.py:543 AKModel/models.py:552
 msgid "Not scheduled yet"
 msgstr "Noch nicht geplant"
 
-#: AKModel/models.py:454
+#: AKModel/models.py:592
 msgid "AK this message belongs to"
 msgstr "AK zu dem die Nachricht gehört"
 
-#: AKModel/models.py:455
+#: AKModel/models.py:593
 msgid "Message text"
 msgstr "Nachrichtentext"
 
-#: AKModel/models.py:456
+#: AKModel/models.py:594
 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:600
 msgid "AK Orga Message"
 msgstr "AK-Organachricht"
 
-#: AKModel/models.py:463
+#: AKModel/models.py:601
 msgid "AK Orga Messages"
 msgstr "AK-Organachrichten"
 
-#: AKModel/models.py:472
+#: AKModel/models.py:618
 msgid "Constraint Violation"
 msgstr "Constraintverletzung"
 
-#: AKModel/models.py:473 AKModel/views/status.py:93
+#: AKModel/models.py:619 AKModel/views/status.py:117
 msgid "Constraint Violations"
 msgstr "Constraintverletzungen"
 
-#: AKModel/models.py:477
+#: AKModel/models.py:626
 msgid "Owner has two parallel slots"
 msgstr "Leitung hat zwei Slots parallel"
 
-#: AKModel/models.py:478
+#: AKModel/models.py:627
 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:628
 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:629
 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:630
 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:631
 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:633
 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:634
 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:635
 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:636
 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:637
 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:643
 msgid "Warning"
 msgstr "Warnung"
 
-#: AKModel/models.py:492
+#: AKModel/models.py:644
 msgid "Violation"
 msgstr "Verletzung"
 
-#: AKModel/models.py:494
+#: AKModel/models.py:646
 msgid "Type"
 msgstr "Art"
 
-#: AKModel/models.py:495
+#: AKModel/models.py:647
 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:648
 msgid "Level"
 msgstr "Level"
 
-#: AKModel/models.py:497
+#: AKModel/models.py:649
 msgid "Severity level of the violation"
 msgstr "Schweregrad der Verletzung"
 
-#: AKModel/models.py:503
+#: AKModel/models.py:656
 msgid "AK(s) belonging to this constraint"
 msgstr "AK(s), die zu diesem Constraint gehören"
 
-#: AKModel/models.py:505
+#: AKModel/models.py:658
 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:660
 msgid "AK Owner belonging to this constraint"
 msgstr "AK Leitung(en), die zu diesem Constraint gehören"
 
-#: AKModel/models.py:509
+#: AKModel/models.py:662
 msgid "Room belonging to this constraint"
 msgstr "Raum, der zu diesem Constraint gehört"
 
-#: AKModel/models.py:512
+#: AKModel/models.py:665
 msgid "AK Requirement belonging to this constraint"
 msgstr "AK Anforderung, die zu diesem Constraint gehört"
 
-#: AKModel/models.py:514
+#: AKModel/models.py:667
 msgid "AK Category belonging to this constraint"
 msgstr "AK Kategorie, di zu diesem Constraint gehört"
 
-#: AKModel/models.py:516
+#: AKModel/models.py:669
 msgid "Comment"
 msgstr "Kommentar"
 
-#: AKModel/models.py:516
+#: AKModel/models.py:669
 msgid "Comment or further details for this violation"
 msgstr "Kommentar oder weitere Details zu dieser Vereletzung"
 
-#: AKModel/models.py:519
+#: AKModel/models.py:672
 msgid "Timestamp"
 msgstr "Timestamp"
 
-#: AKModel/models.py:519
+#: AKModel/models.py:672
 msgid "Time of creation"
 msgstr "Zeitpunkt der ERstellung"
 
-#: AKModel/models.py:520
+#: AKModel/models.py:673
 msgid "Manually Resolved"
 msgstr "Manuell behoben"
 
-#: AKModel/models.py:521
+#: AKModel/models.py:674
 msgid "Mark this violation manually as resolved"
 msgstr "Markiere diese Verletzung manuell als behoben"
 
-#: AKModel/models.py:548
+#: AKModel/models.py:701
 #: AKModel/templates/admin/AKModel/requirements_overview.html:27
 msgid "Details"
 msgstr "Details"
 
-#: AKModel/models.py:657
+#: AKModel/models.py:835
 msgid "Default Slot"
 msgstr "Standardslot"
 
-#: AKModel/models.py:662
+#: AKModel/models.py:840
 msgid "Slot End"
 msgstr "Ende des Slots"
 
-#: AKModel/models.py:662
+#: AKModel/models.py:840
 msgid "Time and date the slot ends"
 msgstr "Zeit und Datum zu der der Slot endet"
 
-#: AKModel/models.py:667
+#: AKModel/models.py:845
 msgid "Primary categories"
 msgstr "Primäre Kategorien"
 
-#: AKModel/models.py:668
+#: AKModel/models.py:846
 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:14
 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:184
 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:109
 msgid "Scheduling"
 msgstr "Scheduling"
 
@@ -1101,204 +1101,204 @@ 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:53
 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:89
 msgid "AK Orga Messages successfully deleted"
 msgstr "AK-Organachrichten erfolgreich gelöscht"
 
-#: AKModel/views/ak.py:82
+#: AKModel/views/ak.py:101
 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:102
 msgid "Reset of interest in AKs successful."
 msgstr "Interesse an AKs erfolgreich zurückgesetzt."
 
-#: AKModel/views/ak.py:92
+#: AKModel/views/ak.py:116
 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:117
 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:104
 #, python-format
 msgid "Copied '%(obj)s'"
 msgstr "'%(obj)s' kopiert"
 
-#: AKModel/views/event_wizard.py:72
+#: AKModel/views/event_wizard.py:107
 #, 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:35 AKModel/views/status.py:151
 msgid "Export AK Slides"
 msgstr "AK-Folien exportieren"
 
-#: AKModel/views/manage.py:36
+#: AKModel/views/manage.py:48
 msgid "Symbols"
 msgstr "Symbole"
 
-#: AKModel/views/manage.py:37
+#: AKModel/views/manage.py:49
 msgid "Who?"
 msgstr "Wer?"
 
-#: AKModel/views/manage.py:38
+#: AKModel/views/manage.py:50
 msgid "Duration(s)"
 msgstr "Dauer(n)"
 
-#: AKModel/views/manage.py:39
+#: AKModel/views/manage.py:51
 msgid "Reso intention?"
 msgstr "Resolutionsabsicht?"
 
-#: AKModel/views/manage.py:40
+#: AKModel/views/manage.py:52
 msgid "Category (for Wishes)"
 msgstr "Kategorie (für Wünsche)"
 
-#: AKModel/views/manage.py:77
+#: AKModel/views/manage.py:101
 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:102
 msgid "Constraint Violations marked as resolved"
 msgstr "Constraintverletzungen als manuell behoben markiert"
 
-#: AKModel/views/manage.py:87
+#: AKModel/views/manage.py:114
 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:115
 msgid "Constraint Violations set to level 'violation'"
 msgstr "Constraintverletzungen auf Level \"Violation\" gesetzt"
 
-#: AKModel/views/manage.py:97
+#: AKModel/views/manage.py:127
 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:128
 msgid "Constraint Violations set to level 'warning'"
 msgstr "Constraintverletzungen auf Level \"Warning\" gesetzt"
 
-#: AKModel/views/manage.py:107
+#: AKModel/views/manage.py:140
 msgid "Publish the plan(s) of:"
 msgstr "Den Plan/die Pläne veröffentlichen von:"
 
-#: AKModel/views/manage.py:108
+#: AKModel/views/manage.py:141
 msgid "Plan published"
 msgstr "Plan veröffentlicht"
 
-#: AKModel/views/manage.py:117
+#: AKModel/views/manage.py:153
 msgid "Unpublish the plan(s) of:"
 msgstr "Den Plan/die Pläne verbergen von:"
 
-#: AKModel/views/manage.py:118
+#: AKModel/views/manage.py:154
 msgid "Plan unpublished"
 msgstr "Plan verborgen"
 
-#: AKModel/views/manage.py:127 AKModel/views/status.py:109
+#: AKModel/views/manage.py:166 AKModel/views/status.py:135
 msgid "Edit Default Slots"
 msgstr "Standardslots bearbeiten"
 
-#: AKModel/views/manage.py:164
+#: AKModel/views/manage.py:204
 #, 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:235
 #, 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:37
 #, 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:51 AKModel/views/status.py:82
 msgid "Import Rooms from CSV"
 msgstr "Räume aus CSV importieren"
 
-#: AKModel/views/room.py:73
+#: AKModel/views/room.py:96
 #, 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:101
 #, python-brace-format
 msgid "Imported {count} room(s)"
 msgstr "{count} Raum/Räume importiert"
 
-#: AKModel/views/room.py:79
+#: AKModel/views/room.py:103
 msgid "No rooms imported"
 msgstr "Keine Räume importiert"
 
-#: AKModel/views/status.py:14
+#: AKModel/views/status.py:17
 msgid "Overview"
 msgstr "Ãœberblick"
 
-#: AKModel/views/status.py:24
+#: AKModel/views/status.py:33
 msgid "Categories"
 msgstr "Kategorien"
 
-#: AKModel/views/status.py:28
+#: AKModel/views/status.py:37
 msgid "Add category"
 msgstr "Kategorie hinzufügen"
 
-#: AKModel/views/status.py:48
+#: AKModel/views/status.py:64
 msgid "Add Room"
 msgstr "Raum hinzufügen"
 
-#: AKModel/views/status.py:98
+#: AKModel/views/status.py:122
 msgid "AKs requiring special attention"
 msgstr "AKs, die besondere Aufmerksamkeit benötigen"
 
-#: AKModel/views/status.py:102
+#: AKModel/views/status.py:126
 msgid "Enter Interest"
 msgstr "Interesse erfassen"
 
-#: AKModel/views/status.py:113
+#: AKModel/views/status.py:139
 msgid "Manage ak tracks"
 msgstr "AK-Tracks verwalten"
 
-#: AKModel/views/status.py:117
+#: AKModel/views/status.py:143
 msgid "Export AKs as CSV"
 msgstr "AKs als CSV exportieren"
 
-#: AKModel/views/status.py:121
+#: AKModel/views/status.py:147
 msgid "Export AKs for Wiki"
 msgstr "AKs im Wiki-Format exportieren"
 
-#: AKModel/views/status.py:146
+#: AKModel/views/status.py:180
 msgid "Show AKs for requirements"
 msgstr "Zu Anforderungen gehörige AKs anzeigen"
 
-#: AKModel/views/status.py:157
+#: AKModel/views/status.py:194
 msgid "Event Status"
 msgstr "Eventstatus"
 
diff --git a/AKModel/management/commands/makemessages.py b/AKModel/management/commands/makemessages.py
index d3e9149ef15610ba54f67e1629283e927e831cdd..bd9f89e1c936c2a97885076cf96b71b508744dca 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 f5a3ee6812b530245b2f052325432c6f85b23207..3956af084874c4d8d213804d325d271d33323fd8 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 9c6f90279875e267c67aae46183d6e50e749d9ce..fb0ff60685f7278a698423afad1296ebc3c71cef 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 2e1c3777d18f4b1e09f45e9bfad633eca338fe49..5579a745f3bfdba93440db868e35d497f7dc2ccb 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 3b1a9da9d3c5a6b4af98193e7b36eb73aaceb7a5..926b7e0e563678ea17eacf3021811e16e88367d5 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 8dbbb4eafa1c658f63de40d27986200e8de3552f..5d9ad60331fa78edef0231e50d2408637dbcba17 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 480689ef0d780732e59a560d0959e4ae03469680..bff4985023fb6e7e3b1e8d784dc081f36e5e8be9 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 06560dc7d056573ffed1d1825c560c99d513237e..9ca14812b3dd87d5a2d25ec5c66e50b9040b2919 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 d676992f93f21ef3d12fcdb7ba4556d0a34e6365..fb24dc08a7891425c24d6017be0a51e25566e52d 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,60 +88,83 @@ 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)
+        for view_name_info in self.VIEWS_STAFF_ONLY:
+            expected_response_code = 302 if len(view_name_info) == 2 else view_name_info[2]
+            view_name_with_prefix, url = self._name_and_url(view_name_info)
             response = self.client.get(url)
-            self.assertEqual(response.status_code, 302, msg=f"{view_name_with_prefix} ({url}) accessible by non-staff")
+            self.assertEqual(response.status_code, expected_response_code,
+                             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)
+        for view_name_info in self.VIEWS_STAFF_ONLY:
+            view_name_with_prefix, url = self._name_and_url(view_name_info)
             try:
                 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)
+        for view_name_info in self.VIEWS_STAFF_ONLY:
+            expected_response_code = 302 if len(view_name_info) == 2 else view_name_info[2]
+            view_name_with_prefix, url = self._name_and_url(view_name_info)
             response = self.client.get(url)
-            self.assertEqual(response.status_code, 302,
+            self.assertEqual(response.status_code, expected_response_code,
                              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 +197,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 +227,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 806178145462d507915f0d4c02d57462325d2255..763cc1a22ae73903242abcc2eefe7f2b4ee24209 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 640d398ee98119407035a4106df3f9ec0e1baf27..3afec5ac2de03a62ad3d103d17062927c3b97026 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 abf4c261e745b06e343388b47de68acba3f9da4b..06ef5abfd9c82195e584a0e9f66f0965e8c5a7fb 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 2aca36607af304fe6d21ea6c01eaa55fc081a736..76a401a135c6fa4b07f5091db145748743636799 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 369395d5504f26535bf126e068134262ea3a3a1e..6f01c10da3c4f33eeb8075d0f54c09f0dae54db9 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 adead3bac55062964d6db07c40023e45542c74f0..138a04a0ba9cb02db4afb86ebefda9b2e6e581bd 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 460c13afaf42ea3ab0b2bfd1f7577f2070182625..11173d972caf64c8b7a4e35c531dd9845f2eab3e 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"
 
diff --git a/AKOnline/admin.py b/AKOnline/admin.py
index 69f4bed71466a3d8fa84341a3c6fedf05ccb8428..811368858c4c7e4e8d85caa72048ea504c2fe6c8 100644
--- a/AKOnline/admin.py
+++ b/AKOnline/admin.py
@@ -5,6 +5,9 @@ from AKOnline.models import VirtualRoom
 
 @admin.register(VirtualRoom)
 class VirtualRoomAdmin(admin.ModelAdmin):
+    """
+    Admin interface for virtual room model
+    """
     model = VirtualRoom
     list_display = ['room', 'event', 'url']
     list_filter = ['room__event']
diff --git a/AKOnline/apps.py b/AKOnline/apps.py
index 16078585764016d5c291069a30b9f90022a89e16..6d73f96bf131847b1993a49bb8030ad45da0080b 100644
--- a/AKOnline/apps.py
+++ b/AKOnline/apps.py
@@ -2,4 +2,7 @@ from django.apps import AppConfig
 
 
 class AkonlineConfig(AppConfig):
+    """
+    App configuration (default -- only to set the app name)
+    """
     name = 'AKOnline'
diff --git a/AKOnline/forms.py b/AKOnline/forms.py
index bf33b9c4eb3366fb9ea61fe0f2d1867bfa96277f..379ad20b2f87ec917559e370c23cd2bcf4037884 100644
--- a/AKOnline/forms.py
+++ b/AKOnline/forms.py
@@ -6,23 +6,38 @@ from AKOnline.models import VirtualRoom
 
 
 class VirtualRoomForm(ModelForm):
+    """
+    Form to create a virtual room
+
+    Should be used as part of a multi form (see :class:`RoomWithVirtualForm` below)
+    """
     class Meta:
         model = VirtualRoom
-        exclude = ['room']
+        # Show all fields except for room
+        exclude = ['room'] #pylint: disable=modelform-uses-exclude
 
     def __init__(self, *args, **kwargs):
-        super(VirtualRoomForm, self).__init__(*args, **kwargs)
+        super().__init__(*args, **kwargs)
+        # Make the URL field optional to allow submitting the multi form without creating a virtual room
         self.fields['url'].required = False
 
 
 class RoomWithVirtualForm(MultiModelForm):
+    """
+    Combined form to create rooms and optionally virtual rooms
+
+    Multi-Form that combines a :class:`RoomForm` (from AKModel) and a :class:`VirtualRoomForm` (see above).
+
+    The form will always create a room on valid input
+    and may additionally create a virtual room if the url field of the virtual room form part is set.
+    """
     form_classes = {
         'room': RoomForm,
         'virtual': VirtualRoomForm
     }
 
     def save(self, commit=True):
-        objects = super(RoomWithVirtualForm, self).save(commit=False)
+        objects = super().save(commit=False)
 
         if commit:
             room = objects['room']
diff --git a/AKOnline/locale/de_DE/LC_MESSAGES/django.po b/AKOnline/locale/de_DE/LC_MESSAGES/django.po
index cf4c442eb4ba431a086fe3c0f9ad748c628ed3f7..142c835d30cdf52c33b56dcd47626f6d534815ea 100644
--- a/AKOnline/locale/de_DE/LC_MESSAGES/django.po
+++ b/AKOnline/locale/de_DE/LC_MESSAGES/django.po
@@ -8,7 +8,7 @@ msgid ""
 msgstr ""
 "Project-Id-Version: PACKAGE VERSION\n"
 "Report-Msgid-Bugs-To: \n"
-"POT-Creation-Date: 2023-03-26 19:51+0200\n"
+"POT-Creation-Date: 2023-08-16 16:30+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"
@@ -34,7 +34,7 @@ msgstr "Raum"
 msgid "Virtual Room"
 msgstr "Virtueller Raum"
 
-#: AKOnline/models.py:17 AKOnline/views.py:27
+#: AKOnline/models.py:17 AKOnline/views.py:38
 msgid "Virtual Rooms"
 msgstr "Virtuelle Räume"
 
@@ -42,12 +42,12 @@ msgstr "Virtuelle Räume"
 msgid "Leave empty if that room is not virtual/hybrid."
 msgstr "Leer lassen wenn der Raum nicht virtuell/hybrid ist"
 
-#: AKOnline/views.py:18
+#: AKOnline/views.py:25
 #, python-format
 msgid "Created Room '%(room)s'"
 msgstr "Raum '%(room)s' angelegt"
 
-#: AKOnline/views.py:20
+#: AKOnline/views.py:28
 #, python-format
 msgid "Created related Virtual Room '%(vroom)s'"
 msgstr "Verbundenen virtuellen Raum '%(vroom)s' angelegt"
diff --git a/AKOnline/models.py b/AKOnline/models.py
index dad360cb42df6b602a99d9366b514a0d5ee5d912..6740d3d51326009512f22bec15d71d4a59df5111 100644
--- a/AKOnline/models.py
+++ b/AKOnline/models.py
@@ -1,7 +1,7 @@
 from django.db import models
 from django.utils.translation import gettext_lazy as _
 
-from AKModel.models import Event, Room
+from AKModel.models import Room
 
 
 class VirtualRoom(models.Model):
@@ -18,6 +18,12 @@ class VirtualRoom(models.Model):
 
     @property
     def event(self):
+        """
+        Property: Event this virtual room belongs to.
+
+        :return: Event this virtual room belongs to
+        :rtype: Event
+        """
         return self.room.event
 
     def __str__(self):
diff --git a/AKOnline/tests.py b/AKOnline/tests.py
deleted file mode 100644
index a39b155ac3ee946fb97efafe6ecbb42f571cd7ad..0000000000000000000000000000000000000000
--- a/AKOnline/tests.py
+++ /dev/null
@@ -1 +0,0 @@
-# Create your tests here.
diff --git a/AKOnline/views.py b/AKOnline/views.py
index 13f089a893fd77e49f223eba9c4bd9d58f4242bb..ecd82a22219c82370950e30c1645a1b4b0eb099c 100644
--- a/AKOnline/views.py
+++ b/AKOnline/views.py
@@ -9,20 +9,31 @@ from AKOnline.forms import RoomWithVirtualForm
 
 
 class RoomCreationWithVirtualView(RoomCreationView):
+    """
+    View to create both rooms and optionally virtual rooms by filling one form
+    """
     form_class = RoomWithVirtualForm
     template_name = 'admin/AKOnline/room_create_with_virtual.html'
+    room = None
 
     def form_valid(self, form):
+        # This will create the room and additionally a virtual room if the url field is not blank
+        # objects['room'] will always a room instance afterwards, objects['virtual'] may be empty
         objects = form.save()
         self.room = objects['room']
-        messages.success(self.request, _("Created Room '%(room)s'" % {'room': objects['room']}))
+        # Create a (translated) success message containing information about the created room
+        messages.success(self.request, _("Created Room '%(room)s'" % {'room': objects['room']})) #pylint: disable=consider-using-f-string, line-too-long
         if objects['virtual'] is not None:
-            messages.success(self.request, _("Created related Virtual Room '%(vroom)s'" % {'vroom': objects['virtual']}))
+            # Create a (translated) success message containing information about the created virtual room
+            messages.success(self.request, _("Created related Virtual Room '%(vroom)s'" % {'vroom': objects['virtual']})) #pylint: disable=consider-using-f-string, line-too-long
         return HttpResponseRedirect(self.get_success_url())
 
 
 @status_manager.register(name="event_virtual_rooms")
 class EventVirtualRoomsWidget(TemplateStatusWidget):
+    """
+    Status page widget to contain information about all virtual rooms belonging to the given event
+    """
     required_context_type = "event"
     title = _("Virtual Rooms")
     template_name = "admin/AKOnline/status/event_virtual_rooms.html"
diff --git a/AKPlan/admin.py b/AKPlan/admin.py
deleted file mode 100644
index 846f6b4061a68eda58bc9c76c36603d1e7721ee8..0000000000000000000000000000000000000000
--- a/AKPlan/admin.py
+++ /dev/null
@@ -1 +0,0 @@
-# Register your models here.
diff --git a/AKPlan/apps.py b/AKPlan/apps.py
index f18da123bf48684c2fda835ceb479dadd24bf758..bf67895b111a417a6d3894c859bf406c82ded869 100644
--- a/AKPlan/apps.py
+++ b/AKPlan/apps.py
@@ -2,4 +2,7 @@ from django.apps import AppConfig
 
 
 class AkplanConfig(AppConfig):
+    """
+    App configuration (default, only specifies name of the app)
+    """
     name = 'AKPlan'
diff --git a/AKPlan/models.py b/AKPlan/models.py
deleted file mode 100644
index 6b2021999398416a78191ac543b7e0e34d86bc2c..0000000000000000000000000000000000000000
--- a/AKPlan/models.py
+++ /dev/null
@@ -1 +0,0 @@
-# Create your models here.
diff --git a/AKPlan/templatetags/color_gradients.py b/AKPlan/templatetags/color_gradients.py
index 1a9f96fd87eda8e405894eef648e048d81e87d32..2574efbcb425752fb35d6159ac3bb59a7df9f132 100644
--- a/AKPlan/templatetags/color_gradients.py
+++ b/AKPlan/templatetags/color_gradients.py
@@ -1,7 +1,7 @@
 # gradients based on http://bsou.io/posts/color-gradients-with-python
 
 
-def hex_to_rgb(hex):
+def hex_to_rgb(hex): #pylint: disable=redefined-builtin
     """
     Convert hex color to RGB color code
     :param hex: hex encoded color
@@ -23,8 +23,7 @@ def rgb_to_hex(rgb):
     """
     # Components need to be integers for hex to make sense
     rgb = [int(x) for x in rgb]
-    return "#"+"".join(["0{0:x}".format(v) if v < 16 else
-                        "{0:x}".format(v) for v in rgb])
+    return "#"+"".join([f"0{v:x}" if v < 16 else f"{v:x}" for v in rgb])
 
 
 def linear_blend(start_hex, end_hex, position):
diff --git a/AKPlan/templatetags/tags_AKPlan.py b/AKPlan/templatetags/tags_AKPlan.py
index 8b30bfabd34b501a858351d6b1e384b4fe39e5bb..ae259be4cc042ede588f4b12867dfabfc37b15bf 100644
--- a/AKPlan/templatetags/tags_AKPlan.py
+++ b/AKPlan/templatetags/tags_AKPlan.py
@@ -11,6 +11,14 @@ register = template.Library()
 
 @register.filter
 def highlight_change_colors(akslot):
+    """
+    Adjust color to highlight recent changes if needed
+
+    :param akslot: akslot to determine color for
+    :type akslot: AKSlot
+    :return: color that should be used (either default color of the category or some kind of red)
+    :rtype: str
+    """
     # Do not highlight in preview mode or when changes occurred before the plan was published
     if akslot.event.plan_hidden or (akslot.event.plan_published_at is not None
                                     and akslot.event.plan_published_at > akslot.updated):
@@ -25,9 +33,14 @@ def highlight_change_colors(akslot):
     # Recent change? Calculate gradient blend between red and
     recentness = seconds_since_update / settings.PLAN_MAX_HIGHLIGHT_UPDATE_SECONDS
     return darken("#b71540", recentness)
-    # return linear_blend("#b71540", "#000000", recentness)
 
 
 @register.simple_tag
 def timestamp_now(tz):
+    """
+    Get the current timestamp for the given timezone
+
+    :param tz: timezone to be used for the timestamp
+    :return: current timestamp in given timezone
+    """
     return date_format(datetime.now().astimezone(tz), "c")
diff --git a/AKPlan/tests.py b/AKPlan/tests.py
index e19157c076dcb497cf7585767fd048b89c3355ba..69365c2ba783311708a7babde1fda534c978b61c 100644
--- a/AKPlan/tests.py
+++ b/AKPlan/tests.py
@@ -4,6 +4,9 @@ from AKModel.tests import BasicViewTests
 
 
 class PlanViewTests(BasicViewTests, TestCase):
+    """
+    Tests for AKPlan
+    """
     fixtures = ['model.json']
     APP_NAME = 'plan'
 
@@ -15,7 +18,10 @@ class PlanViewTests(BasicViewTests, TestCase):
     ]
 
     def test_plan_hidden(self):
-        view_name_with_prefix, url = self._name_and_url(('plan_overview', {'event_slug': 'kif23'}))
+        """
+        Test correct handling of plan visibility
+        """
+        _, url = self._name_and_url(('plan_overview', {'event_slug': 'kif23'}))
 
         self.client.logout()
         response = self.client.get(url)
@@ -28,8 +34,11 @@ class PlanViewTests(BasicViewTests, TestCase):
                                msg_prefix="Plan is not visible for staff user")
 
     def test_wall_redirect(self):
-        view_name_with_prefix, url_wall = self._name_and_url(('plan_wall', {'event_slug': 'kif23'}))
-        view_name_with_prefix, url_plan = self._name_and_url(('plan_overview', {'event_slug': 'kif23'}))
+        """
+        Test: Make sure that user is redirected from wall to overview when plan is hidden
+        """
+        _, url_wall = self._name_and_url(('plan_wall', {'event_slug': 'kif23'}))
+        _, url_plan = self._name_and_url(('plan_overview', {'event_slug': 'kif23'}))
 
         response = self.client.get(url_wall)
         self.assertRedirects(response, url_plan,
diff --git a/AKPlan/views.py b/AKPlan/views.py
index 513bca31838a5f33da37b3127a579cdc2a7c500a..87c513a646dba58661a1a64c50d10121c148ba3b 100644
--- a/AKPlan/views.py
+++ b/AKPlan/views.py
@@ -1,5 +1,3 @@
-from datetime import timedelta
-
 from django.conf import settings
 from django.shortcuts import redirect
 from django.urls import reverse_lazy
@@ -11,6 +9,11 @@ from AKModel.metaviews.admin import FilterByEventSlugMixin
 
 
 class PlanIndexView(FilterByEventSlugMixin, ListView):
+    """
+    Default plan view
+
+    Shows two lists of current and upcoming AKs and a graphical full plan below
+    """
     model = AKSlot
     template_name = "AKPlan/plan_index.html"
     context_object_name = "akslots"
@@ -60,6 +63,15 @@ class PlanIndexView(FilterByEventSlugMixin, ListView):
 
 
 class PlanScreenView(PlanIndexView):
+    """
+    Plan view optimized for screens and projectors
+
+    This again shows current and upcoming AKs as well as a graphical plan,
+    but no navigation elements and trys to use the available space as best as possible
+    such that no scrolling is needed.
+
+    The view contains a frontend functionality for auto-reload.
+    """
     template_name = "AKPlan/plan_wall.html"
 
     def get(self, request, *args, **kwargs):
@@ -92,22 +104,34 @@ class PlanScreenView(PlanIndexView):
 
 
 class PlanRoomView(FilterByEventSlugMixin, DetailView):
+    """
+    Plan view for a single room
+    """
     template_name = "AKPlan/plan_room.html"
     model = Room
     context_object_name = "room"
 
     def get_context_data(self, *, object_list=None, **kwargs):
         context = super().get_context_data(object_list=object_list, **kwargs)
+        # Restrict AKSlot list to the given room
+        # while joining AK, room and category information to reduce the amount of necessary SQL queries
         context["slots"] = AKSlot.objects.filter(room=context['room']).select_related('ak', 'ak__category', 'ak__track')
         return context
 
 
 class PlanTrackView(FilterByEventSlugMixin, DetailView):
+    """
+    Plan view for a single track
+    """
     template_name = "AKPlan/plan_track.html"
     model = AKTrack
     context_object_name = "track"
 
     def get_context_data(self, *, object_list=None, **kwargs):
         context = super().get_context_data(object_list=object_list, **kwargs)
-        context["slots"] = AKSlot.objects.filter(event=self.event, ak__track=context['track']).select_related('ak', 'room', 'ak__category')
+        # Restrict AKSlot list to given track
+        # while joining AK, room and category information to reduce the amount of necessary SQL queries
+        context["slots"] = AKSlot.objects.\
+            filter(event=self.event, ak__track=context['track']).\
+            select_related('ak', 'room', 'ak__category')
         return context
diff --git a/AKPlanning/locale/de_DE/LC_MESSAGES/django.po b/AKPlanning/locale/de_DE/LC_MESSAGES/django.po
index 1149811cf7c7b94b2fc5899f8c518463d376d448..3f47223fa6145f6d4c55f87c6f4c1c660f2bd311 100644
--- a/AKPlanning/locale/de_DE/LC_MESSAGES/django.po
+++ b/AKPlanning/locale/de_DE/LC_MESSAGES/django.po
@@ -8,7 +8,7 @@ msgid ""
 msgstr ""
 "Project-Id-Version: PACKAGE VERSION\n"
 "Report-Msgid-Bugs-To: \n"
-"POT-Creation-Date: 2023-05-15 20:03+0200\n"
+"POT-Creation-Date: 2023-08-16 16:30+0200\n"
 "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
 "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
 "Language-Team: LANGUAGE <LL@li.org>\n"
@@ -17,10 +17,10 @@ msgstr ""
 "Content-Type: text/plain; charset=UTF-8\n"
 "Content-Transfer-Encoding: 8bit\n"
 
-#: AKPlanning/settings.py:147
+#: AKPlanning/settings.py:148
 msgid "German"
 msgstr "Deutsch"
 
-#: AKPlanning/settings.py:148
+#: AKPlanning/settings.py:149
 msgid "English"
 msgstr "Englisch"
diff --git a/AKPlanning/settings.py b/AKPlanning/settings.py
index c697b2e41bc98d2621edd8173b7ceb7155fbb438..3dc46aa57266a36bcdef18393ef3fd6571d3727b 100644
--- a/AKPlanning/settings.py
+++ b/AKPlanning/settings.py
@@ -55,6 +55,7 @@ INSTALLED_APPS = [
     'bootstrap_datepicker_plus',
     'django_tex',
     'compressor',
+    'docs',
 ]
 
 MIDDLEWARE = [
@@ -234,4 +235,9 @@ CSP_FONT_SRC = ("'self'", "data:", "fonts.gstatic.com")
 SEND_MAILS = True
 EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend'
 
+# Documentation
+
+DOCS_ROOT = os.path.join(BASE_DIR, 'docs/_build/html')
+DOCS_ACCESS = 'public'
+
 include(optional("settings/*.py"))
diff --git a/AKPlanning/urls.py b/AKPlanning/urls.py
index cdbbea559617339113a51427b7b138417680b582..c97f013422b9f23edb436c1bf44db327924cd48e 100644
--- a/AKPlanning/urls.py
+++ b/AKPlanning/urls.py
@@ -1,4 +1,5 @@
-"""AKPlanning URL Configuration
+"""
+AKPlanning URL Configuration
 
 The `urlpatterns` list routes URLs to views. For more information please see:
     https://docs.djangoproject.com/en/2.2/topics/http/urls/
@@ -13,13 +14,15 @@ Including another URLconf
     1. Import the include() function: from django.urls import include, path
     2. Add a URL to urlpatterns:  path('blog/', include('blog.urls'))
 """
+
 import debug_toolbar
 from django.apps import apps
 from django.contrib import admin
-from django.urls import path, include
+from django.urls import path, include, re_path
 
 urlpatterns = [
     path('admin/', admin.site.urls),
+    re_path(r'^docs/', include('docs.urls')),
     path('accounts/', include('django.contrib.auth.urls')),
     path('accounts/', include('registration.backends.simple.urls')),
     path('', include('AKModel.urls', namespace='model')),
diff --git a/AKScheduling/admin.py b/AKScheduling/admin.py
deleted file mode 100644
index 846f6b4061a68eda58bc9c76c36603d1e7721ee8..0000000000000000000000000000000000000000
--- a/AKScheduling/admin.py
+++ /dev/null
@@ -1 +0,0 @@
-# Register your models here.
diff --git a/AKScheduling/api.py b/AKScheduling/api.py
index 7155e6fe6e47081ccc6101699eba02bab78274ec..61c939ede1526504409a90ff52bcd55f66c76302 100644
--- a/AKScheduling/api.py
+++ b/AKScheduling/api.py
@@ -12,20 +12,32 @@ from AKModel.metaviews.admin import EventSlugMixin
 
 
 class ResourceSerializer(serializers.ModelSerializer):
+    """
+    REST Framework Serializer for Rooms to produce format required for fullcalendar resources
+    """
     class Meta:
         model = Room
         fields = ['id', 'title']
 
     title = serializers.SerializerMethodField('transform_title')
 
-    def transform_title(self, obj):
+    @staticmethod
+    def transform_title(obj):
+        """
+        Adapt title, add capacity information if room has a restriction (capacity is not -1)
+        """
         if obj.capacity > 0:
             return f"{obj.title} [{obj.capacity}]"
         return obj.title
 
 
-class ResourcesViewSet(EventSlugMixin, mixins.RetrieveModelMixin, mixins.ListModelMixin, viewsets.GenericViewSet):
-    permission_classes = (permissions.DjangoModelPermissionsOrAnonReadOnly,)
+class ResourcesViewSet(EventSlugMixin, mixins.ListModelMixin, viewsets.GenericViewSet):
+    """
+    API View: Rooms (resources to schedule for in fullcalendar)
+
+    Read-only, adaption to fullcalendar format through :class:`ResourceSerializer`
+    """
+    permission_classes = (permissions.DjangoModelPermissions,)
     serializer_class = ResourceSerializer
 
     def get_queryset(self):
@@ -33,6 +45,13 @@ class ResourcesViewSet(EventSlugMixin, mixins.RetrieveModelMixin, mixins.ListMod
 
 
 class EventsView(LoginRequiredMixin, EventSlugMixin, ListView):
+    """
+    API View: Slots (events to schedule in fullcalendar)
+
+    Read-only, JSON formatted response is created manually since it requires a bunch of "custom" fields that have
+    different names compared to the normal model or are not present at all and need to be computed to create the
+    required format for fullcalendar.
+    """
     model = AKSlot
 
     def get_queryset(self):
@@ -42,13 +61,16 @@ class EventsView(LoginRequiredMixin, EventSlugMixin, ListView):
         return JsonResponse(
             [{
                 "slotID": slot.pk,
-                "title": f'{slot.ak.short_name}: \n{slot.ak.owners_list}',
+                "title": f'{slot.ak.short_name}:\n{slot.ak.owners_list}',
                 "description": slot.ak.details,
                 "resourceId": slot.room.id,
                 "start": timezone.localtime(slot.start, self.event.timezone).strftime("%Y-%m-%d %H:%M:%S"),
                 "end": timezone.localtime(slot.end, self.event.timezone).strftime("%Y-%m-%d %H:%M:%S"),
                 "backgroundColor": slot.ak.category.color,
-                "borderColor": "#2c3e50" if slot.fixed else '#e74c3c' if slot.constraintviolation_set.count() > 0 else slot.ak.category.color,
+                "borderColor":
+                    "#2c3e50" if slot.fixed
+                    else '#e74c3c' if slot.constraintviolation_set.count() > 0
+                    else slot.ak.category.color,
                 "constraint": 'roomAvailable',
                 "editable": not slot.fixed,
                 'url': str(reverse('admin:AKModel_akslot_change', args=[slot.pk])),
@@ -59,6 +81,13 @@ class EventsView(LoginRequiredMixin, EventSlugMixin, ListView):
 
 
 class RoomAvailabilitiesView(LoginRequiredMixin, EventSlugMixin, ListView):
+    """
+    API view: Availabilities of rooms
+
+    Read-only, JSON formatted response is created manually since it requires a bunch of "custom" fields that have
+    different names compared to the normal model or are not present at all and need to be computed to create the
+    required format for fullcalendar.
+    """
     model = Availability
     context_object_name = "availabilities"
 
@@ -81,6 +110,13 @@ class RoomAvailabilitiesView(LoginRequiredMixin, EventSlugMixin, ListView):
 
 
 class DefaultSlotsView(LoginRequiredMixin, EventSlugMixin, ListView):
+    """
+    API view: default slots
+
+    Read-only, JSON formatted response is created manually since it requires a bunch of "custom" fields that have
+    different names compared to the normal model or are not present at all and need to be computed to create the
+    required format for fullcalendar.
+    """
     model = DefaultSlot
     context_object_name = "default_slots"
 
@@ -105,6 +141,9 @@ class DefaultSlotsView(LoginRequiredMixin, EventSlugMixin, ListView):
 
 
 class EventSerializer(serializers.ModelSerializer):
+    """
+    REST framework serializer to adapt between AKSlot model and the event format of fullcalendar
+    """
     class Meta:
         model = AKSlot
         fields = ['id', 'start', 'end', 'roomId']
@@ -114,17 +153,31 @@ class EventSerializer(serializers.ModelSerializer):
     roomId = serializers.IntegerField(source='room.pk')
 
     def update(self, instance, validated_data):
+        # Ignore timezone of input (treat it as timezone-less) and set the event timezone
+        # By working like this, the client does not need to know about timezones, since every timestamp it deals with
+        # has the timezone offsets already applied
         start = timezone.make_aware(timezone.make_naive(validated_data.get('start')), instance.event.timezone)
         end = timezone.make_aware(timezone.make_naive(validated_data.get('end')), instance.event.timezone)
         instance.start = start
-        instance.room = get_object_or_404(Room, pk=validated_data.get('room')["pk"])
+        # Also, adapt from start & end format of fullcalendar to our start & duration model
         diff = end - start
         instance.duration = round(diff.days * 24 + (diff.seconds / 3600), 2)
+
+        # Updated room if needed (pk changed -- otherwise, no need for an additional database lookup)
+        new_room_id = validated_data.get('room')["pk"]
+        if instance.room.pk != new_room_id:
+            instance.room = get_object_or_404(Room, pk=new_room_id)
+
         instance.save()
         return instance
 
 
-class EventsViewSet(EventSlugMixin, viewsets.ModelViewSet):
+class EventsViewSet(EventSlugMixin, mixins.UpdateModelMixin, viewsets.GenericViewSet):
+    """
+    API view: Update scheduling of a slot (event in fullcalendar format)
+
+    Write-only (will however reply with written values to PUT request)
+    """
     permission_classes = (permissions.DjangoModelPermissions,)
     serializer_class = EventSerializer
 
@@ -136,17 +189,26 @@ class EventsViewSet(EventSlugMixin, viewsets.ModelViewSet):
 
 
 class ConstraintViolationSerializer(serializers.ModelSerializer):
+    """
+    REST Framework Serializer for constraint violations
+    """
     class Meta:
         model = ConstraintViolation
-        fields = ['pk', 'type_display', 'aks', 'ak_slots', 'ak_owner', 'room', 'requirement', 'category', 'comment', 'timestamp_display', 'manually_resolved', 'level_display', 'details', 'edit_url']
+        fields = ['pk', 'type_display', 'aks', 'ak_slots', 'ak_owner', 'room', 'requirement', 'category', 'comment',
+                  'timestamp_display', 'manually_resolved', 'level_display', 'details', 'edit_url']
+
 
+class ConstraintViolationsViewSet(EventSlugMixin, mixins.ListModelMixin, viewsets.GenericViewSet):
+    """
+    API View: Constraint Violations of an event
 
-class ConstraintViolationsViewSet(EventSlugMixin, viewsets.ModelViewSet):
+    Read-only, fields and model selected in :class:`ConstraintViolationSerializer`
+    """
     permission_classes = (permissions.DjangoModelPermissions,)
     serializer_class = ConstraintViolationSerializer
 
-    def get_object(self):
-        return get_object_or_404(ConstraintViolation, pk=self.kwargs["pk"])
-
     def get_queryset(self):
-        return ConstraintViolation.objects.select_related('event', 'room').prefetch_related('aks', 'ak_slots', 'ak_owner', 'requirement', 'category').filter(event=self.event).order_by('manually_resolved', '-type', '-timestamp')
+        # Optimize query to reduce database load
+        return (ConstraintViolation.objects.select_related('event', 'room')
+                .prefetch_related('aks', 'ak_slots', 'ak_owner', 'requirement', 'category')
+                .filter(event=self.event).order_by('manually_resolved', '-type', '-timestamp'))
diff --git a/AKScheduling/apps.py b/AKScheduling/apps.py
index c76ceac5ba250b7c429226de5f803871102554d8..13a065bfd3fb836dcdfa898c55f1252e79dd2cba 100644
--- a/AKScheduling/apps.py
+++ b/AKScheduling/apps.py
@@ -2,4 +2,7 @@ from django.apps import AppConfig
 
 
 class AkschedulingConfig(AppConfig):
+    """
+    App configuration (default, only specifies name of the app)
+    """
     name = 'AKScheduling'
diff --git a/AKScheduling/forms.py b/AKScheduling/forms.py
index d1739eba90c8c3b2c1af2b24c0284de4c0672d62..47a19f7857c3c9e68d2c6e2b59cecf0d79fbcebf 100644
--- a/AKScheduling/forms.py
+++ b/AKScheduling/forms.py
@@ -1,9 +1,13 @@
 from django import forms
+from django.utils.translation import gettext_lazy as _
 
 from AKModel.models import AK
 
 
 class AKInterestForm(forms.ModelForm):
+    """
+    Form for quickly changing the interest count and notes of an AK
+    """
     required_css_class = 'required'
 
     class Meta:
@@ -11,3 +15,17 @@ class AKInterestForm(forms.ModelForm):
         fields = ['interest',
                   'notes',
                   ]
+
+
+class AKAddSlotForm(forms.Form):
+    """
+    Form to create a new slot for an existing AK directly from scheduling view
+    """
+    start = forms.CharField(label=_("Start"), disabled=True)
+    end = forms.CharField(label=_("End"), disabled=True)
+    duration = forms.CharField(label=_("Duration"), disabled=True)
+    room = forms.IntegerField(label=_("Room"), disabled=True)
+
+    def __init__(self, event):
+        super().__init__()
+        self.fields['ak'] = forms.ModelChoiceField(event.ak_set.all(), label=_("AK"))
diff --git a/AKScheduling/locale/de_DE/LC_MESSAGES/django.po b/AKScheduling/locale/de_DE/LC_MESSAGES/django.po
index ec97a5ab0c8c48e0a28e7cd9d56a0188d30502a0..98bda60437d7408d8d716fc7f2fee6c994801e2d 100644
--- a/AKScheduling/locale/de_DE/LC_MESSAGES/django.po
+++ b/AKScheduling/locale/de_DE/LC_MESSAGES/django.po
@@ -8,7 +8,7 @@ msgid ""
 msgstr ""
 "Project-Id-Version: PACKAGE VERSION\n"
 "Report-Msgid-Bugs-To: \n"
-"POT-Creation-Date: 2023-05-15 20:03+0200\n"
+"POT-Creation-Date: 2023-08-16 16:30+0200\n"
 "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
 "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
 "Language-Team: LANGUAGE <LL@li.org>\n"
@@ -17,7 +17,28 @@ msgstr ""
 "Content-Type: text/plain; charset=UTF-8\n"
 "Content-Transfer-Encoding: 8bit\n"
 
-#: AKScheduling/models.py:80
+#: AKScheduling/forms.py:24
+msgid "Start"
+msgstr "Start"
+
+#: AKScheduling/forms.py:25
+msgid "End"
+msgstr "Ende"
+
+#: AKScheduling/forms.py:26
+msgid "Duration"
+msgstr ""
+
+#: AKScheduling/forms.py:27
+#: AKScheduling/templates/admin/AKScheduling/scheduling.html:171
+msgid "Room"
+msgstr "Raum"
+
+#: AKScheduling/forms.py:31
+msgid "AK"
+msgstr "AK"
+
+#: AKScheduling/models.py:92
 #, python-format
 msgid ""
 "Not enough space for AK interest (Interest: %(interest)d, Capacity: "
@@ -26,7 +47,7 @@ msgstr ""
 "Nicht genug Platz für AK-Interesse (Interesse: %(interest)d, Kapazität: "
 "%(capacity)d)"
 
-#: AKScheduling/models.py:92
+#: AKScheduling/models.py:106
 #, python-format
 msgid ""
 "Space is too close to AK interest (Interest: %(interest)d, Capacity: "
@@ -147,10 +168,6 @@ msgstr "Event (horizontal)"
 msgid "Event (Vertical)"
 msgstr "Event (vertikal)"
 
-#: AKScheduling/templates/admin/AKScheduling/scheduling.html:171
-msgid "Room"
-msgstr "Raum"
-
 #: AKScheduling/templates/admin/AKScheduling/scheduling.html:267
 msgid "Please choose AK"
 msgstr "Bitte AK auswählen"
@@ -211,19 +228,19 @@ msgstr "Noch nicht geschedulte AK-Slots"
 msgid "Count"
 msgstr "Anzahl"
 
-#: AKScheduling/views.py:112
+#: AKScheduling/views.py:148
 msgid "Interest updated"
 msgstr "Interesse aktualisiert"
 
-#: AKScheduling/views.py:157
+#: AKScheduling/views.py:199
 msgid "Wishes"
 msgstr "Wünsche"
 
-#: AKScheduling/views.py:165
+#: AKScheduling/views.py:217
 msgid "Cleanup: Delete unscheduled slots for wishes"
 msgstr "Aufräumen: Noch nicht geplante Slots für Wünsche löschen"
 
-#: AKScheduling/views.py:172
+#: AKScheduling/views.py:224
 #, python-brace-format
 msgid ""
 "The following {count} unscheduled slots of wishes will be deleted:\n"
@@ -235,15 +252,15 @@ msgstr ""
 "\n"
 " {slots}"
 
-#: AKScheduling/views.py:179
+#: AKScheduling/views.py:231
 msgid "Unscheduled slots for wishes successfully deleted"
 msgstr "Noch nicht geplante Slots für Wünsche erfolgreich gelöscht"
 
-#: AKScheduling/views.py:184
+#: AKScheduling/views.py:245
 msgid "Create default availabilities for AKs"
 msgstr "Standardverfügbarkeiten für AKs anlegen"
 
-#: AKScheduling/views.py:191
+#: AKScheduling/views.py:252
 #, python-brace-format
 msgid ""
 "The following {count} AKs don't have any availability information. Create "
@@ -256,12 +273,12 @@ msgstr ""
 "\n"
 " {aks}"
 
-#: AKScheduling/views.py:209
+#: AKScheduling/views.py:272
 #, python-brace-format
 msgid "Could not create default availabilities for AK: {ak}"
 msgstr "Konnte keine Verfügbarkeit anlegen für AK: {ak}"
 
-#: AKScheduling/views.py:214
+#: AKScheduling/views.py:277
 #, python-brace-format
 msgid "Created default availabilities for {count} AKs"
 msgstr "Standardverfügbarkeiten für {count} AKs angelegt"
diff --git a/AKScheduling/models.py b/AKScheduling/models.py
index 6664a8aee83ddf108b04666a5e99a5fea077b4e7..769527f7aec7792c565e7a18bdbb888c938eeb59 100644
--- a/AKScheduling/models.py
+++ b/AKScheduling/models.py
@@ -1,9 +1,15 @@
+# This file mainly contains signal receivers, which follow a very strong interface, having e.g., a sender attribute
+# that is hardly used by us. Nevertheless, to follow the django receiver coding style and since changes might
+# cause issues when loading fixtures or model dumps, it is not wise to replace that attribute with "_".
+# Therefore, the check that finds unused arguments is disabled for this whole file:
+# pylint: disable=unused-argument
+
 from django.db.models.signals import post_save, m2m_changed, pre_delete
 from django.dispatch import receiver
 from django.utils.translation import gettext_lazy as _
 
 from AKModel.availability.models import Availability
-from AKModel.models import AK, AKSlot, Room, Event, AKOwner, ConstraintViolation
+from AKModel.models import AK, AKSlot, Room, Event, ConstraintViolation
 
 
 def update_constraint_violations(new_violations, existing_violations_to_check):
@@ -43,11 +49,15 @@ def update_cv_reso_deadline_for_slot(slot):
     :type slot: AKSlot
     """
     event = slot.event
+
+    # Update only if reso_deadline exists
+    # if event was changed and reso_deadline is removed, CVs will be deleted by event changed handler
+    # Update only has to be done for already scheduled slots with reso intention
     if slot.ak.reso and slot.event.reso_deadline and slot.start:
-        # Update only if reso_deadline exists
-        # if event was changed and reso_deadline is removed, CVs will be deleted by event changed handler
         violation_type = ConstraintViolation.ViolationType.AK_AFTER_RESODEADLINE
         new_violations = []
+
+        # Violation?
         if slot.end > event.reso_deadline:
             c = ConstraintViolation(
                 type=violation_type,
@@ -69,38 +79,47 @@ def check_capacity_for_slot(slot: AKSlot):
     :return: Violation (if any) or None
     :rtype: ConstraintViolation or None
     """
-    if slot.room:
-        if slot.room.capacity >= 0:
-            if slot.room.capacity < slot.ak.interest:
-                c = ConstraintViolation(
-                    type=ConstraintViolation.ViolationType.ROOM_CAPACITY_EXCEEDED,
-                    level=ConstraintViolation.ViolationLevel.VIOLATION,
-                    event=slot.event,
-                    room=slot.room,
-                    comment=_("Not enough space for AK interest (Interest: %(interest)d, Capacity: %(capacity)d)")
-                            % {'interest': slot.ak.interest, 'capacity': slot.room.capacity},
-                )
-                c.ak_slots_tmp.add(slot)
-                c.aks_tmp.add(slot.ak)
-                return c
-            elif slot.room.capacity < slot.ak.interest + 5 or slot.room.capacity < slot.ak.interest * 1.25:
-                c = ConstraintViolation(
-                    type=ConstraintViolation.ViolationType.ROOM_CAPACITY_EXCEEDED,
-                    level=ConstraintViolation.ViolationLevel.WARNING,
-                    event=slot.event,
-                    room=slot.room,
-                    comment=_("Space is too close to AK interest (Interest: %(interest)d, Capacity: %(capacity)d)")
-                            % {'interest': slot.ak.interest, 'capacity': slot.room.capacity}
-                )
-                c.ak_slots_tmp.add(slot)
-                c.aks_tmp.add(slot.ak)
-                return c
-        return None
+
+    # If slot is scheduled in a room
+    if slot.room and slot.room.capacity >= 0:
+        # Create a violation if interest exceeds room capacity
+        if slot.room.capacity < slot.ak.interest:
+            c = ConstraintViolation(
+                type=ConstraintViolation.ViolationType.ROOM_CAPACITY_EXCEEDED,
+                level=ConstraintViolation.ViolationLevel.VIOLATION,
+                event=slot.event,
+                room=slot.room,
+                comment=_("Not enough space for AK interest (Interest: %(interest)d, Capacity: %(capacity)d)")
+                        % {'interest': slot.ak.interest, 'capacity': slot.room.capacity},
+            )
+            c.ak_slots_tmp.add(slot)
+            c.aks_tmp.add(slot.ak)
+            return c
+
+        # Create a warning if interest is close to room capacity
+        if slot.room.capacity < slot.ak.interest + 5 or slot.room.capacity < slot.ak.interest * 1.25:
+            c = ConstraintViolation(
+                type=ConstraintViolation.ViolationType.ROOM_CAPACITY_EXCEEDED,
+                level=ConstraintViolation.ViolationLevel.WARNING,
+                event=slot.event,
+                room=slot.room,
+                comment=_("Space is too close to AK interest (Interest: %(interest)d, Capacity: %(capacity)d)")
+                        % {'interest': slot.ak.interest, 'capacity': slot.room.capacity}
+            )
+            c.ak_slots_tmp.add(slot)
+            c.aks_tmp.add(slot.ak)
+            return c
+
+    return None
 
 
 @receiver(post_save, sender=AK)
 def ak_changed_handler(sender, instance: AK, **kwargs):
-    # Changes might affect: Reso intention, Category, Interest
+    """
+    Signal receiver: Check for violations after AK changed
+
+    Changes might affect: Reso intention, Category, Interest
+    """
     # TODO Reso intention changes
 
     # Check room capacities
@@ -118,14 +137,12 @@ def ak_changed_handler(sender, instance: AK, **kwargs):
 @receiver(m2m_changed, sender=AK.owners.through)
 def ak_owners_changed_handler(sender, instance: AK, action: str, **kwargs):
     """
-    Owners of AK changed
+    Signal receiver: Owners of AK changed
     """
     # Only signal after change (post_add, post_delete, post_clear) are relevant
     if not action.startswith("post"):
         return
 
-    # print(f"{instance} changed")
-
     event = instance.event
 
     # Owner(s) changed: Might affect multiple AKs by the same owner(s) at the same time
@@ -157,8 +174,6 @@ def ak_owners_changed_handler(sender, instance: AK, action: str, **kwargs):
                             c.ak_slots_tmp.add(other_slot)
                             new_violations.append(c)
 
-        #print(f"{owner} has the following conflicts: {new_violations}")
-
     # ... and compare to/update list of existing violations of this type
     # belonging to the AK that was recently changed (important!)
     existing_violations_to_check = list(instance.constraintviolation_set.filter(type=violation_type))
@@ -169,14 +184,12 @@ def ak_owners_changed_handler(sender, instance: AK, action: str, **kwargs):
 @receiver(m2m_changed, sender=AK.conflicts.through)
 def ak_conflicts_changed_handler(sender, instance: AK, action: str, **kwargs):
     """
-    Conflicts of AK changed
+    Signal receiver: Conflicts of AK changed
     """
     # Only signal after change (post_add, post_delete, post_clear) are relevant
     if not action.startswith("post"):
         return
 
-    # print(f"{instance} changed")
-
     event = instance.event
 
     # Conflict(s) changed: Might affect multiple AKs that are conflicts of each other
@@ -186,6 +199,7 @@ def ak_conflicts_changed_handler(sender, instance: AK, action: str, **kwargs):
     slots_of_this_ak: [AKSlot] = instance.akslot_set.filter(start__isnull=False)
     conflicts_of_this_ak: [AK] = instance.conflicts.all()
 
+    # Loop over all existing conflicts
     for ak in conflicts_of_this_ak:
         if ak != instance:
             for other_slot in ak.akslot_set.filter(start__isnull=False):
@@ -203,8 +217,6 @@ def ak_conflicts_changed_handler(sender, instance: AK, action: str, **kwargs):
                         c.ak_slots_tmp.add(other_slot)
                         new_violations.append(c)
 
-        # print(f"{instance} has the following conflicts: {new_violations}")
-
     # ... and compare to/update list of existing violations of this type
     # belonging to the AK that was recently changed (important!)
     existing_violations_to_check = list(instance.constraintviolation_set.filter(type=violation_type))
@@ -215,23 +227,22 @@ def ak_conflicts_changed_handler(sender, instance: AK, action: str, **kwargs):
 @receiver(m2m_changed, sender=AK.prerequisites.through)
 def ak_prerequisites_changed_handler(sender, instance: AK, action: str, **kwargs):
     """
-    Prerequisites of AK changed
+    Signal receiver: Prerequisites of AK changed
     """
     # Only signal after change (post_add, post_delete, post_clear) are relevant
     if not action.startswith("post"):
         return
 
-    # print(f"{instance} changed")
-
     event = instance.event
 
-    # Conflict(s) changed: Might affect multiple AKs that are conflicts of each other
+    # Prerequisite(s) changed: Might affect multiple AKs that should have a certain order
     violation_type = ConstraintViolation.ViolationType.AK_BEFORE_PREREQUISITE
     new_violations = []
 
     slots_of_this_ak: [AKSlot] = instance.akslot_set.filter(start__isnull=False)
     prerequisites_of_this_ak: [AK] = instance.prerequisites.all()
 
+    # Loop over all prerequisites
     for ak in prerequisites_of_this_ak:
         if ak != instance:
             for other_slot in ak.akslot_set.filter(start__isnull=False):
@@ -249,8 +260,6 @@ def ak_prerequisites_changed_handler(sender, instance: AK, action: str, **kwargs
                         c.ak_slots_tmp.add(other_slot)
                         new_violations.append(c)
 
-        # print(f"{instance} has the following conflicts: {new_violations}")
-
     # ... and compare to/update list of existing violations of this type
     # belonging to the AK that was recently changed (important!)
     existing_violations_to_check = list(instance.constraintviolation_set.filter(type=violation_type))
@@ -261,14 +270,12 @@ def ak_prerequisites_changed_handler(sender, instance: AK, action: str, **kwargs
 @receiver(m2m_changed, sender=AK.requirements.through)
 def ak_requirements_changed_handler(sender, instance: AK, action: str, **kwargs):
     """
-    Requirements of AK changed
+    Signal receiver: Requirements of AK changed
     """
     # Only signal after change (post_add, post_delete, post_clear) are relevant
     if not action.startswith("post"):
         return
 
-    # print(f"{instance} changed")
-
     event = instance.event
 
     # Requirement(s) changed: Might affect slots and rooms
@@ -298,8 +305,6 @@ def ak_requirements_changed_handler(sender, instance: AK, action: str, **kwargs)
                 c.ak_slots_tmp.add(slot)
                 new_violations.append(c)
 
-    # print(f"{instance} has the following conflicts: {new_violations}")
-
     # ... and compare to/update list of existing violations of this type
     # belonging to the AK that was recently changed (important!)
     existing_violations_to_check = list(instance.constraintviolation_set.filter(type=violation_type))
@@ -309,8 +314,13 @@ def ak_requirements_changed_handler(sender, instance: AK, action: str, **kwargs)
 
 @receiver(post_save, sender=AKSlot)
 def akslot_changed_handler(sender, instance: AKSlot, **kwargs):
-    # Changes might affect: Duplicate parallel, Two in room, Resodeadline
-    # print(f"{sender} changed")
+    """
+    Signal receiver: AKSlot changed
+
+    Changes might affect: Duplicate parallel, Two in room, Resodeadline
+    """
+    # TODO Consider rewriting this very long and complex method to resolve several (style) issues:
+    # pylint: disable=too-many-nested-blocks,too-many-locals,too-many-branches,too-many-statements
     event = instance.event
 
     # == Check for two parallel slots by one of the owners ==
@@ -341,8 +351,6 @@ def akslot_changed_handler(sender, instance: AKSlot, **kwargs):
                             c.ak_slots_tmp.add(other_slot)
                             new_violations.append(c)
 
-            # print(f"{owner} has the following conflicts: {new_violations}")
-
     # ... and compare to/update list of existing violations of this type
     # belonging to the AK that was recently changed (important!)
     existing_violations_to_check = list(instance.constraintviolation_set.filter(type=violation_type))
@@ -373,8 +381,6 @@ def akslot_changed_handler(sender, instance: AKSlot, **kwargs):
                     c.ak_slots_tmp.add(other_slot)
                     new_violations.append(c)
 
-        # print(f"Multiple slots in room {instance.room}: {new_violations}")
-
     # ... and compare to/update list of existing violations of this type
     # belonging to the slot that was recently changed (important!)
     existing_violations_to_check = list(instance.constraintviolation_set.filter(type=violation_type))
@@ -437,8 +443,6 @@ def akslot_changed_handler(sender, instance: AKSlot, **kwargs):
             c.ak_slots_tmp.add(instance)
             new_violations.append(c)
 
-    # print(f"{instance.ak} has the following slots outside availabilities: {new_violations}")
-
     # ... and compare to/update list of existing violations of this type
     # belonging to the AK that was recently changed (important!)
     existing_violations_to_check = list(instance.constraintviolation_set.filter(type=violation_type))
@@ -470,8 +474,6 @@ def akslot_changed_handler(sender, instance: AKSlot, **kwargs):
                 c.ak_slots_tmp.add(instance)
                 new_violations.append(c)
 
-    # print(f"{instance} has the following conflicts: {new_violations}")
-
     # ... and compare to/update list of existing violations of this type
     # belonging to the AK that was recently changed (important!)
     existing_violations_to_check = list(instance.constraintviolation_set.filter(type=violation_type))
@@ -502,8 +504,6 @@ def akslot_changed_handler(sender, instance: AKSlot, **kwargs):
                         c.ak_slots_tmp.add(other_slot)
                         new_violations.append(c)
 
-            # print(f"{instance} has the following conflicts: {new_violations}")
-
     # ... and compare to/update list of existing violations of this type
     # belonging to the AK that was recently changed (important!)
     existing_violations_to_check = list(instance.ak.constraintviolation_set.filter(type=violation_type))
@@ -534,8 +534,6 @@ def akslot_changed_handler(sender, instance: AKSlot, **kwargs):
                         c.ak_slots_tmp.add(other_slot)
                         new_violations.append(c)
 
-            # print(f"{instance} has the following conflicts: {new_violations}")
-
     # ... and compare to/update list of existing violations of this type
     # belonging to the AK that was recently changed (important!)
     existing_violations_to_check = list(instance.ak.constraintviolation_set.filter(type=violation_type))
@@ -547,15 +545,21 @@ def akslot_changed_handler(sender, instance: AKSlot, **kwargs):
     new_violations = [cv] if cv is not None else []
 
     # Compare to/update list of existing violations of this type for this slot
-    existing_violations_to_check = list(instance.constraintviolation_set.filter(type=ConstraintViolation.ViolationType.ROOM_CAPACITY_EXCEEDED))
+    existing_violations_to_check = list(
+        instance.constraintviolation_set.filter(type=ConstraintViolation.ViolationType.ROOM_CAPACITY_EXCEEDED)
+    )
     update_constraint_violations(new_violations, existing_violations_to_check)
 
 
 @receiver(pre_delete, sender=AKSlot)
 def akslot_deleted_handler(sender, instance: AKSlot, **kwargs):
-    # Manually clean up or remove constraint violations that belong to this slot since there is no cascade deletion
-    # for many2many relationships. Explicitly listening for AK deletion signals is not necessary since they will
-    # transitively trigger this signal and we always set both AK and AKSlot references in a constraint violation
+    """
+    Signal receiver: AKSlot deleted
+
+    Manually clean up or remove constraint violations that belong to this slot since there is no cascade deletion
+    for many2many relationships. Explicitly listening for AK deletion signals is not necessary since they will
+    transitively trigger this signal and we always set both AK and AKSlot references in a constraint violation
+    """
     # print(f"{instance} deleted")
 
     for cv in instance.constraintviolation_set.all():
@@ -566,8 +570,11 @@ def akslot_deleted_handler(sender, instance: AKSlot, **kwargs):
 
 @receiver(post_save, sender=Room)
 def room_changed_handler(sender, instance: Room, **kwargs):
-    # Changes might affect: Room size
+    """
+    Signal receiver: Room changed
 
+    Changes might affect: Room size
+    """
     # Check room capacities
     violation_type = ConstraintViolation.ViolationType.ROOM_CAPACITY_EXCEEDED
     new_violations = []
@@ -583,24 +590,23 @@ def room_changed_handler(sender, instance: Room, **kwargs):
 @receiver(m2m_changed, sender=Room.properties.through)
 def room_requirements_changed_handler(sender, instance: Room, action: str, **kwargs):
     """
-    Requirements of room changed
+    Signal Receiver: Requirements of room changed
     """
     # Only signal after change (post_add, post_delete, post_clear) are relevant
     if not action.startswith("post"):
         return
 
-    # print(f"{instance} changed")
-
-    event = instance.event
-
+    # event = instance.event
     # TODO React to changes
 
 
 @receiver(post_save, sender=Availability)
 def availability_changed_handler(sender, instance: Availability, **kwargs):
-    # Changes might affect: category availability, AK availability, Room availability
-    # print(f"{instance} changed")
+    """
+    Signal receiver: Availalability changed
 
+    Changes might affect: category availability, AK availability, Room availability
+    """
     event = instance.event
 
     # An AK's availability changed: Might affect AK slots scheduled outside the permitted time
@@ -627,8 +633,6 @@ def availability_changed_handler(sender, instance: Availability, **kwargs):
                 c.ak_slots_tmp.add(slot)
                 new_violations.append(c)
 
-        # print(f"{instance.ak} has the following slots outside availabilities: {new_violations}")
-
         # ... and compare to/update list of existing violations of this type
         # belonging to the AK that was recently changed (important!)
         existing_violations_to_check = list(instance.ak.constraintviolation_set.filter(type=violation_type))
@@ -638,7 +642,12 @@ def availability_changed_handler(sender, instance: Availability, **kwargs):
 
 @receiver(post_save, sender=Event)
 def event_changed_handler(sender, instance: Event, **kwargs):
-    # == Check for reso ak after reso deadline (which might have changed) ==
+    """
+    Signal receiver: Event changed
+
+    Changes might affect: Reso deadline
+    """
+    # Check for reso ak after reso deadline (which might have changed)
     if instance.reso_deadline:
         for slot in instance.akslot_set.filter(start__isnull=False, ak__reso=True):
             update_cv_reso_deadline_for_slot(slot)
diff --git a/AKScheduling/tests.py b/AKScheduling/tests.py
index 8b7bcf91eeb40abe1185b814b0018c2f36715c51..0996eedd905259f0f463589a89f68bde055bc01a 100644
--- a/AKScheduling/tests.py
+++ b/AKScheduling/tests.py
@@ -1,11 +1,20 @@
+import json
+from datetime import timedelta
+
 from django.test import TestCase
-from AKModel.tests import BasicViewTests
+from django.utils import timezone
 
+from AKModel.tests import BasicViewTests
+from AKModel.models import AKSlot, Event, Room
 
 class ModelViewTests(BasicViewTests, TestCase):
+    """
+    Tests for AKScheduling
+    """
     fixtures = ['model.json']
 
     VIEWS_STAFF_ONLY = [
+        # Views
         ('admin:schedule', {'event_slug': 'kif42'}),
         ('admin:slots_unscheduled', {'event_slug': 'kif42'}),
         ('admin:constraint-violations', {'slug': 'kif42'}),
@@ -14,4 +23,66 @@ class ModelViewTests(BasicViewTests, TestCase):
         ('admin:autocreate-availabilities', {'event_slug': 'kif42'}),
         ('admin:tracks_manage', {'event_slug': 'kif42'}),
         ('admin:enter-interest', {'event_slug': 'kif42', 'pk': 1}),
+        # API (Read)
+        ('model:scheduling-resources-list', {'event_slug': 'kif42'}, 403),
+        ('model:scheduling-constraint-violations-list', {'event_slug': 'kif42'}, 403),
+        ('model:scheduling-events', {'event_slug': 'kif42'}),
+        ('model:scheduling-room-availabilities', {'event_slug': 'kif42'}),
+        ('model:scheduling-default-slots', {'event_slug': 'kif42'}),
     ]
+
+    def test_scheduling_of_slot_update(self):
+        """
+        Test rescheduling a slot to a different time or room
+        """
+        self.client.force_login(self.admin_user)
+
+        event = Event.get_by_slug('kif42')
+
+        # Get the first already scheduled slot belonging to this event
+        slot = event.akslot_set.filter(start__isnull=False).first()
+        pk = slot.pk
+        room_id = slot.room_id
+        events_api_url = f"/kif42/api/scheduling-event/{pk}/"
+
+        # Create updated time
+        offset = timedelta(hours=1)
+        new_start_time = slot.start + offset
+        new_end_time = slot.end + offset
+        new_start_time_string = timezone.localtime(new_start_time, event.timezone).strftime("%Y-%m-%d %H:%M:%S")
+        new_end_time_string = timezone.localtime(new_end_time, event.timezone).strftime("%Y-%m-%d %H:%M:%S")
+
+        # Try API call
+        response = self.client.put(
+            events_api_url,
+            json.dumps({
+                'start': new_start_time_string,
+                'end': new_end_time_string,
+                'roomId': room_id,
+            }),
+            content_type = 'application/json'
+        )
+        self.assertEqual(response.status_code, 200, "PUT to API endpoint did not work")
+
+        # Make sure API call did update the slot as expected
+        slot = AKSlot.objects.get(pk=pk)
+        self.assertEqual(new_start_time, slot.start, "Update did not work")
+
+        # Test updating room
+        new_room = Room.objects.exclude(pk=room_id).first()
+
+        # Try second API call
+        response = self.client.put(
+            events_api_url,
+            json.dumps({
+                'start': new_start_time_string,
+                'end': new_end_time_string,
+                'roomId': new_room.pk,
+            }),
+            content_type = 'application/json'
+        )
+        self.assertEqual(response.status_code, 200, "Second PUT to API endpoint did not work")
+
+        # Make sure API call did update the slot as expected
+        slot = AKSlot.objects.get(pk=pk)
+        self.assertEqual(new_room.pk, slot.room.pk, "Update did not work")
diff --git a/AKScheduling/views.py b/AKScheduling/views.py
index 7174287f22a37ad499fc602ec3e83c5ec687479f..f0be637f0ec4129507d08129e4b2801eeb671f90 100644
--- a/AKScheduling/views.py
+++ b/AKScheduling/views.py
@@ -7,11 +7,13 @@ from django.views.generic import ListView, DetailView, UpdateView
 
 from AKModel.models import AKSlot, AKTrack, Event, AK, AKCategory
 from AKModel.metaviews.admin import EventSlugMixin, FilterByEventSlugMixin, AdminViewMixin, IntermediateAdminView
-from AKScheduling.forms import AKInterestForm
-from AKSubmission.forms import AKAddSlotForm
+from AKScheduling.forms import AKInterestForm, AKAddSlotForm
 
 
 class UnscheduledSlotsAdminView(AdminViewMixin, FilterByEventSlugMixin, ListView):
+    """
+    Admin view: Get a list of all unscheduled slots
+    """
     template_name = "admin/AKScheduling/unscheduled.html"
     model = AKSlot
     context_object_name = "akslots"
@@ -26,6 +28,12 @@ class UnscheduledSlotsAdminView(AdminViewMixin, FilterByEventSlugMixin, ListView
 
 
 class SchedulingAdminView(AdminViewMixin, FilterByEventSlugMixin, ListView):
+    """
+    Admin view: Scheduler
+
+    View and adapt the schedule of an event. This view heavily uses JavaScript to display a calendar view plus
+    a list of unscheduled slots and to allow dragging slots in and into the calendar
+    """
     template_name = "admin/AKScheduling/scheduling.html"
     model = AKSlot
     context_object_name = "slots_unscheduled"
@@ -47,6 +55,12 @@ class SchedulingAdminView(AdminViewMixin, FilterByEventSlugMixin, ListView):
 
 
 class TrackAdminView(AdminViewMixin, FilterByEventSlugMixin, ListView):
+    """
+    Admin view: Distribute AKs to tracks
+
+    Again using JavaScript, the user can here see a list of all AKs split-up by tracks and can move them to other or
+    even new tracks using drag and drop. The state is then automatically synchronized via API calls in the background
+    """
     template_name = "admin/AKScheduling/manage_tracks.html"
     model = AKTrack
     context_object_name = "tracks"
@@ -58,6 +72,12 @@ class TrackAdminView(AdminViewMixin, FilterByEventSlugMixin, ListView):
 
 
 class ConstraintViolationsAdminView(AdminViewMixin, DetailView):
+    """
+    Admin view: Inspect and adjust all constraint violations of the event
+
+    This view populates a table of constraint violations via background API call (JavaScript), offers the option to
+    see details or edit each of them and provides an auto-reload feature.
+    """
     template_name = "admin/AKScheduling/constraint_violations.html"
     model = Event
     context_object_name = "event"
@@ -69,6 +89,10 @@ class ConstraintViolationsAdminView(AdminViewMixin, DetailView):
 
 
 class SpecialAttentionAKsAdminView(AdminViewMixin, DetailView):
+    """
+    Admin view: List all AKs that require special attention via scheduling, e.g., because of free-form comments,
+    since there are slots even though it is a wish, or no slots even though it is an AK etc.
+    """
     template_name = "admin/AKScheduling/special_attention.html"
     model = Event
     context_object_name = "event"
@@ -77,12 +101,16 @@ class SpecialAttentionAKsAdminView(AdminViewMixin, DetailView):
         context = super().get_context_data(**kwargs)
         context["title"] = f"{_('AKs requiring special attention for')} {context['event']}"
 
-        aks = AK.objects.filter(event=context["event"]).annotate(Count('owners', distinct=True)).annotate(Count('akslot', distinct=True)).annotate(Count('availabilities', distinct=True))
+        # Load all "special" AKs from the database using annotations to reduce the amount of necessary queries
+        aks = (AK.objects.filter(event=context["event"]).annotate(Count('owners', distinct=True))
+               .annotate(Count('akslot', distinct=True)).annotate(Count('availabilities', distinct=True)))
         aks_with_comment = []
         ak_wishes_with_slots = []
         aks_without_availabilities = []
         aks_without_slots = []
 
+        # Loop over all AKs of this event and identify all relevant factors that make the AK "special" and add them to
+        # the respective lists if the AK fullfills an condition
         for ak in aks:
             if ak.notes != "":
                 aks_with_comment.append(ak)
@@ -105,6 +133,14 @@ class SpecialAttentionAKsAdminView(AdminViewMixin, DetailView):
 
 
 class InterestEnteringAdminView(SuccessMessageMixin, AdminViewMixin, EventSlugMixin, UpdateView):
+    """
+    Admin view: Form view to quickly store information about the interest in an AK
+    (e.g., during presentation of the AK list)
+
+    The view offers a field to update interest and manually set a comment for the current AK, but also features links
+    to the AKs before and probably coming up next, as well as links to other AKs sorted by category, for quick
+    and hazzle-free navigation during the AK presentation
+    """
     template_name = "admin/AKScheduling/interest.html"
     model = AK
     context_object_name = "ak"
@@ -127,6 +163,8 @@ class InterestEnteringAdminView(SuccessMessageMixin, AdminViewMixin, EventSlugMi
         last_ak = None
         next_is_next = False
 
+        # Building the right navigation is a bit tricky since wishes have to be treated as an own category here
+        # Hence, depending on the AK we are currently at (displaying the form for) we need to either:
         # Find other AK wishes (regardless of the category)...
         if context['ak'].wish:
             other_aks = [ak for ak in context['event'].ak_set.prefetch_related('owners').all() if ak.wish]
@@ -134,6 +172,7 @@ class InterestEnteringAdminView(SuccessMessageMixin, AdminViewMixin, EventSlugMi
         else:
             other_aks = [ak for ak in context['ak'].category.ak_set.prefetch_related('owners').all() if not ak.wish]
 
+        # Use that list of other AKs belonging to this category to identify the previous and next AK (if any)
         for other_ak in other_aks:
             if next_is_next:
                 context['next_ak'] = other_ak
@@ -143,6 +182,7 @@ class InterestEnteringAdminView(SuccessMessageMixin, AdminViewMixin, EventSlugMi
                 next_is_next = True
             last_ak = other_ak
 
+        # Gather information for link lists for all categories (and wishes)
         for category in context['event'].akcategory_set.prefetch_related('ak_set').all():
             aks_for_category = []
             for ak in category.ak_set.prefetch_related('owners').all():
@@ -152,6 +192,8 @@ class InterestEnteringAdminView(SuccessMessageMixin, AdminViewMixin, EventSlugMi
                     aks_for_category.append(ak)
             categories_with_aks.append((category, aks_for_category))
 
+        # Make sure wishes have the right order (since the list was filled category by category before, this requires
+        # explicitly reordering them by their primary key)
         ak_wishes.sort(key=lambda x: x.pk)
         categories_with_aks.append(
                 (AKCategory(name=_("Wishes"), pk=0, description="-"), ak_wishes))
@@ -162,6 +204,16 @@ class InterestEnteringAdminView(SuccessMessageMixin, AdminViewMixin, EventSlugMi
 
 
 class WishSlotCleanupView(EventSlugMixin, IntermediateAdminView):
+    """
+    Admin action view: Allow to delete all unscheduled slots for wishes
+
+    The view will render a preview of all slots that are affected by this. It is not possible to manually choose
+    which slots should be deleted (either all or none) and the functionality will therefore delete slots that were
+    created in the time between rendering of the preview and running the action ofter confirmation as well.
+
+    Due to the automated slot cleanup functionality for wishes in the AKSubmission app, this functionality should be
+    rarely needed/used
+    """
     title = _('Cleanup: Delete unscheduled slots for wishes')
 
     def get_success_url(self):
@@ -181,6 +233,15 @@ class WishSlotCleanupView(EventSlugMixin, IntermediateAdminView):
 
 
 class AvailabilityAutocreateView(EventSlugMixin, IntermediateAdminView):
+    """
+    Admin action view: Allow to automatically create default availabilities (event start to end) for all AKs without
+    any manually specified availability information
+
+    The view will render a preview of all AKs that are affected by this. It is not possible to manually choose
+    which AKs should be affected (either all or none) and the functionality will therefore create availability entries
+    for AKs that were created in the time between rendering of the preview and running the action ofter confirmation
+    as well.
+    """
     title = _('Create default availabilities for AKs')
 
     def get_success_url(self):
@@ -195,6 +256,8 @@ class AvailabilityAutocreateView(EventSlugMixin, IntermediateAdminView):
         )
 
     def form_valid(self, form):
+        # Local import to prevent cyclic imports
+        # pylint: disable=import-outside-toplevel
         from AKModel.availability.models import Availability
 
         success_count = 0
@@ -203,7 +266,7 @@ class AvailabilityAutocreateView(EventSlugMixin, IntermediateAdminView):
                 availability = Availability.with_event_length(event=self.event, ak=ak)
                 availability.save()
                 success_count += 1
-            except:
+            except: # pylint: disable=bare-except
                 messages.add_message(
                     self.request, messages.WARNING,
                     _("Could not create default availabilities for AK: {ak}").format(ak=ak)
diff --git a/AKSubmission/admin.py b/AKSubmission/admin.py
deleted file mode 100644
index 846f6b4061a68eda58bc9c76c36603d1e7721ee8..0000000000000000000000000000000000000000
--- a/AKSubmission/admin.py
+++ /dev/null
@@ -1 +0,0 @@
-# Register your models here.
diff --git a/AKSubmission/api.py b/AKSubmission/api.py
index db46d40c0455c0553bb311302cc39145c41af1f7..4bcbafa8eb85d80118e4f0ff8b2d8e382c04feb8 100644
--- a/AKSubmission/api.py
+++ b/AKSubmission/api.py
@@ -25,9 +25,13 @@ def ak_interest_indication_active(event, current_timestamp):
 def increment_interest_counter(request, event_slug, pk, **kwargs):
     """
     Increment interest counter for AK
+
+    This view either returns a HTTP 200 if the counter was incremented,
+    an HTTP 403 if indicating interest is currently not allowed,
+    or an HTTP 404 if there is no matching AK for the given primary key and event slug.
     """
     try:
-        ak = AK.objects.get(pk=pk)
+        ak = AK.objects.get(pk=pk, event__slug=event_slug)
         # Check whether interest indication is currently allowed
         current_timestamp = datetime.now().astimezone(ak.event.timezone)
         if ak_interest_indication_active(ak.event, current_timestamp):
diff --git a/AKSubmission/apps.py b/AKSubmission/apps.py
index 6a3d8e74a76d551e512471836bf7c673d047a970..51527b4e8d2eb0f9b88bc4f42ffdf114d6f4fcaf 100644
--- a/AKSubmission/apps.py
+++ b/AKSubmission/apps.py
@@ -2,4 +2,7 @@ from django.apps import AppConfig
 
 
 class AksubmissionConfig(AppConfig):
+    """
+    App configuration (default, only specifies name of the app)
+    """
     name = 'AKSubmission'
diff --git a/AKSubmission/forms.py b/AKSubmission/forms.py
index 3f324bd41e6e70c0707e557a43f026843a7ac3af..1ee786d22963dacf1301d1b2984b7771defc1d58 100644
--- a/AKSubmission/forms.py
+++ b/AKSubmission/forms.py
@@ -1,3 +1,7 @@
+"""
+Submission-specific forms
+"""
+
 import itertools
 import re
 
@@ -7,10 +11,21 @@ from django.utils.translation import gettext_lazy as _
 
 from AKModel.availability.forms import AvailabilitiesFormMixin
 from AKModel.availability.models import Availability
-from AKModel.models import AK, AKOwner, AKCategory, AKRequirement, AKSlot, AKOrgaMessage, Event
+from AKModel.models import AK, AKOwner, AKCategory, AKRequirement, AKSlot, AKOrgaMessage
 
 
 class AKForm(AvailabilitiesFormMixin, forms.ModelForm):
+    """
+    Base form to add and edit AKs
+
+    Contains suitable widgets for the different data types, restricts querysets (e.g., of requirements) to entries
+    belonging to the event this AK belongs to.
+    Prepares initial slot creation (by accepting multiple input formats and a list of slots to generate),
+    automatically generate short names and wiki links if necessary
+
+    Will be modified/used by :class:`AKSubmissionForm` (that allows to add slots and excludes links)
+    and :class:`AKWishForm`
+    """
     required_css_class = 'required'
     split_string = re.compile('[,;]')
 
@@ -57,7 +72,14 @@ class AKForm(AvailabilitiesFormMixin, forms.ModelForm):
 
     @staticmethod
     def _clean_duration(duration):
-        # Handle different duration formats (h:mm and decimal comma instead of point)
+        """
+        Clean/convert input format for the duration(s) of the slot(s)
+
+        Handle different duration formats (h:mm and decimal comma instead of point)
+
+        :param duration: raw input, either with ":", "," or "."
+        :return: normalized duration (point-separated hour float)
+        """
         if ":" in duration:
             h, m = duration.split(":")
             duration = int(h) + int(m) / 60
@@ -66,31 +88,44 @@ class AKForm(AvailabilitiesFormMixin, forms.ModelForm):
 
         try:
             float(duration)
-        except ValueError:
+        except ValueError as exc:
             raise ValidationError(
                 _('"%(duration)s" is not a valid duration'),
                 code='invalid',
                 params={'duration': duration},
-            )
+            ) from exc
 
         return duration
 
     def clean(self):
+        """
+        Normalize/clean inputs
+
+        Generate a (not yet used) short name if field was left blank, generate a wiki link,
+        create a list of normalized slot durations
+
+        :return: cleaned inputs
+        """
         cleaned_data = super().clean()
 
         # Generate short name if not given
         short_name = self.cleaned_data["short_name"]
         if len(short_name) == 0:
             short_name = self.cleaned_data['name']
+            # First try to split AK name at positions with semantic value (e.g., where the full name is separated
+            # by a ':'), if not possible, do a hard cut at the maximum specified length
             short_name = short_name.partition(':')[0]
             short_name = short_name.partition(' - ')[0]
             short_name = short_name.partition(' (')[0]
             short_name = short_name[:AK._meta.get_field('short_name').max_length]
+            # Check whether this short name already exists...
             for i in itertools.count(1):
+                # ...and either use it...
                 if not AK.objects.filter(short_name=short_name, event=self.cleaned_data["event"]).exists():
                     break
+                # ... or postfix a number starting at 1 and growing until an unused short name is found
                 digits = len(str(i))
-                short_name = '{}-{}'.format(short_name[:-(digits + 1)], i)
+                short_name = f'{short_name[:-(digits + 1)]}-{i}'
             cleaned_data["short_name"] = short_name
 
         # Generate wiki link
@@ -106,35 +141,57 @@ class AKForm(AvailabilitiesFormMixin, forms.ModelForm):
 
 
 class AKSubmissionForm(AKForm):
+    """
+    Form for Submitting new AKs
+
+    Is a special variant of :class:`AKForm` that does not allow to manually edit wiki and protocol links and enforces
+    the generation of at least one slot.
+    """
     class Meta(AKForm.Meta):
-        exclude = ['link', 'protocol_link']
+        # Exclude fields again that were previously included in the parent class
+        exclude = ['link', 'protocol_link'] #pylint: disable=modelform-uses-exclude
 
     def __init__(self, *args, **kwargs):
         super().__init__(*args, **kwargs)
-        # Add field for durations
+        # Add field for durations (cleaning will be handled by parent class)
         self.fields["durations"] = forms.CharField(
             widget=forms.Textarea,
             label=_("Duration(s)"),
             help_text=_(
-                "Enter at least one planned duration (in hours). If your AK should have multiple slots, use multiple lines"),
-            initial=
-            self.initial.get('event').default_slot
+                "Enter at least one planned duration (in hours). "
+                "If your AK should have multiple slots, use multiple lines"),
+            initial=self.initial.get('event').default_slot
         )
 
     def clean_availabilities(self):
+        """
+        Automatically improve availabilities entered.
+        If the user did not specify availabilities assume the full event duration is possible
+        :return: cleaned availabilities
+        (either user input or one availability for the full length of the event if user input was empty)
+        """
         availabilities = super().clean_availabilities()
-        # If the user did not specify availabilities assume the full event duration is possible
         if len(availabilities) == 0:
             availabilities.append(Availability.with_event_length(event=self.cleaned_data["event"]))
         return availabilities
 
 
 class AKWishForm(AKForm):
+    """
+    Form for submitting or editing wishes
+
+    Is a special variant of :class:`AKForm` that does not allow to specify owner(s) or
+    manually edit wiki and protocol links
+    """
     class Meta(AKForm.Meta):
-        exclude = ['owners', 'link', 'protocol_link']
+        # Exclude fields again that were previously included in the parent class
+        exclude = ['owners', 'link', 'protocol_link'] #pylint: disable=modelform-uses-exclude
 
 
 class AKOwnerForm(forms.ModelForm):
+    """
+    Form to create/edit AK owners
+    """
     required_css_class = 'required'
 
     class Meta:
@@ -146,6 +203,9 @@ class AKOwnerForm(forms.ModelForm):
 
 
 class AKDurationForm(forms.ModelForm):
+    """
+    Form to add an additional slot to a given AK
+    """
     class Meta:
         model = AKSlot
         fields = ['duration', 'ak', 'event']
@@ -156,6 +216,9 @@ class AKDurationForm(forms.ModelForm):
 
 
 class AKOrgaMessageForm(forms.ModelForm):
+    """
+    Form to create a confidential message to the organizers  belonging to a given AK
+    """
     class Meta:
         model = AKOrgaMessage
         fields = ['ak', 'text', 'event']
@@ -164,14 +227,3 @@ class AKOrgaMessageForm(forms.ModelForm):
             'event': forms.HiddenInput,
             'text': forms.Textarea,
         }
-
-
-class AKAddSlotForm(forms.Form):
-    start = forms.CharField(label=_("Start"), disabled=True)
-    end = forms.CharField(label=_("End"), disabled=True)
-    duration = forms.CharField(label=_("Duration"), disabled=True)
-    room = forms.IntegerField(label=_("Room"), disabled=True)
-
-    def __init__(self, event):
-        super().__init__()
-        self.fields['ak'] = forms.ModelChoiceField(event.ak_set.all(), label=_("AK"))
diff --git a/AKSubmission/locale/de_DE/LC_MESSAGES/django.po b/AKSubmission/locale/de_DE/LC_MESSAGES/django.po
index b8462e566cae32be6292571c7b6e1a78999b2d22..b25db7600b98a97b8498ea72067f9504f5b2b1c8 100644
--- a/AKSubmission/locale/de_DE/LC_MESSAGES/django.po
+++ b/AKSubmission/locale/de_DE/LC_MESSAGES/django.po
@@ -8,7 +8,7 @@ msgid ""
 msgstr ""
 "Project-Id-Version: PACKAGE VERSION\n"
 "Report-Msgid-Bugs-To: \n"
-"POT-Creation-Date: 2023-05-15 20:03+0200\n"
+"POT-Creation-Date: 2023-08-16 16:30+0200\n"
 "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
 "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
 "Language-Team: LANGUAGE <LL@li.org>\n"
@@ -17,16 +17,16 @@ msgstr ""
 "Content-Type: text/plain; charset=UTF-8\n"
 "Content-Transfer-Encoding: 8bit\n"
 
-#: AKSubmission/forms.py:71
+#: AKSubmission/forms.py:93
 #, python-format
 msgid "\"%(duration)s\" is not a valid duration"
 msgstr "\"%(duration)s\" ist keine gültige Dauer"
 
-#: AKSubmission/forms.py:117
+#: AKSubmission/forms.py:159
 msgid "Duration(s)"
 msgstr "Dauer(n)"
 
-#: AKSubmission/forms.py:119
+#: AKSubmission/forms.py:161
 msgid ""
 "Enter at least one planned duration (in hours). If your AK should have "
 "multiple slots, use multiple lines"
@@ -34,33 +34,6 @@ msgstr ""
 "Mindestens eine geplante Dauer (in Stunden) angeben. Wenn der AK mehrere "
 "Slots haben soll, mehrere Zeilen verwenden"
 
-#: AKSubmission/forms.py:170
-#: AKSubmission/templates/AKSubmission/ak_detail.html:309
-msgid "Start"
-msgstr "Start"
-
-#: AKSubmission/forms.py:171
-#: AKSubmission/templates/AKSubmission/ak_detail.html:310
-msgid "End"
-msgstr "Ende"
-
-#: AKSubmission/forms.py:172
-#: AKSubmission/templates/AKSubmission/ak_detail.html:239
-#: AKSubmission/templates/AKSubmission/akslot_delete.html:35
-msgid "Duration"
-msgstr "Dauer"
-
-#: AKSubmission/forms.py:173
-#: AKSubmission/templates/AKSubmission/ak_detail.html:241
-msgid "Room"
-msgstr "Raum"
-
-#: AKSubmission/forms.py:177
-#: AKSubmission/templates/AKSubmission/ak_history.html:11
-#: AKSubmission/templates/AKSubmission/akslot_delete.html:31
-msgid "AK"
-msgstr "AK"
-
 #: AKSubmission/templates/AKSubmission/ak_detail.html:22
 #: AKSubmission/templates/AKSubmission/ak_edit.html:13
 #: AKSubmission/templates/AKSubmission/ak_history.html:16
@@ -215,6 +188,15 @@ msgstr "Notizen"
 msgid "When?"
 msgstr "Wann?"
 
+#: AKSubmission/templates/AKSubmission/ak_detail.html:239
+#: AKSubmission/templates/AKSubmission/akslot_delete.html:35
+msgid "Duration"
+msgstr "Dauer"
+
+#: AKSubmission/templates/AKSubmission/ak_detail.html:241
+msgid "Room"
+msgstr "Raum"
+
 #: AKSubmission/templates/AKSubmission/ak_detail.html:272
 msgid "Delete"
 msgstr "Löschen"
@@ -231,6 +213,14 @@ msgstr "Einen neuen AK-Slot hinzufügen"
 msgid "Possible Times"
 msgstr "Mögliche Zeiten"
 
+#: AKSubmission/templates/AKSubmission/ak_detail.html:309
+msgid "Start"
+msgstr "Start"
+
+#: AKSubmission/templates/AKSubmission/ak_detail.html:310
+msgid "End"
+msgstr "Ende"
+
 #: AKSubmission/templates/AKSubmission/ak_edit.html:8
 #: AKSubmission/templates/AKSubmission/ak_history.html:11
 #: AKSubmission/templates/AKSubmission/ak_overview.html:8
@@ -260,6 +250,11 @@ msgstr ""
 "Person hinzufügen, die noch nicht in der Liste ist. Ungespeicherte "
 "Änderungen in diesem Formular gehen verloren."
 
+#: AKSubmission/templates/AKSubmission/ak_history.html:11
+#: AKSubmission/templates/AKSubmission/akslot_delete.html:31
+msgid "AK"
+msgstr "AK"
+
 #: AKSubmission/templates/AKSubmission/ak_history.html:27
 msgid "Back"
 msgstr "Zurück"
@@ -283,7 +278,7 @@ msgstr "Die Ergebnisse dieses AKs vorstellen"
 msgid "Intends to submit a resolution"
 msgstr "Beabsichtigt eine Resolution einzureichen"
 
-#: AKSubmission/templates/AKSubmission/ak_list.html:6 AKSubmission/views.py:42
+#: AKSubmission/templates/AKSubmission/ak_list.html:6 AKSubmission/views.py:84
 msgid "All AKs"
 msgstr "Alle AKs"
 
@@ -409,74 +404,74 @@ msgstr ""
 msgid "Submit"
 msgstr "Eintragen"
 
-#: AKSubmission/views.py:73
+#: AKSubmission/views.py:127
 msgid "Wishes"
 msgstr "Wünsche"
 
-#: AKSubmission/views.py:73
+#: AKSubmission/views.py:127
 msgid "AKs one would like to have"
 msgstr ""
 "AKs die sich gewünscht wurden, aber bei denen noch nicht klar ist, wer sie "
 "macht. Falls du dir das vorstellen kannst, trag dich einfach ein"
 
-#: AKSubmission/views.py:93
+#: AKSubmission/views.py:167
 msgid "Currently planned AKs"
 msgstr "Aktuell geplante AKs"
 
-#: AKSubmission/views.py:186
+#: AKSubmission/views.py:300
 msgid "Event inactive. Cannot create or update."
 msgstr "Event inaktiv. Hinzufügen/Bearbeiten nicht möglich."
 
-#: AKSubmission/views.py:202
+#: AKSubmission/views.py:325
 msgid "AK successfully created"
 msgstr "AK erfolgreich angelegt"
 
-#: AKSubmission/views.py:252
+#: AKSubmission/views.py:398
 msgid "AK successfully updated"
 msgstr "AK erfolgreich aktualisiert"
 
-#: AKSubmission/views.py:290
+#: AKSubmission/views.py:449
 #, python-brace-format
 msgid "Added '{owner}' as new owner of '{ak.name}'"
 msgstr "'{owner}' als neue Leitung von '{ak.name}' hinzugefügt"
 
-#: AKSubmission/views.py:333
-msgid "Person Info successfully updated"
-msgstr "Personen-Info erfolgreich aktualisiert"
-
-#: AKSubmission/views.py:355
+#: AKSubmission/views.py:553
 msgid "No user selected"
 msgstr "Keine Person ausgewählt"
 
-#: AKSubmission/views.py:382
+#: AKSubmission/views.py:569
+msgid "Person Info successfully updated"
+msgstr "Personen-Info erfolgreich aktualisiert"
+
+#: AKSubmission/views.py:605
 msgid "AK Slot successfully added"
 msgstr "AK-Slot erfolgreich angelegt"
 
-#: AKSubmission/views.py:395
+#: AKSubmission/views.py:624
 msgid "You cannot edit a slot that has already been scheduled"
 msgstr "Bereits geplante AK-Slots können nicht mehr bearbeitet werden"
 
-#: AKSubmission/views.py:405
+#: AKSubmission/views.py:634
 msgid "AK Slot successfully updated"
 msgstr "AK-Slot erfolgreich aktualisiert"
 
-#: AKSubmission/views.py:417
+#: AKSubmission/views.py:652
 msgid "You cannot delete a slot that has already been scheduled"
 msgstr "Bereits geplante AK-Slots können nicht mehr gelöscht werden"
 
-#: AKSubmission/views.py:427
+#: AKSubmission/views.py:662
 msgid "AK Slot successfully deleted"
 msgstr "AK-Slot erfolgreich angelegt"
 
-#: AKSubmission/views.py:434
+#: AKSubmission/views.py:674
 msgid "Messages"
 msgstr "Nachrichten"
 
-#: AKSubmission/views.py:444
+#: AKSubmission/views.py:684
 msgid "Delete all messages"
 msgstr "Alle Nachrichten löschen"
 
-#: AKSubmission/views.py:467
+#: AKSubmission/views.py:711
 msgid "Message to organizers successfully saved"
 msgstr "Nachricht an die Organisator*innen erfolgreich gespeichert"
 
diff --git a/AKSubmission/models.py b/AKSubmission/models.py
index ef1bff7a11e17e483fed79316c0cc03ed143a982..a3a5f022ed22b77970dd300543c010dc310fb136 100644
--- a/AKSubmission/models.py
+++ b/AKSubmission/models.py
@@ -3,14 +3,15 @@ from django.conf import settings
 from django.core.mail import EmailMessage
 from django.db.models.signals import post_save
 from django.dispatch import receiver
-from django.urls import reverse_lazy
 
 from AKModel.models import AKOrgaMessage, AKSlot
 
 
 @receiver(post_save, sender=AKOrgaMessage)
-def orga_message_saved_handler(sender, instance: AKOrgaMessage, created, **kwargs):
-    # React to newly created Orga message by sending an email
+def orga_message_saved_handler(sender, instance: AKOrgaMessage, created, **kwargs): # pylint: disable=unused-argument
+    """
+    React to newly created Orga message by sending an email
+    """
 
     if created and settings.SEND_MAILS:
         host = 'https://' + settings.ALLOWED_HOSTS[0] if len(settings.ALLOWED_HOSTS) > 0 else 'http://127.0.0.1:8000'
@@ -26,10 +27,12 @@ def orga_message_saved_handler(sender, instance: AKOrgaMessage, created, **kwarg
 
 
 @receiver(post_save, sender=AKSlot)
-def slot_created_handler(sender, instance: AKSlot, created, **kwargs):
-    # React to slots that are created after the plan was already published by sending an email
-
-    if created and settings.SEND_MAILS and apps.is_installed("AKPlan") and not instance.event.plan_hidden and instance.room is None and instance.start is None:
+def slot_created_handler(sender, instance: AKSlot, created, **kwargs): # pylint: disable=unused-argument
+    """
+    React to slots that are created after the plan was already published by sending an email
+    """
+    if created and settings.SEND_MAILS and apps.is_installed("AKPlan") \
+            and not instance.event.plan_hidden and instance.room is None and instance.start is None: # pylint: disable=too-many-boolean-expressions,line-too-long
         host = 'https://' + settings.ALLOWED_HOSTS[0] if len(settings.ALLOWED_HOSTS) > 0 else 'http://127.0.0.1:8000'
         url = f"{host}{instance.ak.detail_url}"
 
diff --git a/AKSubmission/templatetags/__init__.py b/AKSubmission/templatetags/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
diff --git a/AKSubmission/templatetags/tags_AKSubmission.py b/AKSubmission/templatetags/tags_AKSubmission.py
index c1d42989a0c1ab931c2e3ee6746667297451abea..b68fe48a5ec3c929113162c223d1b08d8a049bc7 100644
--- a/AKSubmission/templatetags/tags_AKSubmission.py
+++ b/AKSubmission/templatetags/tags_AKSubmission.py
@@ -6,6 +6,11 @@ register = template.Library()
 
 @register.filter
 def bool_symbol(bool_val):
+    """
+    Show a nice icon instead of the string true/false
+    :param bool_val: boolean value to iconify
+    :return: check or times icon depending on the value
+    """
     if bool_val:
         return fa6_icon("check", "fas")
     return fa6_icon("times", "fas")
@@ -13,14 +18,34 @@ def bool_symbol(bool_val):
 
 @register.inclusion_tag("AKSubmission/tracks_list.html")
 def track_list(tracks, event_slug):
+    """
+    Generate a clickable list of tracks (one badge per track) based upon the tracks_list template
+
+    :param tracks: tracks to consider
+    :param event_slug: slug of this event, required for link creation
+    :return: html fragment containing track links
+    """
     return {"tracks": tracks, "event_slug": event_slug}
 
 
 @register.inclusion_tag("AKSubmission/category_list.html")
 def category_list(categories, event_slug):
+    """
+    Generate a clickable list of categories (one badge per category) based upon the category_list template
+
+    :param categories: categories to consider
+    :param event_slug: slug of this event, required for link creation
+    :return: html fragment containing category links
+    """
     return {"categories": categories, "event_slug": event_slug}
 
 
 @register.inclusion_tag("AKSubmission/category_linked_badge.html")
 def category_linked_badge(category, event_slug):
+    """
+    Generate a clickable category badge based upon the category_linked_badge template
+    :param category: category to show/link
+    :param event_slug: slug of this event, required for link creation
+    :return: html fragment containing badge
+    """
     return {"category": category, "event_slug": event_slug}
diff --git a/AKSubmission/tests.py b/AKSubmission/tests.py
index 54ff20a06dda9701362aa5d8017e8313cbf689b0..018289aae6f73b36e2f1f6a11b11c016ee357748 100644
--- a/AKSubmission/tests.py
+++ b/AKSubmission/tests.py
@@ -9,6 +9,15 @@ from AKModel.tests import BasicViewTests
 
 
 class ModelViewTests(BasicViewTests, TestCase):
+    """
+    Testcases for AKSubmission app.
+
+    This extends :class:`BasicViewTests` for standard view and edit testcases
+    that are specified in this class as VIEWS and EDIT_TESTCASES.
+
+    Additionally several additional testcases, in particular to test the API
+    and the dispatching for owner selection and editing are specified.
+    """
     fixtures = ['model.json']
 
     VIEWS = [
@@ -47,24 +56,27 @@ class ModelViewTests(BasicViewTests, TestCase):
         """
         self.client.logout()
 
-        view_name_with_prefix, url = self._name_and_url(('akslot_edit', {'event_slug': 'kif42', 'pk': 1}))
+        _, url = self._name_and_url(('akslot_edit', {'event_slug': 'kif42', 'pk': 1}))
         response = self.client.get(url)
         self.assertEqual(response.status_code, 302,
                          msg=f"AK Slot editing ({url}) possible even though slot was already scheduled")
         self._assert_message(response, "You cannot edit a slot that has already been scheduled")
 
-        view_name_with_prefix, url = self._name_and_url(('akslot_delete', {'event_slug': 'kif42', 'pk': 1}))
+        _, url = self._name_and_url(('akslot_delete', {'event_slug': 'kif42', 'pk': 1}))
         response = self.client.get(url)
         self.assertEqual(response.status_code, 302,
                          msg=f"AK Slot deletion ({url}) possible even though slot was already scheduled")
         self._assert_message(response, "You cannot delete a slot that has already been scheduled")
 
     def test_slot_creation_deletion(self):
+        """
+        Test creation and deletion of slots in frontend
+        """
         ak_args = {'event_slug': 'kif42', 'pk': 1}
         redirect_url = reverse_lazy(f"{self.APP_NAME}:ak_detail", kwargs=ak_args)
 
+        # Create a valid slot -> Redirect to AK detail page, message to user
         count_slots = AK.objects.get(pk=1).akslot_set.count()
-
         create_url = reverse_lazy(f"{self.APP_NAME}:akslot_add", kwargs=ak_args)
         response = self.client.post(create_url, {'ak': 1, 'event': 2, 'duration': 1.5})
         self.assertRedirects(response, redirect_url, status_code=302, target_status_code=200,
@@ -75,6 +87,8 @@ class ModelViewTests(BasicViewTests, TestCase):
         # Get primary key of newly created Slot
         slot_pk = AK.objects.get(pk=1).akslot_set.order_by('pk').last().pk
 
+        # Edit the recently created slot: Make sure view is accessible, post change
+        # -> redirect to detail page, duration updated
         edit_url = reverse_lazy(f"{self.APP_NAME}:akslot_edit", kwargs={'event_slug': 'kif42', 'pk': slot_pk})
         response = self.client.get(edit_url)
         self.assertEqual(response.status_code, 200, msg=f"Cant open edit view for newly created slot ({edit_url})")
@@ -84,6 +98,8 @@ class ModelViewTests(BasicViewTests, TestCase):
         self.assertEqual(AKSlot.objects.get(pk=slot_pk).duration, 2,
                          msg="Slot was not correctly changed")
 
+        # Delete recently created slot: Make sure view is accessible, post deletion
+        # -> redirect to detail page, slot deleted, message to user
         deletion_url = reverse_lazy(f"{self.APP_NAME}:akslot_delete", kwargs={'event_slug': 'kif42', 'pk': slot_pk})
         response = self.client.get(deletion_url)
         self.assertEqual(response.status_code, 200,
@@ -95,55 +111,77 @@ class ModelViewTests(BasicViewTests, TestCase):
         self.assertEqual(AK.objects.get(pk=1).akslot_set.count(), count_slots, msg="AK still has to many slots")
 
     def test_ak_owner_editing(self):
-        # Test editing of new user
+        """
+        Test dispatch of user editing requests
+        """
         edit_url = reverse_lazy(f"{self.APP_NAME}:akowner_edit_dispatch", kwargs={'event_slug': 'kif42'})
 
         base_url = reverse_lazy(f"{self.APP_NAME}:submission_overview", kwargs={'event_slug': 'kif42'})
+
+        # Empty form/no user selected -> start page
         response = self.client.post(edit_url, {'owner_id': -1})
         self.assertRedirects(response, base_url, status_code=302, target_status_code=200,
                              msg_prefix="Did not redirect to start page even though no user was selected")
         self._assert_message(response, "No user selected")
 
+        # Correct selection -> user edit page for that user
         edit_redirect_url = reverse_lazy(f"{self.APP_NAME}:akowner_edit", kwargs={'event_slug': 'kif42', 'slug': 'a'})
         response = self.client.post(edit_url, {'owner_id': 1})
         self.assertRedirects(response, edit_redirect_url, status_code=302, target_status_code=200,
                              msg_prefix=f"Dispatch redirect failed (should go to {edit_redirect_url})")
 
     def test_ak_owner_selection(self):
+        """
+        Test dispatch of owner selection requests
+        """
         select_url = reverse_lazy(f"{self.APP_NAME}:akowner_select", kwargs={'event_slug': 'kif42'})
-
         create_url = reverse_lazy(f"{self.APP_NAME}:akowner_create", kwargs={'event_slug': 'kif42'})
+
+        # Empty user selection -> create a new user view
         response = self.client.post(select_url, {'owner_id': -1})
         self.assertRedirects(response, create_url, status_code=302, target_status_code=200,
                              msg_prefix="Did not redirect to user create view even though no user was specified")
 
+        # Valid user selected -> redirect to view that allows to add a new AK with this user as owner
         add_redirect_url = reverse_lazy(f"{self.APP_NAME}:submit_ak", kwargs={'event_slug': 'kif42', 'owner_slug': 'a'})
         response = self.client.post(select_url, {'owner_id': 1})
         self.assertRedirects(response, add_redirect_url, status_code=302, target_status_code=200,
                     msg_prefix=f"Dispatch redirect to ak submission page failed (should go to {add_redirect_url})")
 
     def test_orga_message_submission(self):
+        """
+        Test submission and storing of direct confident messages to organizers
+        """
         form_url = reverse_lazy(f"{self.APP_NAME}:akmessage_add", kwargs={'event_slug': 'kif42', 'pk': 1})
         detail_url = reverse_lazy(f"{self.APP_NAME}:ak_detail", kwargs={'event_slug': 'kif42', 'pk': 1})
 
         count_messages = AK.objects.get(pk=1).akorgamessage_set.count()
 
+        # Test that submission view is accessible
         response = self.client.get(form_url)
         self.assertEqual(response.status_code, 200, msg="Could not load message form view")
+
+        # Test submission itself and the following redirect -> AK detail page
         response = self.client.post(form_url, {'ak': 1, 'event': 2, 'text': 'Test message text'})
         self.assertRedirects(response, detail_url, status_code=302, target_status_code=200,
                              msg_prefix=f"Did not trigger redirect to ak detail page ({detail_url})")
+
+        # Make sure message was correctly saved in database and user is notified about that
         self._assert_message(response, "Message to organizers successfully saved")
         self.assertEqual(AK.objects.get(pk=1).akorgamessage_set.count(), count_messages + 1,
                          msg="Message was not correctly saved")
 
     def test_interest_api(self):
+        """
+        Test interest indicating API (access, functionality)
+        """
         interest_api_url = "/kif42/api/ak/1/indicate-interest/"
 
         ak = AK.objects.get(pk=1)
         event = Event.objects.get(slug='kif42')
         ak_interest_counter = ak.interest_counter
 
+        # Check Access method (only POST)
         response = self.client.get(interest_api_url)
         self.assertEqual(response.status_code, 405, "Should not be accessible via GET")
 
@@ -151,6 +189,7 @@ class ModelViewTests(BasicViewTests, TestCase):
         event.interest_end = datetime.now().astimezone(event.timezone) + timedelta(minutes=+10)
         event.save()
 
+        # Test correct indication -> HTTP 200, counter increased
         response = self.client.post(interest_api_url)
         self.assertEqual(response.status_code, 200, f"API end point not working ({interest_api_url})")
         self.assertEqual(AK.objects.get(pk=1).interest_counter, ak_interest_counter + 1, "Counter was not increased")
@@ -158,30 +197,41 @@ class ModelViewTests(BasicViewTests, TestCase):
         event.interest_end = datetime.now().astimezone(event.timezone) + timedelta(minutes=-2)
         event.save()
 
+        # Test indication outside of indication window -> HTTP 403, counter not increased
         response = self.client.post(interest_api_url)
         self.assertEqual(response.status_code, 403,
                     "API end point still reachable even though interest indication window ended ({interest_api_url})")
         self.assertEqual(AK.objects.get(pk=1).interest_counter, ak_interest_counter + 1,
                          "Counter was increased even though interest indication window ended")
 
+        # Test call for non-existing AK -> HTTP 403
         invalid_interest_api_url = "/kif42/api/ak/-1/indicate-interest/"
         response = self.client.post(invalid_interest_api_url)
         self.assertEqual(response.status_code, 404, f"Invalid URL reachable ({interest_api_url})")
 
     def test_adding_of_unknown_user(self):
+        """
+        Test adding of a previously not existing owner to an AK
+        """
+        # Pre-Check: AK detail page existing?
         detail_url = reverse_lazy(f"{self.APP_NAME}:ak_detail", kwargs={'event_slug': 'kif42', 'pk': 1})
         response = self.client.get(detail_url)
         self.assertEqual(response.status_code, 200, msg="Could not load ak detail view")
 
+        # Make sure AK detail page contains a link to add a new owner
         edit_url = reverse_lazy(f"{self.APP_NAME}:ak_edit", kwargs={'event_slug': 'kif42', 'pk': 1})
         response = self.client.get(edit_url)
         self.assertEqual(response.status_code, 200, msg="Could not load ak detail view")
         self.assertContains(response, "Add person not in the list yet",
                             msg_prefix="Link to add unknown user not contained")
 
+        # Check adding of a new owner by posting an according request
+        # -> Redirect to AK detail page, message to user, owners list updated
         self.assertEqual(AK.objects.get(pk=1).owners.count(), 1)
-        add_new_user_to_ak_url = reverse_lazy(f"{self.APP_NAME}:akowner_create", kwargs={'event_slug': 'kif42'}) + f"?add_to_existing_ak=1"
-        response = self.client.post(add_new_user_to_ak_url, {'name': 'New test owner', 'event': Event.get_by_slug('kif42').pk})
+        add_new_user_to_ak_url = reverse_lazy(f"{self.APP_NAME}:akowner_create", kwargs={'event_slug': 'kif42'}) \
+                                 + "?add_to_existing_ak=1"
+        response = self.client.post(add_new_user_to_ak_url,
+                                    {'name': 'New test owner', 'event': Event.get_by_slug('kif42').pk})
         self.assertRedirects(response, detail_url,
                              msg_prefix=f"No correct redirect: {add_new_user_to_ak_url} (POST) -> {detail_url}")
         self._assert_message(response, "Added 'New test owner' as new owner of 'Test AK Inhalt'")
diff --git a/AKSubmission/views.py b/AKSubmission/views.py
index 3c7fd8e93add9e1a34307ebd7faf594f91ce9eba..e7940a01c0d97a9fba0118265efe3fc9cdd9f84e 100644
--- a/AKSubmission/views.py
+++ b/AKSubmission/views.py
@@ -1,5 +1,6 @@
 from datetime import timedelta
 from math import floor
+from abc import ABC, abstractmethod
 
 from django.apps import apps
 from django.conf import settings
@@ -23,27 +24,74 @@ from AKSubmission.forms import AKWishForm, AKOwnerForm, AKSubmissionForm, AKDura
 
 
 class SubmissionErrorNotConfiguredView(EventSlugMixin, TemplateView):
+    """
+    View to show when submission is not correctly configured yet for this event
+    and hence the submission component cannot be used already.
+    """
     template_name = "AKSubmission/submission_not_configured.html"
 
 
 class AKOverviewView(FilterByEventSlugMixin, ListView):
+    """
+    View: Show a tabbed list of AKs belonging to this event split by categories
+
+    Wishes show up in between of the other AKs in the category they belong to.
+    In contrast to :class:`SubmissionOverviewView` that inherits from this view,
+    on this view there is no form to add new AKs or edit owners.
+
+    Since the inherited version of this view will have a slightly different behaviour,
+    this view contains multiple methods that can be overriden for this adaption.
+    """
     model = AKCategory
     context_object_name = "categories"
     template_name = "AKSubmission/ak_overview.html"
     wishes_as_category = False
 
-    def filter_aks(self, context, category):
+    def filter_aks(self, context, category): # pylint: disable=unused-argument
+        """
+        Filter which AKs to display based on the given context and category
+
+        In the default case, all AKs of that category are returned (including wishes)
+
+        :param context: context of the view
+        :param category: category to filter the AK list for
+        :return: filtered list of AKs for the given category
+        :rtype: QuerySet[AK]
+        """
+        # Use prefetching and relation selection/joining to reduce the amount of necessary queries
         return category.ak_set.select_related('event').prefetch_related('owners').all()
 
     def get_active_category_name(self, context):
+        """
+        Get the category name to display by default/before further user interaction
+
+        In the default case, simply the first category (the one with the lowest ID for this event) is used
+
+        :param context: context of the view
+        :return: name of the default category
+        :rtype: str
+        """
         return context["categories_with_aks"][0][0].name
 
-    def get_table_title(self, context):
+    def get_table_title(self, context): # pylint: disable=unused-argument
+        """
+        Specify the title above the AK list/table in this view
+
+        :param context: context of the view
+        :return: title to use
+        :rtype: str
+        """
         return _("All AKs")
 
     def get(self, request, *args, **kwargs):
+        """
+        Handle GET request
+
+        Overriden to allow checking for correct configuration and
+        redirect to error page if necessary (see :class:`SubmissionErrorNotConfiguredView`)
+        """
         self._load_event()
-        self.object_list = self.get_queryset()
+        self.object_list = self.get_queryset() # pylint: disable=attribute-defined-outside-init
 
         # No categories yet? Redirect to configuration error page
         if self.object_list.count() == 0:
@@ -55,10 +103,16 @@ class AKOverviewView(FilterByEventSlugMixin, ListView):
     def get_context_data(self, *, object_list=None, **kwargs):
         context = super().get_context_data(object_list=object_list, **kwargs)
 
+        # ==========================================================
         # Sort AKs into different lists (by their category)
+        # ==========================================================
         ak_wishes = []
         categories_with_aks = []
 
+        # Loop over categories, load AKs (while filtering them if necessary) and create a list of (category, aks)-tuples
+        # Depending on the setting of self.wishes_as_category, wishes are either included
+        # or added to a special "Wish"-Category that is created on-the-fly to provide consistent handling in the
+        # template (without storing it in the database)
         for category in context["categories"]:
             aks_for_category = []
             for ak in self.filter_aks(context, category):
@@ -76,7 +130,9 @@ class AKOverviewView(FilterByEventSlugMixin, ListView):
         context["active_category"] = self.get_active_category_name(context)
         context['table_title'] = self.get_table_title(context)
 
+        # ==========================================================
         # Display interest indication button?
+        # ==========================================================
         current_timestamp = datetime.now().astimezone(self.event.timezone)
         context['interest_indication_active'] = ak_interest_indication_active(self.event, current_timestamp)
 
@@ -84,12 +140,30 @@ class AKOverviewView(FilterByEventSlugMixin, ListView):
 
 
 class SubmissionOverviewView(AKOverviewView):
+    """
+    View: List of AKs and possibility to add AKs or adapt owner information
+
+    Main/start view of the component.
+
+    This view inherits from :class:`AKOverviewView`, but treats wishes as separate category if requested in the settings
+    and handles the change actions mentioned above.
+    """
     model = AKCategory
     context_object_name = "categories"
     template_name = "AKSubmission/submission_overview.html"
+
+    # this mainly steers the different handling of wishes
+    # since the code for that is already included in the parent class
     wishes_as_category = settings.WISHES_AS_CATEGORY
 
     def get_table_title(self, context):
+        """
+        Specify the title above the AK list/table in this view
+
+        :param context: context of the view
+        :return: title to use
+        :rtype: str
+        """
         return _("Currently planned AKs")
 
     def get_context_data(self, *, object_list=None, **kwargs):
@@ -102,32 +176,71 @@ class SubmissionOverviewView(AKOverviewView):
 
 
 class AKListByCategoryView(AKOverviewView):
+    """
+    View: List of only the AKs belonging to a certain category.
+
+    This view inherits from :class:`AKOverviewView`, but produces only one list instead of a tabbed one.
+    """
     def dispatch(self, request, *args, **kwargs):
-        self.category = get_object_or_404(AKCategory, pk=kwargs['category_pk'])
+        # Override dispatching
+        # Needed to handle the checking whether the category exists
+        self.category = get_object_or_404(AKCategory, pk=kwargs['category_pk']) # pylint: disable=attribute-defined-outside-init,line-too-long
         return super().dispatch(request, *args, **kwargs)
 
     def get_active_category_name(self, context):
+        """
+        Get the category name to display by default/before further user interaction
+
+        In this case, this will be the name of the category specified via pk
+
+        :param context: context of the view
+        :return: name of the category
+        :rtype: str
+        """
         return self.category.name
 
 
 class AKListByTrackView(AKOverviewView):
+    """
+    View: List of only the AKs belonging to a certain track.
+
+    This view inherits from :class:`AKOverviewView` and there will be one list per category
+    -- but only AKs of a certain given track will be included in them.
+    """
     def dispatch(self, request, *args, **kwargs):
-        self.track = get_object_or_404(AKTrack, pk=kwargs['track_pk'])
+        # Override dispatching
+        # Needed to handle the checking whether the track exists
+
+        self.track = get_object_or_404(AKTrack, pk=kwargs['track_pk']) # pylint: disable=attribute-defined-outside-init
         return super().dispatch(request, *args, **kwargs)
 
     def filter_aks(self, context, category):
-        return category.ak_set.filter(track=self.track)
+        """
+        Filter which AKs to display based on the given context and category
+
+        In this case, the list is further restricted by the track
+
+        :param context: context of the view
+        :param category: category to filter the AK list for
+        :return: filtered list of AKs for the given category
+        :rtype: QuerySet[AK]
+        """
+        return super().filter_aks(context, category).filter(track=self.track)
 
     def get_table_title(self, context):
         return f"{_('AKs with Track')} = {self.track.name}"
 
 
 class AKDetailView(EventSlugMixin, DetailView):
+    """
+    View: AK Details
+    """
     model = AK
     context_object_name = "ak"
     template_name = "AKSubmission/ak_detail.html"
 
     def get_queryset(self):
+        # Get information about the AK and do some query optimization
         return super().get_queryset().select_related('event').prefetch_related('owners')
 
     def get_context_data(self, *, object_list=None, **kwargs):
@@ -163,29 +276,34 @@ class AKDetailView(EventSlugMixin, DetailView):
 
 
 class AKHistoryView(EventSlugMixin, DetailView):
+    """
+    View: Show history of a given AK
+    """
     model = AK
     context_object_name = "ak"
     template_name = "AKSubmission/ak_history.html"
 
 
-class AKListView(FilterByEventSlugMixin, ListView):
-    model = AK
-    context_object_name = "AKs"
-    template_name = "AKSubmission/ak_overview.html"
-    table_title = ""
-
-    def get_context_data(self, *, object_list=None, **kwargs):
-        context = super().get_context_data(object_list=object_list, **kwargs)
-        context['categories'] = AKCategory.objects.filter(event=self.event)
-        context['tracks'] = AKTrack.objects.filter(event=self.event)
-        return context
-
-
 class EventInactiveRedirectMixin:
+    """
+    Mixin that will cause a redirect when actions are performed on an inactive event.
+    Will add a message explaining why the action was not performed to the user
+    and then redirect to start page of the submission component
+    """
     def get_error_message(self):
+        """
+        Error message to display after redirect (can be adjusted by this method)
+
+        :return: error message
+        :rtype: str
+        """
         return _("Event inactive. Cannot create or update.")
 
     def get(self, request, *args, **kwargs):
+        """
+        Override GET request handling
+        Will either perform the redirect including the message creation or continue with the planned dispatching
+        """
         s = super().get(request, *args, **kwargs)
         if not self.event.active:
             messages.add_message(self.request, messages.ERROR, self.get_error_message())
@@ -194,6 +312,11 @@ class EventInactiveRedirectMixin:
 
 
 class AKAndAKWishSubmissionView(EventSlugMixin, EventInactiveRedirectMixin, CreateView):
+    """
+    View: Submission form for AKs and Wishes
+
+    Base view, will be used by :class:`AKSubmissionView` and :class:`AKWishSubmissionView`
+    """
     model = AK
     template_name = 'AKSubmission/submit_new.html'
     form_class = AKSubmissionForm
@@ -221,7 +344,14 @@ class AKAndAKWishSubmissionView(EventSlugMixin, EventInactiveRedirectMixin, Crea
 
 
 class AKSubmissionView(AKAndAKWishSubmissionView):
+    """
+    View: AK submission form
+
+    Extends :class:`AKAndAKWishSubmissionView`
+    """
     def get_initial(self):
+        # Load initial values for the form
+        # Used to directly add the first owner and the event this AK will belong to
         initials = super(AKAndAKWishSubmissionView, self).get_initial()
         initials['owners'] = [AKOwner.get_by_slug(self.event, self.kwargs['owner_slug'])]
         initials['event'] = self.event
@@ -234,33 +364,54 @@ class AKSubmissionView(AKAndAKWishSubmissionView):
 
 
 class AKWishSubmissionView(AKAndAKWishSubmissionView):
+    """
+    View: Wish submission form
+
+    Extends :class:`AKAndAKWishSubmissionView`
+    """
     template_name = 'AKSubmission/submit_new_wish.html'
     form_class = AKWishForm
 
     def get_initial(self):
+        # Load initial values for the form
+        # Used to directly select the event this AK will belong to
         initials = super(AKAndAKWishSubmissionView, self).get_initial()
         initials['event'] = self.event
         return initials
 
 
 class AKEditView(EventSlugMixin, EventInactiveRedirectMixin, UpdateView):
+    """
+    View: Update an AK
+
+    This allows to change most fields of an AK as specified in :class:`AKSubmission.forms.AKForm`,
+    including the availabilities.
+    It will also handle the change from AK to wish and vice versa (triggered by adding or removing owners)
+    and automatically create or delete (unscheduled) slots
+    """
     model = AK
     template_name = 'AKSubmission/ak_edit.html'
     form_class = AKForm
 
     def get_success_url(self):
+        # Redirection after successfully saving to detail page of AK where also a success message is displayed
         messages.add_message(self.request, messages.SUCCESS, _("AK successfully updated"))
         return self.object.detail_url
 
     def form_valid(self, form):
+        # Handle valid form submission
+
+        # Only save when event is active, otherwise redirect
         if not form.cleaned_data["event"].active:
             messages.add_message(self.request, messages.ERROR, self.get_error_message())
             return redirect(reverse_lazy('submit:submission_overview',
                                          kwargs={'event_slug': form.cleaned_data["event"].slug}))
 
+        # Remember owner count before saving to know whether the AK changed its state between AK and wish
         previous_owner_count = self.object.owners.count()
 
-        super_form_valid = super().form_valid(form)
+        # Perform saving and redirect handling by calling default/parent implementation of form_valid
+        redirect_response = super().form_valid(form)
 
         # Did this AK change from wish to AK or vice versa?
         new_owner_count = self.object.owners.count()
@@ -273,15 +424,23 @@ class AKEditView(EventSlugMixin, EventInactiveRedirectMixin, UpdateView):
             # Delete all unscheduled slots
             self.object.akslot_set.filter(start__isnull=True).delete()
 
-        return super_form_valid
+        # Redirect to success url
+        return redirect_response
 
 
 class AKOwnerCreateView(EventSlugMixin, EventInactiveRedirectMixin, CreateView):
+    """
+    View: Create a new owner
+    """
     model = AKOwner
     template_name = 'AKSubmission/akowner_create_update.html'
     form_class = AKOwnerForm
 
     def get_success_url(self):
+        # The redirect url depends on the source this view was called from:
+
+        # Called from an existing AK? Add the new owner as an owner of that AK, notify the user and redirect to detail
+        # page of that AK
         if "add_to_existing_ak" in self.request.GET:
             ak_pk = self.request.GET['add_to_existing_ak']
             ak = get_object_or_404(AK, pk=ak_pk)
@@ -289,15 +448,20 @@ class AKOwnerCreateView(EventSlugMixin, EventInactiveRedirectMixin, CreateView):
             messages.add_message(self.request, messages.SUCCESS,
                                  _("Added '{owner}' as new owner of '{ak.name}'").format(owner=self.object, ak=ak))
             return ak.detail_url
+
+        # Called from the submission overview? Offer the user to create a new AK with the recently created owner
+        # prefilled as owner of that AK in the creation form
         return reverse_lazy('submit:submit_ak',
                             kwargs={'event_slug': self.kwargs['event_slug'], 'owner_slug': self.object.slug})
 
     def get_initial(self):
-        initials = super(AKOwnerCreateView, self).get_initial()
+        # Set the event in the (hidden) event field in the form based on the URL this view was called with
+        initials = super().get_initial()
         initials['event'] = self.event
         return initials
 
     def form_valid(self, form):
+        # Prevent changes if event is not active
         if not form.cleaned_data["event"].active:
             messages.add_message(self.request, messages.ERROR, self.get_error_message())
             return redirect(reverse_lazy('submit:submission_overview',
@@ -305,29 +469,98 @@ class AKOwnerCreateView(EventSlugMixin, EventInactiveRedirectMixin, CreateView):
         return super().form_valid(form)
 
 
-class AKOwnerSelectDispatchView(EventSlugMixin, View):
+class AKOwnerDispatchView(ABC, EventSlugMixin, View):
     """
-    This view only serves as redirect to prepopulate the owners field in submission create view
+    Base view: Dispatch to correct view based upon
+
+    Will be used by :class:`AKOwnerSelectDispatchView` and :class:`AKOwnerEditDispatchView` to handle button clicks for
+    "New AK" and "Edit Person Info" in submission overview based upon the selection in the owner dropdown field
     """
 
+    @abstractmethod
+    def get_new_owner_redirect(self, event_slug):
+        """
+        Get redirect when user selected "I do not own AKs yet"
+
+        :param event_slug: slug of the event, needed for constructing redirect
+        :return: redirect to perform
+        :rtype: HttpResponseRedirect
+        """
+
+    @abstractmethod
+    def get_valid_owner_redirect(self, event_slug, owner):
+        """
+        Get redirect when user selected "I do not own AKs yet"
+
+        :param event_slug: slug of the event, needed for constructing redirect
+        :param owner: owner to perform the dispatching for
+        :return: redirect to perform
+        :rtype: HttpResponseRedirect
+        """
+
+
     def post(self, request, *args, **kwargs):
+        # This view is solely meant to handle POST requests
+        # Perform dispatching based on the submitted owner_id
+
+        # No owner_id? Redirect to submission overview view
         if "owner_id" not in request.POST:
             return redirect('submit:submission_overview', event_slug=kwargs['event_slug'])
         owner_id = request.POST["owner_id"]
 
+        # Special owner_id "-1" (value of "I do not own AKs yet)? Redirect to owner creation view
         if owner_id == "-1":
-            return HttpResponseRedirect(
-                reverse_lazy('submit:akowner_create', kwargs={'event_slug': kwargs['event_slug']}))
+            return self.get_new_owner_redirect(kwargs['event_slug'])
 
+        # Normal owner_id given? Check vor validity and redirect to AK submission page with that owner prefilled
+        # or display a 404 error page if no owner for the given id can be found. The latter should only happen when the
+        # user manipulated the value before sending or when the owner was deleted in backend and the user did not
+        # reload the dropdown between deletion and sending the dispatch request
         owner = get_object_or_404(AKOwner, pk=request.POST["owner_id"])
-        return HttpResponseRedirect(
-            reverse_lazy('submit:submit_ak', kwargs={'event_slug': kwargs['event_slug'], 'owner_slug': owner.slug}))
+        return self.get_valid_owner_redirect(kwargs['event_slug'], owner)
 
     def get(self, request, *args, **kwargs):
+        # This view should never be called with GET, perform a redirect to overview in that case
         return redirect('submit:submission_overview', event_slug=kwargs['event_slug'])
 
 
-class AKOwnerEditView(FilterByEventSlugMixin, EventSlugMixin, UpdateView):
+class AKOwnerSelectDispatchView(AKOwnerDispatchView):
+    """
+    View: Handle submission from the owner selection dropdown in submission overview for AK creation
+    ("New AK" button)
+
+    This view will perform redirects depending on the selection in the owner dropdown field.
+    Based upon the abstract base view :class:`AKOwnerDispatchView`.
+    """
+
+    def get_new_owner_redirect(self, event_slug):
+        return redirect('submit:akowner_create', event_slug=event_slug)
+
+    def get_valid_owner_redirect(self, event_slug, owner):
+        return redirect('submit:submit_ak', event_slug=event_slug, owner_slug=owner.slug)
+
+
+class AKOwnerEditDispatchView(AKOwnerDispatchView):
+    """
+    View: Handle submission from the owner selection dropdown in submission overview for owner editing
+    ("Edit Person Info" button)
+
+    This view will perform redirects depending on the selection in the owner dropdown field.
+    Based upon the abstract base view :class:`AKOwnerDispatchView`.
+    """
+
+    def get_new_owner_redirect(self, event_slug):
+        messages.add_message(self.request, messages.WARNING, _("No user selected"))
+        return redirect('submit:submission_overview', event_slug)
+
+    def get_valid_owner_redirect(self, event_slug, owner):
+        return redirect('submit:akowner_edit', event_slug=event_slug, slug=owner.slug)
+
+
+class AKOwnerEditView(FilterByEventSlugMixin, EventSlugMixin, EventInactiveRedirectMixin, UpdateView):
+    """
+    View: Edit an owner
+    """
     model = AKOwner
     template_name = "AKSubmission/akowner_create_update.html"
     form_class = AKOwnerForm
@@ -337,6 +570,7 @@ class AKOwnerEditView(FilterByEventSlugMixin, EventSlugMixin, UpdateView):
         return reverse_lazy('submit:submission_overview', kwargs={'event_slug': self.kwargs['event_slug']})
 
     def form_valid(self, form):
+        # Prevent updating if event is not active
         if not form.cleaned_data["event"].active:
             messages.add_message(self.request, messages.ERROR, self.get_error_message())
             return redirect(reverse_lazy('submit:submission_overview',
@@ -344,36 +578,19 @@ class AKOwnerEditView(FilterByEventSlugMixin, EventSlugMixin, UpdateView):
         return super().form_valid(form)
 
 
-class AKOwnerEditDispatchView(EventSlugMixin, View):
-    """
-    This view only serves as redirect choose the correct edit view
+class AKSlotAddView(EventSlugMixin, EventInactiveRedirectMixin, CreateView):
     """
+    View: Add an additional slot to an AK
+    The user has to select the duration of the slot in this view
 
-    def post(self, request, *args, **kwargs):
-        if "owner_id" not in request.POST:
-            return redirect('submit:submission_overview', event_slug=kwargs['event_slug'])
-        owner_id = request.POST["owner_id"]
-
-        if owner_id == "-1":
-            messages.add_message(self.request, messages.WARNING, _("No user selected"))
-            return HttpResponseRedirect(
-                reverse_lazy('submit:submission_overview', kwargs={'event_slug': kwargs['event_slug']}))
-
-        owner = get_object_or_404(AKOwner, pk=request.POST["owner_id"])
-        return HttpResponseRedirect(
-            reverse_lazy('submit:akowner_edit', kwargs={'event_slug': kwargs['event_slug'], 'slug': owner.slug}))
-
-    def get(self, request, *args, **kwargs):
-        return redirect('submit:submission_overview', event_slug=kwargs['event_slug'])
-
-
-class AKSlotAddView(EventSlugMixin, EventInactiveRedirectMixin, CreateView):
+    The view will only process the request when the event is active (as steered by :class:`EventInactiveRedirectMixin`)
+    """
     model = AKSlot
     form_class = AKDurationForm
     template_name = "AKSubmission/akslot_add_update.html"
 
     def get_initial(self):
-        initials = super(AKSlotAddView, self).get_initial()
+        initials = super().get_initial()
         initials['event'] = self.event
         initials['ak'] = get_object_or_404(AK, pk=self.kwargs['pk'])
         initials['duration'] = self.event.default_slot
@@ -390,6 +607,12 @@ class AKSlotAddView(EventSlugMixin, EventInactiveRedirectMixin, CreateView):
 
 
 class AKSlotEditView(EventSlugMixin, EventInactiveRedirectMixin, UpdateView):
+    """
+    View: Update the duration of an AK slot
+
+    The view will only process the request when the event is active (as steered by :class:`EventInactiveRedirectMixin`)
+    and only slots that are not scheduled yet may be changed
+    """
     model = AKSlot
     form_class = AKDurationForm
     template_name = "AKSubmission/akslot_add_update.html"
@@ -413,6 +636,12 @@ class AKSlotEditView(EventSlugMixin, EventInactiveRedirectMixin, UpdateView):
 
 
 class AKSlotDeleteView(EventSlugMixin, EventInactiveRedirectMixin, DeleteView):
+    """
+    View: Delete an AK slot
+
+    The view will only process the request when the event is active (as steered by :class:`EventInactiveRedirectMixin`)
+    and only slots that are not scheduled yet may be deleted
+    """
     model = AKSlot
     template_name = "AKSubmission/akslot_delete.html"
 
@@ -436,6 +665,11 @@ class AKSlotDeleteView(EventSlugMixin, EventInactiveRedirectMixin, DeleteView):
 
 @status_manager.register(name="event_ak_messages")
 class EventAKMessagesWidget(TemplateStatusWidget):
+    """
+    Status page widget: AK Messages
+
+    A widget to display information about AK-related messages sent to organizers for the given event
+    """
     required_context_type = "event"
     title = _("Messages")
     template_name = "admin/AKModel/render_ak_messages.html"
@@ -454,12 +688,16 @@ class EventAKMessagesWidget(TemplateStatusWidget):
 
 
 class AKAddOrgaMessageView(EventSlugMixin, CreateView):
+    """
+    View: Form to create a (confidential) message to the organizers as defined in
+    :class:`AKSubmission.forms.AKOrgaMessageForm`
+    """
     model = AKOrgaMessage
     form_class = AKOrgaMessageForm
     template_name = "AKSubmission/akmessage_add.html"
 
     def get_initial(self):
-        initials = super(AKAddOrgaMessageView, self).get_initial()
+        initials = super().get_initial()
         initials['ak'] = get_object_or_404(AK, pk=self.kwargs['pk'])
         initials['event'] = initials['ak'].event
         return initials
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 900a9753f97b6564abe8d433c652d21ac5437d35..44a6354fdca65e4203db1b0301f5536c6d3b296e 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -61,7 +61,7 @@ Provide more context by answering these questions:
 
 Include details about your configuration and environment:
 
-* **Which version (commit)) are you using?**
+* **Which version (commit) are you using?**
 * **What's the OS you're using**?
 
 ### Suggesting Enhancements
diff --git a/Utils/setup.sh b/Utils/setup.sh
index 7993082cbff409f7bbae515b6e8d3b18edef09fd..1c951824905e99e4650f40a562b633b92d7b8b02 100755
--- a/Utils/setup.sh
+++ b/Utils/setup.sh
@@ -15,6 +15,14 @@ source venv/bin/activate
 pip install --upgrade setuptools pip wheel
 pip install -r requirements.txt
 
+# set environment variable when we want to update in production
+if [ "$1" = "--prod" ]; then
+    export DJANGO_SETTINGS_MODULE=AKPlanning.settings_production
+fi
+if [ "$1" = "--ci" ]; then
+    export DJANGO_SETTINGS_MODULE=AKPlanning.settings_ci
+fi
+
 # Setup database
 python manage.py migrate
 
@@ -26,4 +34,12 @@ python manage.py compilemessages -l de_DE
 # Credentials are entered interactively on CLI
 python manage.py createsuperuser
 
+# Generate documentation (but not for CI use)
+if [ -n "$1" = "--ci" ]; then
+    cd docs
+    make html
+    cd ..
+fi
+
+
 deactivate
diff --git a/Utils/update.sh b/Utils/update.sh
index 780baef2ff52ef2a8d96ffb291f900900acdb048..a711b73b912fcd07e8543b6b49d4eb8950ff0d79 100755
--- a/Utils/update.sh
+++ b/Utils/update.sh
@@ -27,4 +27,10 @@ pip install --upgrade -r requirements.txt
 ./manage.py collectstatic --noinput
 ./manage.py compilemessages -l de_DE
 
+
+# Update documentation
+cd docs
+make html
+cd ..
+
 touch AKPlanning/wsgi.py
diff --git a/docs/.gitignore b/docs/.gitignore
new file mode 100644
index 0000000000000000000000000000000000000000..6c87b539c1a92836fcc92060af8b8fe4ce6192ed
--- /dev/null
+++ b/docs/.gitignore
@@ -0,0 +1,2 @@
+_build/
+code/
diff --git a/docs/Makefile b/docs/Makefile
new file mode 100644
index 0000000000000000000000000000000000000000..d4bb2cbb9eddb1bb1b4f366623044af8e4830919
--- /dev/null
+++ b/docs/Makefile
@@ -0,0 +1,20 @@
+# Minimal makefile for Sphinx documentation
+#
+
+# You can set these variables from the command line, and also
+# from the environment for the first two.
+SPHINXOPTS    ?=
+SPHINXBUILD   ?= sphinx-build
+SOURCEDIR     = .
+BUILDDIR      = _build
+
+# Put it first so that "make" without argument is like "make help".
+help:
+	@$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
+
+.PHONY: help Makefile
+
+# Catch-all target: route all unknown targets to Sphinx using the new
+# "make mode" option.  $(O) is meant as a shortcut for $(SPHINXOPTS).
+%: Makefile
+	@$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
diff --git a/docs/code.rst b/docs/code.rst
new file mode 100644
index 0000000000000000000000000000000000000000..690352ce8aa1a395632029f1369e373be4a7b8f9
--- /dev/null
+++ b/docs/code.rst
@@ -0,0 +1,10 @@
+Code
+=====
+
+.. toctree::
+    code/AKDashboard
+    code/AKModel
+    code/AKOnline
+    code/AKPlan
+    code/AKScheduling
+    code/AKSubmission
diff --git a/docs/conf.py b/docs/conf.py
new file mode 100644
index 0000000000000000000000000000000000000000..f47181771f7cefca631b81f4bd0dd0542a5c92f0
--- /dev/null
+++ b/docs/conf.py
@@ -0,0 +1,83 @@
+# Configuration file for the Sphinx documentation builder.
+#
+# For the full list of built-in configuration values, see the documentation:
+# https://www.sphinx-doc.org/en/master/usage/configuration.html
+import os
+import sys
+from recommonmark.parser import CommonMarkParser
+import django
+
+# -- Project information -----------------------------------------------------
+# https://www.sphinx-doc.org/en/master/usage/configuration.html#project-information
+
+project = 'AK Planning'
+copyright = '2023, N. Geisler, B. Hättasch & more'
+author = 'N. Geisler, B. Hättasch & more'
+
+# -- General configuration ---------------------------------------------------
+# https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration
+
+extensions = [
+    'sphinxcontrib.apidoc',   # runs sphinx-apidoc automatically as part of sphinx-build
+    'sphinx.ext.autodoc',     # the autodoc extensions uses files generated by apidoc
+    "sphinx.ext.autosummary",
+    "sphinxcontrib_django",
+    'sphinx.ext.viewcode',    # enable viewing autodoc'd code
+]
+
+templates_path = ['_templates']
+exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store']
+
+# -- Django specific settings ------------------------------------------------
+
+# Add source directory to sys.path
+sys.path.insert(0, os.path.abspath(".."))
+
+# Configure the path to the Django settings module
+django_settings = "AKPlanning.settings"
+os.environ['DJANGO_SETTINGS_MODULE'] = django_settings
+
+django.setup()
+
+# Include the database table names of Django models
+django_show_db_tables = True                # Boolean, default: False
+# Add abstract database tables names (only takes effect if django_show_db_tables is True)
+django_show_db_tables_abstract = True       # Boolean, default: False
+
+# Auto-generate API documentation.
+os.environ['SPHINX_APIDOC_OPTIONS'] = "members,show-inheritance"
+
+# -- Input ----
+
+source_parsers = {
+    '.md': CommonMarkParser,
+}
+
+source_suffix = ['.rst', '.md']
+
+# -- Extension Conf ----
+
+autodoc_member_order = 'bysource'
+autodoc_inherit_docstrings = False
+
+apidoc_module_dir = '../'
+apidoc_output_dir = 'code'
+apidoc_excluded_paths = ['*/migrations',
+                         'AKPlanning/',
+                         'manage.py',
+                         'docs',
+                         'locale',
+                         'Utils',
+                         '*/urls.py',
+                         ]
+apidoc_separate_modules = True
+apidoc_toc_file = False
+apidoc_module_first = True
+apidoc_extra_args = ['-f']
+apidoc_project = project
+
+# -- Options for HTML output -------------------------------------------------
+# https://www.sphinx-doc.org/en/master/usage/configuration.html#options-for-html-output
+
+html_static_path = ['_static']
+html_theme = 'sphinx_rtd_theme'
diff --git a/docs/index.rst b/docs/index.rst
new file mode 100644
index 0000000000000000000000000000000000000000..50fb2772421632f7195696de5f37f7810b75a11f
--- /dev/null
+++ b/docs/index.rst
@@ -0,0 +1,22 @@
+.. AK Planning documentation master file, created by
+   sphinx-quickstart on Wed Jun 21 09:54:11 2023.
+   You can adapt this file completely to your liking, but it should at least
+   contain the root `toctree` directive.
+
+Start
+=======================================
+
+.. toctree::
+   :maxdepth: 2
+   :caption: Contents:
+
+   usage/usage
+   code
+
+
+Indices and tables
+==================
+
+* :ref:`genindex`
+* :ref:`modindex`
+* :ref:`search`
diff --git a/docs/make.bat b/docs/make.bat
new file mode 100644
index 0000000000000000000000000000000000000000..954237b9b9f2b248bb1397a15c055c0af1cad03e
--- /dev/null
+++ b/docs/make.bat
@@ -0,0 +1,35 @@
+@ECHO OFF
+
+pushd %~dp0
+
+REM Command file for Sphinx documentation
+
+if "%SPHINXBUILD%" == "" (
+	set SPHINXBUILD=sphinx-build
+)
+set SOURCEDIR=.
+set BUILDDIR=_build
+
+%SPHINXBUILD% >NUL 2>NUL
+if errorlevel 9009 (
+	echo.
+	echo.The 'sphinx-build' command was not found. Make sure you have Sphinx
+	echo.installed, then set the SPHINXBUILD environment variable to point
+	echo.to the full path of the 'sphinx-build' executable. Alternatively you
+	echo.may add the Sphinx directory to PATH.
+	echo.
+	echo.If you don't have Sphinx installed, grab it from
+	echo.https://www.sphinx-doc.org/
+	exit /b 1
+)
+
+if "%1" == "" goto help
+
+%SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O%
+goto end
+
+:help
+%SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O%
+
+:end
+popd
diff --git a/docs/usage/usage.rst b/docs/usage/usage.rst
new file mode 100644
index 0000000000000000000000000000000000000000..e29d849e91999e2c3dea1a9ba8f8aa080b5f7ba5
--- /dev/null
+++ b/docs/usage/usage.rst
@@ -0,0 +1,5 @@
+Usage
+=====
+
+.. toctree::
+
diff --git a/locale/de_DE/LC_MESSAGES/django.po b/locale/de_DE/LC_MESSAGES/django.po
index 37a585fa79adb6a71571aa740a3b0dd52d8056cd..c1bbd2db4e5c22f703d2acc6a7388eb37116792b 100644
--- a/locale/de_DE/LC_MESSAGES/django.po
+++ b/locale/de_DE/LC_MESSAGES/django.po
@@ -8,7 +8,7 @@ msgid ""
 msgstr ""
 "Project-Id-Version: PACKAGE VERSION\n"
 "Report-Msgid-Bugs-To: \n"
-"POT-Creation-Date: 2023-05-15 20:05+0200\n"
+"POT-Creation-Date: 2023-08-16 16:30+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"
@@ -27,11 +27,15 @@ msgstr "Wirklich jetzt die Sprache ändern? Das wird das Formular zurücksetzen!
 msgid "Go to backend"
 msgstr "Zum Backend"
 
-#: templates/base.html:114
+#: templates/base.html:109
+msgid "Docs"
+msgstr "Doku"
+
+#: templates/base.html:115
 msgid "Impress"
 msgstr "Impressum"
 
-#: templates/base.html:117
+#: templates/base.html:118
 msgid "This software is open source"
 msgstr "Diese Software ist Open Source"
 
diff --git a/pylintrc b/pylintrc
new file mode 100644
index 0000000000000000000000000000000000000000..cb836685ac68b0b0aac04fd8574c9ac5d9dfb300
--- /dev/null
+++ b/pylintrc
@@ -0,0 +1,76 @@
+[MAIN]
+
+ignore=urls.py, migrations, AKPlanning
+
+load-plugins=
+    pylint_django,
+    pylint_django.checkers.migrations
+
+django-settings-module=AKPlanning.settings
+
+disable=
+    C0114, # missing-module-docstring
+
+
+[FORMAT]
+
+# Maximum number of characters on a single line.
+max-line-length=120
+
+indent-string='    '
+
+
+
+[SIMILARITIES]
+
+# Minimum lines number of a similarity.
+min-similarity-lines=6
+
+# Ignore comments when computing similarities.
+ignore-comments=yes
+
+# Ignore docstrings when computing similarities.
+ignore-docstrings=yes
+
+# Ignore imports when computing similarities.
+ignore-imports=yes
+
+# Signatures are removed from the similarity computation
+ignore-signatures=yes
+
+
+[BASIC]
+
+# Regular expression matching correct module names
+module-rgx=(([a-z_][a-z0-9_]*)|([A-Z][a-zA-Z0-9]+)|((tags_)*AK[A-Z][a-z0-9_]+))$
+
+# Good variable names which should always be accepted, separated by a comma
+good-names=i,j,k,a,e,ak,tz,_,pk
+
+# Allow single-letter variables and enforce lowercase variables with underscores otherwise
+variable-rgx=[a-z_][a-z0-9_]{0,30}$
+
+
+[TYPECHECK]
+
+# Tells whether missing members accessed in mixin class should be ignored. A
+# mixin class is detected if its name ends with "mixin" (case insensitive).
+ignore-mixin-members=yes
+
+# List of classes names for which member attributes should not be checked
+# (useful for classes with attributes dynamically set).
+ignored-classes=SQLObject,WSGIRequest
+
+# List of members which are set dynamically and missed by pylint inference
+# system, and so shouldn't trigger E0201 when accessed.
+generated-members=objects,DoesNotExist,id,pk,_meta,base_fields,context
+
+# List of method names used to declare (i.e. assign) instance attributes
+defining-attr-methods=__init__,__new__,setUp
+
+
+[DESIGN]
+
+max-parents=15
+
+max-args=8
diff --git a/requirements.txt b/requirements.txt
index 8a886c58b920b5d8746a0e9228981006fcc456ac..c537d2312e8d1fd5e83ca6c978f2eb00a48568d8 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -16,3 +16,11 @@ django-libsass==0.9
 django-betterforms==2.0.0
 mysqlclient==2.2.0  # for production deployment
 tzdata==2023.3
+
+# Documentation
+sphinxcontrib-django==2.3
+sphinxcontrib-apidoc==0.3.0
+recommonmark==0.7.1
+django-docs==0.3.3
+sphinx-rtd-theme==1.2.2
+sphinx==6.2.1
\ No newline at end of file
diff --git a/setup.cfg b/setup.cfg
new file mode 100644
index 0000000000000000000000000000000000000000..e9591b4f5895df25170c55b21588eaf290f909e1
--- /dev/null
+++ b/setup.cfg
@@ -0,0 +1,4 @@
+[pycodestyle]
+max-line-length = 120
+exclude = migrations,static
+max-complexity = 11
diff --git a/templates/base.html b/templates/base.html
index 9d64a9618f90b8f98ce56022577fc2d72c5a1e6f..5c1fd95736cfba150e750d8d2255fe0e483b3f5e 100644
--- a/templates/base.html
+++ b/templates/base.html
@@ -106,6 +106,7 @@
             {% block footer_custom %}
             {% endblock %}
             <a href="{% url "admin:index" %}">{% trans "Go to backend" %}</a> &middot;
+            <a href="{% url "docs_root" %}">{% trans "Docs" %}</a> &middot;
             {% footer_info as FI %}
             {% if FI.impress_text %}
                 {{ FI.impress_text }} &middot;