From d479a3cde984199322df7d3a38419cd7ed495a05 Mon Sep 17 00:00:00 2001
From: Jonathan Weth <git@jonathanweth.de>
Date: Wed, 6 Apr 2022 21:35:01 +0200
Subject: [PATCH 01/11] Add missing migration

---
 .../0004_alter_seatingplan_subject.py         | 20 +++++++++++++++++++
 1 file changed, 20 insertions(+)
 create mode 100644 aleksis/apps/stoelindeling/migrations/0004_alter_seatingplan_subject.py

diff --git a/aleksis/apps/stoelindeling/migrations/0004_alter_seatingplan_subject.py b/aleksis/apps/stoelindeling/migrations/0004_alter_seatingplan_subject.py
new file mode 100644
index 0000000..9f0fb57
--- /dev/null
+++ b/aleksis/apps/stoelindeling/migrations/0004_alter_seatingplan_subject.py
@@ -0,0 +1,20 @@
+# Generated by Django 3.2.12 on 2022-04-06 17:54
+
+from django.db import migrations, models
+import django.db.models.deletion
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('chronos', '0011_alter_timeperiod_weekday'),
+        ('stoelindeling', '0003_seat_negative_x_y'),
+    ]
+
+    operations = [
+        migrations.AlterField(
+            model_name='seatingplan',
+            name='subject',
+            field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='chronos.subject', verbose_name='Subject'),
+        ),
+    ]
-- 
GitLab


From 412153bfc87e954ee963802373ce421adabd3d06 Mon Sep 17 00:00:00 2001
From: Jonathan Weth <git@jonathanweth.de>
Date: Sat, 9 Apr 2022 16:22:52 +0200
Subject: [PATCH 02/11] Add intergration of seating plans

---
 aleksis/apps/stoelindeling/forms.py           | 71 ++++++++++++++++++-
 .../apps/stoelindeling/model_extensions.py    | 46 ++++++++++++
 aleksis/apps/stoelindeling/rules.py           |  4 ++
 .../stoelindeling/seating_plan/copy.html      | 26 +++++++
 aleksis/apps/stoelindeling/urls.py            |  5 ++
 aleksis/apps/stoelindeling/views.py           | 62 ++++++++++++++--
 6 files changed, 206 insertions(+), 8 deletions(-)
 create mode 100644 aleksis/apps/stoelindeling/model_extensions.py
 create mode 100644 aleksis/apps/stoelindeling/templates/stoelindeling/seating_plan/copy.html

diff --git a/aleksis/apps/stoelindeling/forms.py b/aleksis/apps/stoelindeling/forms.py
index e5f8c0e..a5067b7 100644
--- a/aleksis/apps/stoelindeling/forms.py
+++ b/aleksis/apps/stoelindeling/forms.py
@@ -1,4 +1,5 @@
 from django import forms
+from django.utils.translation import gettext as _
 
 from django_select2.forms import ModelSelect2Widget
 from material import Layout, Row
@@ -23,11 +24,26 @@ class SeatingPlanCreateForm(forms.ModelForm):
         super().__init__(*args, **kwargs)
 
         qs = Group.objects.all()
-        if not self.request.user.has_perm("stoelindeling.view_seatingplan"):
+        if not self.request.user.has_perm("stoelindeling.add_seatingplan"):
             qs = qs.filter(owners=self.request.user.person)
 
         self.fields["group"].queryset = qs
 
+    def clean(self):
+        cleaned_data = super().clean()
+        if (
+            cleaned_data["group"].subject
+            and cleaned_data["subject"]
+            and cleaned_data["group"].subject != cleaned_data["subject"]
+        ):
+            raise forms.ValidationError(
+                _(
+                    "The group you selected has a fixed subject ({}). "
+                    "You are not allowed to use another subject than it."
+                ).format(cleaned_data["group"].subject)
+            )
+        return cleaned_data
+
     class Meta:
         model = SeatingPlan
         fields = ["group", "subject", "room"]
@@ -47,6 +63,59 @@ class SeatingPlanCreateForm(forms.ModelForm):
         }
 
 
