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 96e0419df12c54be457544d5448a8eea702c8e68..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']
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/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