+class SeatingPlanCopyForm(forms.ModelForm):
+    layout = Layout(
+        Row(
+            "room",
+            "group",
+            "subject",
+        )
+    )
+
+    def __init__(self, *args, **kwargs):
+        self.request = kwargs.pop("request")
+        super().__init__(*args, **kwargs)
+
+        self.fields["room"].disabled = True
+
+        qs = Group.objects.filter(parent_groups=self.instance.group)
+        if not self.request.user.has_perm("stoelindeling.add_seatingplan"):
+            qs = qs.filter(owners=self.request.user.person)
+
+        self.fields["group"].queryset = qs
+
+    def clean(self):
+        cleaned_data = super().clean()
+        if cleaned_data["group"] == self.instance.group:
+            raise forms.ValidationError(_("Group cannot be the same as the original."))
+        if (
+            cleaned_data["group"].subject
+            and cleaned_data["subject"]
+            and cleaned_data["group"].subject != cleaned_data["subject"]
+        ):
+            raise forms.ValidationError(
+                _(
+                    "The group you selected has a fixed subject ({}). "
+                    "You are not allowed to use another subject than it."
+                ).format(cleaned_data["group"].subject)
+            )
+        return cleaned_data
+
+    class Meta:
+        model = SeatingPlan
+        fields = ["room", "group", "subject"]
+        widgets = {
+            "group": ModelSelect2Widget(
+                search_fields=["name__icontains", "short_name__icontains"],
+                attrs={"data-minimum-input-length": 0, "class": "browser-default"},
+            ),
+            "subject": ModelSelect2Widget(
+                search_fields=["name__icontains", "short_name__icontains"],
+                attrs={"data-minimum-input-length": 0, "class": "browser-default"},
+            ),
+        }
+
+
 class SeatingPlanForm(forms.ModelForm):
     layout = Layout(
         Row(
diff --git a/aleksis/apps/stoelindeling/model_extensions.py b/aleksis/apps/stoelindeling/model_extensions.py
new file mode 100644
index 0000000..558f186
--- /dev/null
+++ b/aleksis/apps/stoelindeling/model_extensions.py
@@ -0,0 +1,46 @@
+from django.apps import apps
+
+from aleksis.apps.chronos.models import Room, Subject
+from aleksis.apps.stoelindeling.models import SeatingPlan
+from aleksis.core.models import Group
+
+
+@Group.method
+def get_seating_plan(self, room: Room, subject: Subject = None):
+    """Find a seating plan for a group."""
+    qs = SeatingPlan.objects.filter(group=self, room=room)
+    if subject:
+        qs = qs.filter(subject=subject)
+    if qs.exists():
+        return qs.first()
+    if subject:
+        qs = SeatingPlan.objects.filter(group=self, room=room)
+        if qs.exists():
+            return qs.first()
+    qs = SeatingPlan.objects.filter(group__child_groups=self, room=room)
+    return qs.first()
+
+
+if apps.is_installed("aleksis.apps.alsijil"):
+    from aleksis.apps.alsijil.models import Event, ExtraLesson, LessonPeriod
+
+    def seating_plan_lesson_period(self):
+        if self.get_groups().count() == 1:
+            return self.get_groups().all()[0].get_seating_plan(self.get_room(), self.get_subject())
+        return None
+
+    LessonPeriod.property_(seating_plan_lesson_period, name="seating_plan")
+
+    def seating_plan_extra_lesson(self):
+        if self.groups.count() == 1:
+            return self.groups.all()[0].get_seating_plan(self.room, self.subject)
+        return None
+
+    ExtraLesson.property_(seating_plan_extra_lesson, name="seating_plan")
+
+    def seating_plan_event(self):
+        if self.groups.count() == 1 and self.rooms.count() == 1:
+            return self.groups.all()[0].get_seating_plan(self.rooms.all()[0])
+        return None
+
+    Event.property_(seating_plan_event, name="seating_plan")
diff --git a/aleksis/apps/stoelindeling/rules.py b/aleksis/apps/stoelindeling/rules.py
index 3ad3c08..f01f7e1 100644
--- a/aleksis/apps/stoelindeling/rules.py
+++ b/aleksis/apps/stoelindeling/rules.py
@@ -32,6 +32,10 @@ add_seatingplan_predicate = view_seatingplans_predicate & has_global_perm(
 )
 add_perm("stoelindeling.add_seatingplan_rule", add_seatingplan_predicate)
 
+# Copy seating plan
+copy_seatingplan_predicate = view_seatingplan_predicate & add_seatingplan_predicate
+add_perm("stoelindeling.copy_seatingplan_rule", copy_seatingplan_predicate)
+
 # Edit seating plan
 edit_seatingplan_predicate = view_seatingplans_predicate & (
     has_global_perm("stoelindeling.change_seatingplan")
diff --git a/aleksis/apps/stoelindeling/templates/stoelindeling/seating_plan/copy.html b/aleksis/apps/stoelindeling/templates/stoelindeling/seating_plan/copy.html
new file mode 100644
index 0000000..0c7ebf3
--- /dev/null
+++ b/aleksis/apps/stoelindeling/templates/stoelindeling/seating_plan/copy.html
@@ -0,0 +1,26 @@
+{# -*- engine:django -*- #}
+
+{% extends "core/base.html" %}
+{% load material_form i18n any_js %}
+
+{% block browser_title %}{% blocktrans %}Copy seating plan{% endblocktrans %}{% endblock %}
+{% block page_title %}{% blocktrans %}Copy seating plan{% endblocktrans %}{% endblock %}
+
+{% block extra_head %}
+  {{ form.media.css }}
+  {% include_css "select2-materialize" %}
+{% endblock %}
+
+{% block content %}
+  <p class="flow-text">
+    {% trans "Seating plan to copy" %}: {{ object }}
+  </p>
+  <form method="post">
+    {% csrf_token %}
+    {% form form=form %}{% endform %}
+    {% include "core/partials/save_button.html" %}
+  </form>
+
+  {% include_js "select2-materialize" %}
+  {{ form.media.js }}
+{% endblock %}
diff --git a/aleksis/apps/stoelindeling/urls.py b/aleksis/apps/stoelindeling/urls.py
index 94c22cc..0e1ce6b 100644
--- a/aleksis/apps/stoelindeling/urls.py
+++ b/aleksis/apps/stoelindeling/urls.py
@@ -19,6 +19,11 @@ urlpatterns = [
         views.SeatingPlanEditView.as_view(),
         name="edit_seating_plan",
     ),
+    path(
+        "seating_plans/<int:pk>/copy/",
+        views.SeatingPlanCopyView.as_view(),
+        name="copy_seating_plan",
+    ),
     path(
         "seating_plans/<int:pk>/delete/",
         views.SeatingPlanDeleteView.as_view(),
diff --git a/aleksis/apps/stoelindeling/views.py b/aleksis/apps/stoelindeling/views.py
index 51b7189..65025df 100644
--- a/aleksis/apps/stoelindeling/views.py
+++ b/aleksis/apps/stoelindeling/views.py
@@ -1,6 +1,6 @@
 from django.contrib import messages
 from django.shortcuts import redirect
-from django.urls import reverse_lazy
+from django.urls import reverse, reverse_lazy
 from django.utils.decorators import method_decorator
 from django.utils.translation import gettext as _
 from django.views.decorators.cache import never_cache
@@ -10,9 +10,15 @@ from django_tables2 import SingleTableView
 from reversion.views import RevisionMixin
 from rules.contrib.views import PermissionRequiredMixin
 
-from aleksis.core.mixins import AdvancedCreateView, AdvancedDeleteView, AdvancedEditView
+from aleksis.core.mixins import (
+    AdvancedCreateView,
+    AdvancedDeleteView,
+    AdvancedEditView,
+    SuccessNextMixin,
+)
 
-from .forms import SeatFormSet, SeatingPlanCreateForm, SeatingPlanForm
+from aleksis.core.views import LoginView
+from .forms import SeatFormSet, SeatingPlanCopyForm, SeatingPlanCreateForm, SeatingPlanForm
 from .models import Seat, SeatingPlan
 from .tables import SeatingPlanTable
 from .util.perms import get_allowed_seating_plans
@@ -39,7 +45,7 @@ class SeatingPlanDetailView(PermissionRequiredMixin, DetailView):
 
 
 @method_decorator(never_cache, name="dispatch")
-class SeatingPlanCreateView(PermissionRequiredMixin, AdvancedCreateView):
+class SeatingPlanCreateView(PermissionRequiredMixin, SuccessNextMixin, AdvancedCreateView):
     """Create view for seating plans."""
 
     model = SeatingPlan
@@ -56,7 +62,7 @@ class SeatingPlanCreateView(PermissionRequiredMixin, AdvancedCreateView):
 
 
 @method_decorator(never_cache, name="dispatch")
-class SeatingPlanEditView(PermissionRequiredMixin, AdvancedEditView):
+class SeatingPlanEditView(PermissionRequiredMixin, SuccessNextMixin, AdvancedEditView):
     """Edit view for seating plans."""
 
     model = SeatingPlan
@@ -65,6 +71,9 @@ class SeatingPlanEditView(PermissionRequiredMixin, AdvancedEditView):
     template_name = "stoelindeling/seating_plan/edit.html"
     success_message = _("The seating plan has been saved.")
 
+    def get_success_url(self):
+        return LoginView.get_redirect_url(self) or reverse("seating_plan", args=[self.object.pk])
+
     def get_context_data(self, **kwargs):
         context = super().get_context_data(**kwargs)
 
@@ -112,13 +121,52 @@ class SeatingPlanEditView(PermissionRequiredMixin, AdvancedEditView):
             Seat.objects.bulk_update(objects_to_update, ["x", "y", "seated"])
 
             messages.success(self.request, _("The seating plan has been updated."))
-            return redirect("seating_plan", self.object.pk)
+            return redirect(self.get_success_url())
 
         return super().form_invalid(form)
 
 
 @method_decorator(never_cache, name="dispatch")
-class SeatingPlanDeleteView(PermissionRequiredMixin, RevisionMixin, AdvancedDeleteView):
+class SeatingPlanCopyView(PermissionRequiredMixin, SuccessNextMixin, AdvancedEditView):
+    """Copy view for seating plans."""
+
+    model = SeatingPlan
+    form_class = SeatingPlanCopyForm
+    permission_required = "stoelindeling.copy_seatingplan_rule"
+    template_name = "stoelindeling/seating_plan/copy.html"
+
+    def get_success_url(self):
+        return reverse("edit_seating_plan", args=[self.new_object.pk])  # FiXME NEXT URL
+
+    def get_form_kwargs(self):
+        kwargs = super().get_form_kwargs()
+        kwargs["request"] = self.request
+        return kwargs
+
+    def form_valid(self, form):
+        context = self.get_context_data()
+
+        self.new_object = self.object
+        self.new_object.pk = None
+        self.new_object.group = form.cleaned_data["group"]
+        self.new_object.subject = form.cleaned_data["subject"]
+        self.new_object.save()
+
+        for seat in self.object.seats.all():
+            if seat.person in self.new_object.group.members.all():
+                new_seat = seat
+                new_seat.pk = None
+                new_seat.seating_plan = self.new_object
+                new_seat.save()
+
+        messages.success(self.request, _("The seating plan has been copied successfully."))
+        return redirect(self.get_success_url())
+
+
+@method_decorator(never_cache, name="dispatch")
+class SeatingPlanDeleteView(
+    PermissionRequiredMixin, RevisionMixin, SuccessNextMixin, AdvancedDeleteView
+):
     """Delete view for seating plans."""
 
     model = SeatingPlan
-- 
GitLab


From 7c1ecce6db68c09de421883f7ba2a665b9834e23 Mon Sep 17 00:00:00 2001
From: Jonathan Weth <git@jonathanweth.de>
Date: Sat, 9 Apr 2022 16:59:06 +0200
Subject: [PATCH 03/11] Fix permissions

---
 aleksis/apps/stoelindeling/rules.py      | 17 +++++++++--------
 aleksis/apps/stoelindeling/util/perms.py | 14 +++++++++++++-
 aleksis/apps/stoelindeling/views.py      | 10 +++++++++-
 3 files changed, 31 insertions(+), 10 deletions(-)

diff --git a/aleksis/apps/stoelindeling/rules.py b/aleksis/apps/stoelindeling/rules.py
index f01f7e1..1617bdf 100644
--- a/aleksis/apps/stoelindeling/rules.py
+++ b/aleksis/apps/stoelindeling/rules.py
@@ -5,10 +5,11 @@ from aleksis.core.util.predicates import (
     has_global_perm,
     has_object_perm,
     has_person,
+    is_group_owner,
 )
 
 from .models import SeatingPlan
-from .util.perms import is_group_owner
+from .util.perms import is_plan_group_owner
 
 # View seating plan list
 view_seatingplans_predicate = has_person & (
@@ -22,13 +23,13 @@ add_perm("stoelindeling.view_seatingplans_rule", view_seatingplans_predicate)
 view_seatingplan_predicate = has_person & (
     has_global_perm("stoelindeling.view_seatingplan")
     | has_object_perm("stoelindeling.view_seatingplan")
-    | is_group_owner
+    | is_plan_group_owner
 )
 add_perm("stoelindeling.view_seatingplan_rule", view_seatingplan_predicate)
 
 # Add seating plan
-add_seatingplan_predicate = view_seatingplans_predicate & has_global_perm(
-    "stoelindeling.add_seatingplan"
+add_seatingplan_predicate = view_seatingplans_predicate & (
+    has_global_perm("stoelindeling.add_seatingplan") | is_group_owner | is_plan_group_owner
 )
 add_perm("stoelindeling.add_seatingplan_rule", add_seatingplan_predicate)
 
@@ -37,17 +38,17 @@ copy_seatingplan_predicate = view_seatingplan_predicate & add_seatingplan_predic
 add_perm("stoelindeling.copy_seatingplan_rule", copy_seatingplan_predicate)
 
 # Edit seating plan
-edit_seatingplan_predicate = view_seatingplans_predicate & (
+edit_seatingplan_predicate = view_seatingplan_predicate & (
     has_global_perm("stoelindeling.change_seatingplan")
-    | is_group_owner
+    | is_plan_group_owner
     | has_object_perm("stoelindeling.change_seatingplan")
 )
 add_perm("stoelindeling.edit_seatingplan_rule", edit_seatingplan_predicate)
 
 # Delete seating plan
-delete_seatingplan_predicate = view_seatingplans_predicate & (
+delete_seatingplan_predicate = view_seatingplan_predicate & (
     has_global_perm("stoelindeling.delete_seatingplan")
-    | is_group_owner
+    | is_plan_group_owner
     | has_object_perm("stoelindeling.delete_seatingplan")
 )
 add_perm("stoelindeling.delete_seatingplan_rule", delete_seatingplan_predicate)
diff --git a/aleksis/apps/stoelindeling/util/perms.py b/aleksis/apps/stoelindeling/util/perms.py
index c55d09b..0876019 100644
--- a/aleksis/apps/stoelindeling/util/perms.py
+++ b/aleksis/apps/stoelindeling/util/perms.py
@@ -3,12 +3,24 @@ from django.db.models import Q
 from guardian.shortcuts import get_objects_for_user
 from rules import predicate
 
+from aleksis.core.models import Group
+
 from ..models import SeatingPlan
 
 
 @predicate
-def is_group_owner(user, seating_plan: SeatingPlan) -> bool:
+def is_group_owner(user, group: Group) -> bool:
+    """Predicate which checks if the user is a owner of the group."""
+    if not isinstance(group, Group):
+        return False
+    return user.person in group.owners.all()
+
+
+@predicate
+def is_plan_group_owner(user, seating_plan: SeatingPlan) -> bool:
     """Predicate which checks if the user is a owner of the seating plan's group."""
+    if not isinstance(seating_plan, SeatingPlan):
+        return False
     return user.person in seating_plan.group.owners.all()
 
 
diff --git a/aleksis/apps/stoelindeling/views.py b/aleksis/apps/stoelindeling/views.py
index 65025df..0b3f4b9 100644
--- a/aleksis/apps/stoelindeling/views.py
+++ b/aleksis/apps/stoelindeling/views.py
@@ -16,8 +16,8 @@ from aleksis.core.mixins import (
     AdvancedEditView,
     SuccessNextMixin,
 )
-
 from aleksis.core.views import LoginView
+
 from .forms import SeatFormSet, SeatingPlanCopyForm, SeatingPlanCreateForm, SeatingPlanForm
 from .models import Seat, SeatingPlan
 from .tables import SeatingPlanTable
@@ -58,6 +58,14 @@ class SeatingPlanCreateView(PermissionRequiredMixin, SuccessNextMixin, AdvancedC
     def get_form_kwargs(self):
         kwargs = super().get_form_kwargs()
         kwargs["request"] = self.request
+        initial = {}
+        if "room" in self.request.GET:
+            initial["room"] = self.request.GET["room"]
+        if "subject" in self.request.GET:
+            initial["subject"] = self.request.GET["subject"]
+        if "group" in self.request.GET:
+            initial["group"] = self.request.GET["group"]
+        kwargs["initial"] = initial
         return kwargs
 
 
-- 
GitLab


From e72d5a407d965f24acf55033491a3961c4a400eb Mon Sep 17 00:00:00 2001
From: Jonathan Weth <git@jonathanweth.de>
Date: Mon, 11 Apr 2022 16:21:44 +0200
Subject: [PATCH 04/11] Fix migration dep

---
 .../stoelindeling/migrations/0004_alter_seatingplan_subject.py  | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/aleksis/apps/stoelindeling/migrations/0004_alter_seatingplan_subject.py b/aleksis/apps/stoelindeling/migrations/0004_alter_seatingplan_subject.py
index 9f0fb57..4182b55 100644
--- a/aleksis/apps/stoelindeling/migrations/0004_alter_seatingplan_subject.py
+++ b/aleksis/apps/stoelindeling/migrations/0004_alter_seatingplan_subject.py
@@ -7,7 +7,7 @@ import django.db.models.deletion
 class Migration(migrations.Migration):
 
     dependencies = [
-        ('chronos', '0011_alter_timeperiod_weekday'),
+        ('chronos', '0010_remove_subject_unique_name_per_site'),
         ('stoelindeling', '0003_seat_negative_x_y'),
     ]
 
-- 
GitLab


From a977b64bc655b4a1fbddcaeb808bbb87fcd87ee0 Mon Sep 17 00:00:00 2001
From: Jonathan Weth <git@jonathanweth.de>
Date: Wed, 13 Apr 2022 16:03:40 +0200
Subject: [PATCH 05/11] Fix some permissions for use in Alsijil

---
 aleksis/apps/stoelindeling/rules.py      | 17 ++++++++++++++---
 aleksis/apps/stoelindeling/util/perms.py | 10 +++++++++-
 2 files changed, 23 insertions(+), 4 deletions(-)

diff --git a/aleksis/apps/stoelindeling/rules.py b/aleksis/apps/stoelindeling/rules.py
index 9c73efe..93fb0cd 100644
--- a/aleksis/apps/stoelindeling/rules.py
+++ b/aleksis/apps/stoelindeling/rules.py
@@ -9,7 +9,7 @@ from aleksis.core.util.predicates import (
 )
 
 from .models import SeatingPlan
-from .util.perms import is_plan_group_owner
+from .util.perms import is_plan_child_group_owner, is_plan_group_owner
 
 # View seating plan list
 view_seatingplans_predicate = has_person & (
@@ -24,17 +24,28 @@ view_seatingplan_predicate = has_person & (
     has_global_perm("stoelindeling.view_seatingplan")
     | has_object_perm("stoelindeling.view_seatingplan")
     | is_plan_group_owner
+    | is_plan_child_group_owner
 )
 add_perm("stoelindeling.view_seatingplan_rule", view_seatingplan_predicate)
 
+#
+view_seatingplan_for_group_predicate = has_person & (
+    has_global_perm("stoelindeling.view_seatingplan") | is_group_owner
+)
+add_perm("stoelindeling.view_seatingplan_for_group_rule", view_seatingplan_for_group_predicate)
+
 # Add seating plan
-create_seatingplan_predicate = view_seatingplans_predicate & (
+create_seatingplan_predicate = has_person & (
     has_global_perm("stoelindeling.add_seatingplan") | is_group_owner | is_plan_group_owner
 )
 add_perm("stoelindeling.create_seatingplan_rule", create_seatingplan_predicate)
 
 # Copy seating plan
-copy_seatingplan_predicate = view_seatingplan_predicate & create_seatingplan_predicate
+copy_seatingplan_predicate = view_seatingplan_for_group_predicate & create_seatingplan_predicate
+add_perm("stoelindeling.copy_seatingplan_for_group_rule", copy_seatingplan_predicate)
+
+# Copy seating plan
+copy_seatingplan_predicate = view_seatingplan_predicate
 add_perm("stoelindeling.copy_seatingplan_rule", copy_seatingplan_predicate)
 
 # Edit seating plan
diff --git a/aleksis/apps/stoelindeling/util/perms.py b/aleksis/apps/stoelindeling/util/perms.py
index 0876019..2b00e28 100644
--- a/aleksis/apps/stoelindeling/util/perms.py
+++ b/aleksis/apps/stoelindeling/util/perms.py
@@ -3,7 +3,7 @@ from django.db.models import Q
 from guardian.shortcuts import get_objects_for_user
 from rules import predicate
 
-from aleksis.core.models import Group
+from aleksis.core.models import Group, Person
 
 from ..models import SeatingPlan
 
@@ -24,6 +24,14 @@ def is_plan_group_owner(user, seating_plan: SeatingPlan) -> bool:
     return user.person in seating_plan.group.owners.all()
 
 
+@predicate
+def is_plan_child_group_owner(user, seating_plan: SeatingPlan) -> bool:
+    """Predicate which checks if the user is an owner of the seating plan's child groups."""
+    if not isinstance(seating_plan, SeatingPlan):
+        return False
+    return user.person in Person.objects.filter(owner_of__in=seating_plan.group.child_groups.all())
+
+
 def get_allowed_seating_plans(user):
     """Get all seating plans the user is allowed to see."""
     if not user.has_perm("stoelindeling.view_seatingplan"):
-- 
GitLab


From d423655fced1ce3ceed9d8d5aa2f315e0edbca63 Mon Sep 17 00:00:00 2001
From: Jonathan Weth <git@jonathanweth.de>
Date: Wed, 13 Apr 2022 16:03:52 +0200
Subject: [PATCH 06/11] Reduce menu as there is just one menu item

---
 aleksis/apps/stoelindeling/menus.py | 26 +++++++-------------------
 1 file changed, 7 insertions(+), 19 deletions(-)

diff --git a/aleksis/apps/stoelindeling/menus.py b/aleksis/apps/stoelindeling/menus.py
index 492015d..fc2ce01 100644
--- a/aleksis/apps/stoelindeling/menus.py
+++ b/aleksis/apps/stoelindeling/menus.py
@@ -4,26 +4,14 @@ MENUS = {
     "NAV_MENU_CORE": [
         {
             "name": _("Seating plans"),
-            "url": "#",
-            "root": True,
-            "svg_icon": "mdi:seat-outline",
+            "url": "seating_plans",
+            "svg_icon": "mdi:view-list-outline",
             "validators": [
-                "menu_generator.validators.is_authenticated",
-                "aleksis.core.util.core_helpers.has_person",
+                (
+                    "aleksis.core.util.predicates.permission_validator",
+                    "stoelindeling.view_seatingplans_rule",
+                ),
             ],
-            "submenu": [
-                {
-                    "name": _("All seating plans"),
-                    "url": "seating_plans",
-                    "svg_icon": "mdi:view-list-outline",
-                    "validators": [
-                        (
-                            "aleksis.core.util.predicates.permission_validator",
-                            "stoelindeling.view_seatingplans_rule",
-                        ),
-                    ],
-                },
-            ],
-        }
+        },
     ]
 }
-- 
GitLab


From 5bae62cfe0c81cadcae12560524468b33267c02f Mon Sep 17 00:00:00 2001
From: magicfelix <felix@felix-zauberer.de>
Date: Tue, 12 Apr 2022 14:43:33 +0200
Subject: [PATCH 07/11] Fix permission names

---
 aleksis/apps/stoelindeling/rules.py                         | 6 +++---
 .../templates/stoelindeling/seating_plan/list.html          | 4 ++--
 .../templates/stoelindeling/seating_plan/view.html          | 4 ++--
 3 files changed, 7 insertions(+), 7 deletions(-)

diff --git a/aleksis/apps/stoelindeling/rules.py b/aleksis/apps/stoelindeling/rules.py
index 1617bdf..a5346f5 100644
--- a/aleksis/apps/stoelindeling/rules.py
+++ b/aleksis/apps/stoelindeling/rules.py
@@ -28,10 +28,10 @@ view_seatingplan_predicate = has_person & (
 add_perm("stoelindeling.view_seatingplan_rule", view_seatingplan_predicate)
 
 # Add seating plan
-add_seatingplan_predicate = view_seatingplans_predicate & (
-    has_global_perm("stoelindeling.add_seatingplan") | is_group_owner | is_plan_group_owner
+create_seatingplan_predicate = view_seatingplans_predicate & has_global_perm(
+    "stoelindeling.add_seatingplan"
 )
-add_perm("stoelindeling.add_seatingplan_rule", add_seatingplan_predicate)
+add_perm("stoelindeling.create_seatingplan_rule", create_seatingplan_predicate)
 
 # Copy seating plan
 copy_seatingplan_predicate = view_seatingplan_predicate & add_seatingplan_predicate
diff --git a/aleksis/apps/stoelindeling/templates/stoelindeling/seating_plan/list.html b/aleksis/apps/stoelindeling/templates/stoelindeling/seating_plan/list.html
index d7810ee..36ee743 100644
--- a/aleksis/apps/stoelindeling/templates/stoelindeling/seating_plan/list.html
+++ b/aleksis/apps/stoelindeling/templates/stoelindeling/seating_plan/list.html
@@ -9,8 +9,8 @@
 {% block page_title %}{% blocktrans %}Seating plans{% endblocktrans %}{% endblock %}
 
 {% block content %}
-  {% has_perm "stoelindeling.create_seating_plan_rule" user as can_create_seating_plan %}
-  {% if can_create_seating_plan %}
+  {% has_perm "stoelindeling.create_seatingplan_rule" user as can_create_seatingplan %}
+  {% if can_create_seatingplan %}
     <a class="btn green waves-effect waves-light" href="{% url 'create_seating_plan' %}">
       <i class="material-icons left iconify" data-icon="mdi:plus">add</i>
       {% trans "Create seating plan" %}
diff --git a/aleksis/apps/stoelindeling/templates/stoelindeling/seating_plan/view.html b/aleksis/apps/stoelindeling/templates/stoelindeling/seating_plan/view.html
index f0c8b9a..36fd6a5 100644
--- a/aleksis/apps/stoelindeling/templates/stoelindeling/seating_plan/view.html
+++ b/aleksis/apps/stoelindeling/templates/stoelindeling/seating_plan/view.html
@@ -18,8 +18,8 @@
 {% endblock %}
 
 {% block content %}
-  {% has_perm "stoelindeling.edit_seating_plan_rule" user seating_plan as can_edit %}
-  {% has_perm "stoelindeling.delete_seating_plan_rule" user seating_plan as can_delete %}
+  {% has_perm "stoelindeling.edit_seatingplan_rule" user seating_plan as can_edit %}
+  {% has_perm "stoelindeling.delete_seatingplan_rule" user seating_plan as can_delete %}
 
   {% if can_edit %}
     <a class="btn waves-effect waves-light orange margin-bottom" href="{% url "edit_seating_plan" object.pk %}">
-- 
GitLab


From 8fa12ebc103e79ed1ef42e20a7b7ecc86b0462f7 Mon Sep 17 00:00:00 2001
From: Jonathan Weth <git@jonathanweth.de>
Date: Wed, 13 Apr 2022 16:03:40 +0200
Subject: [PATCH 08/11] Fix some permissions for use in Alsijil

---
 aleksis/apps/stoelindeling/rules.py      | 19 +++++++++++++++----
 aleksis/apps/stoelindeling/util/perms.py | 10 +++++++++-
 2 files changed, 24 insertions(+), 5 deletions(-)

diff --git a/aleksis/apps/stoelindeling/rules.py b/aleksis/apps/stoelindeling/rules.py
index a5346f5..93fb0cd 100644
--- a/aleksis/apps/stoelindeling/rules.py
+++ b/aleksis/apps/stoelindeling/rules.py
@@ -9,7 +9,7 @@ from aleksis.core.util.predicates import (
 )
 
 from .models import SeatingPlan
-from .util.perms import is_plan_group_owner
+from .util.perms import is_plan_child_group_owner, is_plan_group_owner
 
 # View seating plan list
 view_seatingplans_predicate = has_person & (
@@ -24,17 +24,28 @@ view_seatingplan_predicate = has_person & (
     has_global_perm("stoelindeling.view_seatingplan")
     | has_object_perm("stoelindeling.view_seatingplan")
     | is_plan_group_owner
+    | is_plan_child_group_owner
 )
 add_perm("stoelindeling.view_seatingplan_rule", view_seatingplan_predicate)
 
+#
+view_seatingplan_for_group_predicate = has_person & (
+    has_global_perm("stoelindeling.view_seatingplan") | is_group_owner
+)
+add_perm("stoelindeling.view_seatingplan_for_group_rule", view_seatingplan_for_group_predicate)
+
 # Add seating plan
-create_seatingplan_predicate = view_seatingplans_predicate & has_global_perm(
-    "stoelindeling.add_seatingplan"
+create_seatingplan_predicate = has_person & (
+    has_global_perm("stoelindeling.add_seatingplan") | is_group_owner | is_plan_group_owner
 )
 add_perm("stoelindeling.create_seatingplan_rule", create_seatingplan_predicate)
 
 # Copy seating plan
-copy_seatingplan_predicate = view_seatingplan_predicate & add_seatingplan_predicate
+copy_seatingplan_predicate = view_seatingplan_for_group_predicate & create_seatingplan_predicate
+add_perm("stoelindeling.copy_seatingplan_for_group_rule", copy_seatingplan_predicate)
+
+# Copy seating plan
+copy_seatingplan_predicate = view_seatingplan_predicate
 add_perm("stoelindeling.copy_seatingplan_rule", copy_seatingplan_predicate)
 
 # Edit seating plan
diff --git a/aleksis/apps/stoelindeling/util/perms.py b/aleksis/apps/stoelindeling/util/perms.py
index 0876019..2b00e28 100644
--- a/aleksis/apps/stoelindeling/util/perms.py
+++ b/aleksis/apps/stoelindeling/util/perms.py
@@ -3,7 +3,7 @@ from django.db.models import Q
 from guardian.shortcuts import get_objects_for_user
 from rules import predicate
 
-from aleksis.core.models import Group
+from aleksis.core.models import Group, Person
 
 from ..models import SeatingPlan
 
@@ -24,6 +24,14 @@ def is_plan_group_owner(user, seating_plan: SeatingPlan) -> bool:
     return user.person in seating_plan.group.owners.all()
 
 
+@predicate
+def is_plan_child_group_owner(user, seating_plan: SeatingPlan) -> bool:
+    """Predicate which checks if the user is an owner of the seating plan's child groups."""
+    if not isinstance(seating_plan, SeatingPlan):
+        return False
+    return user.person in Person.objects.filter(owner_of__in=seating_plan.group.child_groups.all())
+
+
 def get_allowed_seating_plans(user):
     """Get all seating plans the user is allowed to see."""
     if not user.has_perm("stoelindeling.view_seatingplan"):
-- 
GitLab


From ef6704856e6243754ca9a635cf303c12b9aeb283 Mon Sep 17 00:00:00 2001
From: Jonathan Weth <git@jonathanweth.de>
Date: Wed, 13 Apr 2022 16:03:52 +0200
Subject: [PATCH 09/11] Reduce menu as there is just one menu item

---
 aleksis/apps/stoelindeling/menus.py | 26 +++++++-------------------
 1 file changed, 7 insertions(+), 19 deletions(-)

diff --git a/aleksis/apps/stoelindeling/menus.py b/aleksis/apps/stoelindeling/menus.py
index 492015d..fc2ce01 100644
--- a/aleksis/apps/stoelindeling/menus.py
+++ b/aleksis/apps/stoelindeling/menus.py
@@ -4,26 +4,14 @@ MENUS = {
     "NAV_MENU_CORE": [
         {
             "name": _("Seating plans"),
-            "url": "#",
-            "root": True,
-            "svg_icon": "mdi:seat-outline",
+            "url": "seating_plans",
+            "svg_icon": "mdi:view-list-outline",
             "validators": [
-                "menu_generator.validators.is_authenticated",
-                "aleksis.core.util.core_helpers.has_person",
+                (
+                    "aleksis.core.util.predicates.permission_validator",
+                    "stoelindeling.view_seatingplans_rule",
+                ),
             ],
-            "submenu": [
-                {
-                    "name": _("All seating plans"),
-                    "url": "seating_plans",
-                    "svg_icon": "mdi:view-list-outline",
-                    "validators": [
-                        (
-                            "aleksis.core.util.predicates.permission_validator",
-                            "stoelindeling.view_seatingplans_rule",
-                        ),
-                    ],
-                },
-            ],
-        }
+        },
     ]
 }
-- 
GitLab


From 472eecc1356accd61d8e61d4378e4c1860d1e23f Mon Sep 17 00:00:00 2001
From: magicfelix <felix@felix-zauberer.de>
Date: Wed, 13 Apr 2022 16:27:24 +0200
Subject: [PATCH 10/11] [CI] Publish package

---
 .gitlab-ci.yml | 2 ++
 1 file changed, 2 insertions(+)

diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index 2f98ddb..fb6ad25 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -11,3 +11,5 @@ include:
       file: /ci/test/security.yml
     - project: "AlekSIS/official/AlekSIS"
       file: /ci/build/dist.yml
+    - project: "AlekSIS/official/AlekSIS"
+      file: /ci/publish/pypi.yml
-- 
GitLab


From 8dd17b560b3cd5539b82401d90707e07c6fca002 Mon Sep 17 00:00:00 2001
From: Jonathan Weth <git@jonathanweth.de>
Date: Sun, 1 May 2022 18:21:56 +0200
Subject: [PATCH 11/11] Make code better readable and add docstrings

---
 .../apps/stoelindeling/model_extensions.py    | 21 +++++++++++++------
 aleksis/apps/stoelindeling/rules.py           |  6 ++++--
 2 files changed, 19 insertions(+), 8 deletions(-)

diff --git a/aleksis/apps/stoelindeling/model_extensions.py b/aleksis/apps/stoelindeling/model_extensions.py
index 558f186..c61ae15 100644
--- a/aleksis/apps/stoelindeling/model_extensions.py
+++ b/aleksis/apps/stoelindeling/model_extensions.py
@@ -1,3 +1,5 @@
+from typing import Union
+
 from django.apps import apps
 
 from aleksis.apps.chronos.models import Room, Subject
@@ -6,25 +8,30 @@ from aleksis.core.models import Group
 
 
 @Group.method
-def get_seating_plan(self, room: Room, subject: Subject = None):
+def get_seating_plan(self, room: Room, subject: Subject = None) -> Union[SeatingPlan, None]:
     """Find a seating plan for a group."""
     qs = SeatingPlan.objects.filter(group=self, room=room)
     if subject:
         qs = qs.filter(subject=subject)
     if qs.exists():
         return qs.first()
-    if subject:
-        qs = SeatingPlan.objects.filter(group=self, room=room)
-        if qs.exists():
-            return qs.first()
+
+    qs = SeatingPlan.objects.filter(group=self, room=room)
+    if qs.exists():
+        return qs.first()
+
     qs = SeatingPlan.objects.filter(group__child_groups=self, room=room)
-    return qs.first()
+    if qs.exists():
+        return qs.first()
+
+    return None
 
 
 if apps.is_installed("aleksis.apps.alsijil"):
     from aleksis.apps.alsijil.models import Event, ExtraLesson, LessonPeriod
 
     def seating_plan_lesson_period(self):
+        """Get the seating plan for a specific lesson period."""
         if self.get_groups().count() == 1:
             return self.get_groups().all()[0].get_seating_plan(self.get_room(), self.get_subject())
         return None
@@ -32,6 +39,7 @@ if apps.is_installed("aleksis.apps.alsijil"):
     LessonPeriod.property_(seating_plan_lesson_period, name="seating_plan")
 
     def seating_plan_extra_lesson(self):
+        """Get the seating plan for an extra lesson."""
         if self.groups.count() == 1:
             return self.groups.all()[0].get_seating_plan(self.room, self.subject)
         return None
@@ -39,6 +47,7 @@ if apps.is_installed("aleksis.apps.alsijil"):
     ExtraLesson.property_(seating_plan_extra_lesson, name="seating_plan")
 
     def seating_plan_event(self):
+        """Get the seating plan for an event."""
         if self.groups.count() == 1 and self.rooms.count() == 1:
             return self.groups.all()[0].get_seating_plan(self.rooms.all()[0])
         return None
diff --git a/aleksis/apps/stoelindeling/rules.py b/aleksis/apps/stoelindeling/rules.py
index 93fb0cd..38dab4c 100644
--- a/aleksis/apps/stoelindeling/rules.py
+++ b/aleksis/apps/stoelindeling/rules.py
@@ -41,8 +41,10 @@ create_seatingplan_predicate = has_person & (
 add_perm("stoelindeling.create_seatingplan_rule", create_seatingplan_predicate)
 
 # Copy seating plan
-copy_seatingplan_predicate = view_seatingplan_for_group_predicate & create_seatingplan_predicate
-add_perm("stoelindeling.copy_seatingplan_for_group_rule", copy_seatingplan_predicate)
+copy_seatingplan_for_group_predicate = (
+    view_seatingplan_for_group_predicate & create_seatingplan_predicate
+)
+add_perm("stoelindeling.copy_seatingplan_for_group_rule", copy_seatingplan_for_group_predicate)
 
 # Copy seating plan
 copy_seatingplan_predicate = view_seatingplan_predicate
-- 
GitLab