From 72775aa9617d0dc018989602c0ad7bccff8f1271 Mon Sep 17 00:00:00 2001
From: Jonathan Weth <git@jonathanweth.de>
Date: Sun, 5 Jun 2022 17:58:43 +0200
Subject: [PATCH] Add views for managing instructions

---
 aleksis/apps/alsijil/forms.py                 |  9 ++++
 ...014_instruction.py => 0017_instruction.py} |  2 +-
 aleksis/apps/alsijil/models.py                |  1 +
 aleksis/apps/alsijil/rules.py                 | 26 +++++++++-
 .../templates/alsijil/instruction/create.html | 15 ++++++
 .../templates/alsijil/instruction/edit.html   | 17 +++++++
 .../templates/alsijil/instruction/list.html   | 32 +++++++++++-
 aleksis/apps/alsijil/urls.py                  |  9 ++++
 aleksis/apps/alsijil/util/predicates.py       | 22 +++++++-
 aleksis/apps/alsijil/views.py                 | 51 ++++++++++++++++++-
 10 files changed, 179 insertions(+), 5 deletions(-)
 rename aleksis/apps/alsijil/migrations/{0014_instruction.py => 0017_instruction.py} (99%)
 create mode 100644 aleksis/apps/alsijil/templates/alsijil/instruction/create.html
 create mode 100644 aleksis/apps/alsijil/templates/alsijil/instruction/edit.html

diff --git a/aleksis/apps/alsijil/forms.py b/aleksis/apps/alsijil/forms.py
index 6f4e65b20..e325586cb 100644
--- a/aleksis/apps/alsijil/forms.py
+++ b/aleksis/apps/alsijil/forms.py
@@ -31,6 +31,7 @@ from .models import (
     ExtraMark,
     GroupRole,
     GroupRoleAssignment,
+    Instruction,
     LessonDocumentation,
     PersonalNote,
 )
@@ -383,3 +384,11 @@ class RegisterObjectActionForm(ListActionForm):
     """Action form for managing register objects for use with ``RegisterObjectTable``."""
 
     actions = [send_request_to_check_entry]
+
+
+class InstructionForm(forms.ModelForm):
+    layout = Layout("name", "icon", "pdf_file", "groups")
+
+    class Meta:
+        model = Instruction
+        fields = ["name", "icon", "pdf_file", "groups"]
diff --git a/aleksis/apps/alsijil/migrations/0014_instruction.py b/aleksis/apps/alsijil/migrations/0017_instruction.py
similarity index 99%
rename from aleksis/apps/alsijil/migrations/0014_instruction.py
rename to aleksis/apps/alsijil/migrations/0017_instruction.py
index 48525627f..11e200873 100644
--- a/aleksis/apps/alsijil/migrations/0014_instruction.py
+++ b/aleksis/apps/alsijil/migrations/0017_instruction.py
@@ -10,7 +10,7 @@ class Migration(migrations.Migration):
     dependencies = [
         ('core', '0019_fix_uniqueness_per_site'),
         ('sites', '0002_alter_domain_unique'),
-        ('alsijil', '0013_fix_uniqueness_per_site'),
+        ('alsijil', '0016_add_not_counted_excuse_types'),
     ]
 
     operations = [
diff --git a/aleksis/apps/alsijil/models.py b/aleksis/apps/alsijil/models.py
index 42189856b..90c2868ba 100644
--- a/aleksis/apps/alsijil/models.py
+++ b/aleksis/apps/alsijil/models.py
@@ -525,6 +525,7 @@ class Instruction(SchoolTermRelatedExtensibleModel):
         help_text=_(
             "The instruction will be shown for the members and owners of the selected groups."
         ),
+        related_name="instructions",
     )
 
     def __str__(self):
diff --git a/aleksis/apps/alsijil/rules.py b/aleksis/apps/alsijil/rules.py
index 621f3e3b2..aa80713bd 100644
--- a/aleksis/apps/alsijil/rules.py
+++ b/aleksis/apps/alsijil/rules.py
@@ -11,12 +11,14 @@ from aleksis.core.util.predicates import (
 )
 
 from .util.predicates import (
+    has_any_instruction,
     has_lesson_group_object_perm,
     has_person_group_object_perm,
     has_personal_note_group_perm,
     is_group_member,
     is_group_owner,
     is_group_role_assignment_group_owner,
+    is_instruction_for_person,
     is_lesson_original_teacher,
     is_lesson_parent_group_owner,
     is_lesson_participant,
@@ -311,5 +313,27 @@ view_register_objects_list_predicate = has_person & (
 )
 add_perm("alsijil.view_register_objects_list_rule", view_register_objects_list_predicate)
 
-view_instructions_predicate = has_person
+view_instructions_predicate = has_person & (
+    has_global_perm("alsijil.view_instruction") | has_any_instruction
+)
 add_perm("alsijil.view_instructions_rule", view_instructions_predicate)
+
+view_instruction_predicate = has_person & (
+    has_global_perm("alsijil.view_instruction")
+    | is_instruction_for_person
+    | has_object_perm("alsijil.view_instruction")
+)
+add_perm("alsijil.view_instruction_rule", view_instruction_predicate)
+
+add_instruction_predicate = view_instructions_predicate & has_global_perm("alsijil.add_instruction")
+add_perm("alsijil.add_instruction_rule", add_instruction_predicate)
+
+edit_instruction_predicate = view_instructions_predicate & (
+    has_global_perm("alsijil.change_instruction") | has_object_perm("alsijil.change_instruction")
+)
+add_perm("alsijil.edit_instruction_rule", edit_instruction_predicate)
+
+delete_instruction_predicate = view_instructions_predicate & (
+    has_global_perm("alsijil.delete_instruction") | has_object_perm("alsijil.delete_instruction")
+)
+add_perm("alsijil.delete_instruction_rule", delete_instruction_predicate)
diff --git a/aleksis/apps/alsijil/templates/alsijil/instruction/create.html b/aleksis/apps/alsijil/templates/alsijil/instruction/create.html
new file mode 100644
index 000000000..42761a813
--- /dev/null
+++ b/aleksis/apps/alsijil/templates/alsijil/instruction/create.html
@@ -0,0 +1,15 @@
+{# -*- engine:django -*- #}
+
+{% extends "core/base.html" %}
+{% load material_form i18n %}
+
+{% block browser_title %}{% blocktrans %}Create instruction{% endblocktrans %}{% endblock %}
+{% block page_title %}{% blocktrans %}Create instruction{% endblocktrans %}{% endblock %}
+
+{% block content %}
+  <form method="post" enctype="multipart/form-data">
+    {% csrf_token %}
+    {% form form=form %}{% endform %}
+    {% include "core/partials/save_button.html" %}
+  </form>
+{% endblock %}
diff --git a/aleksis/apps/alsijil/templates/alsijil/instruction/edit.html b/aleksis/apps/alsijil/templates/alsijil/instruction/edit.html
new file mode 100644
index 000000000..b9732e4c4
--- /dev/null
+++ b/aleksis/apps/alsijil/templates/alsijil/instruction/edit.html
@@ -0,0 +1,17 @@
+{# -*- engine:django -*- #}
+
+{% extends "core/base.html" %}
+{% load material_form i18n %}
+
+{% block browser_title %}{% blocktrans %}Edit instruction{% endblocktrans %}{% endblock %}
+{% block page_title %}{% blocktrans %}Edit instruction{% endblocktrans %}{% endblock %}
+
+{% block content %}
+  <form method="post" enctype="multipart/form-data">
+    {% csrf_token %}
+    {% form form=form %}{% endform %}
+    {% include "core/partials/save_button.html" %}
+  </form>
+
+  {{ form.media.js }}
+{% endblock %}
diff --git a/aleksis/apps/alsijil/templates/alsijil/instruction/list.html b/aleksis/apps/alsijil/templates/alsijil/instruction/list.html
index da3c384b3..02320901a 100644
--- a/aleksis/apps/alsijil/templates/alsijil/instruction/list.html
+++ b/aleksis/apps/alsijil/templates/alsijil/instruction/list.html
@@ -2,21 +2,51 @@
 
 {% extends "core/base.html" %}
 
-{% load i18n %}
+{% load i18n rules %}
 
 {% block browser_title %}{% blocktrans %}Instructions{% endblocktrans %}{% endblock %}
 {% block page_title %}{% blocktrans %}Instructions{% endblocktrans %}{% endblock %}
 
 {% block content %}
+  {% has_perm "alsijil.add_instruction_rule" user as can_add %}
+  {% if can_add %}
+    <a class="btn green waves-effect waves-light" href="{% url 'create_instruction' %}">
+      <i class="material-icons left">add</i>
+      {% trans "Create instruction" %}
+    </a>
+  {% endif %}
+
   <ul class="collection">
     {% for instruction in instruction_list %}
       <li class="collection-item avatar">
         <i class="material-icons materialize-circle primary-color">{{ instruction.icon|default:"rule" }}</i>
         <span class="title"> {{ instruction.name }}</span>
+        <div class="right">
+          {% has_perm "alsijil.edit_instruction_rule" user as can_edit %}
+          {% has_perm "alsijil.delete_instruction_rule" user as can_delete %}
+          {% if can_edit %}
+            <a class="btn-flat waves-effect waves-orange orange-text"
+               href="{% url "edit_instruction" instruction.pk %}">
+              <i class="material-icons left">edit</i>
+              {% trans "Edit" %}
+            </a>
+          {% endif %}
+          {% if can_delete %}
+            <a class="btn-flat waves-effect waves-red red-text" href="{% url "delete_instruction" instruction.pk %}">
+              <i class="material-icons left">delete</i>
+              {% trans "Delete" %}
+            </a>
+          {% endif %}
+
           <a class="btn-flat waves-effect waves-green right" href="{{ instruction.pdf_file.url }}" target="_blank">
             <i class="material-icons left">picture_as_pdf</i>
             {% trans "Show PDF file with instruction" %}
           </a>
+        </div>
+      </li>
+    {% empty %}
+      <li class="collection-item">
+        {% trans "No instructions available." %}
       </li>
     {% endfor %}
   </ul>
diff --git a/aleksis/apps/alsijil/urls.py b/aleksis/apps/alsijil/urls.py
index f7551eb8e..19f92a235 100644
--- a/aleksis/apps/alsijil/urls.py
+++ b/aleksis/apps/alsijil/urls.py
@@ -129,4 +129,13 @@ urlpatterns = [
     ),
     path("all/", views.AllRegisterObjectsView.as_view(), name="all_register_objects"),
     path("instructions/", views.InstructionsListView.as_view(), name="instructions"),
+    path("instructions/create/", views.InstructionCreateView.as_view(), name="create_instruction"),
+    path(
+        "instructions/<int:pk>/edit/", views.InstructionEditView.as_view(), name="edit_instruction"
+    ),
+    path(
+        "instructions/<int:pk>/delete/",
+        views.InstructionDeleteView.as_view(),
+        name="delete_instruction",
+    ),
 ]
diff --git a/aleksis/apps/alsijil/util/predicates.py b/aleksis/apps/alsijil/util/predicates.py
index 1759a5446..377779d30 100644
--- a/aleksis/apps/alsijil/util/predicates.py
+++ b/aleksis/apps/alsijil/util/predicates.py
@@ -1,6 +1,7 @@
 from typing import Any, Union
 
 from django.contrib.auth.models import User
+from django.db.models import Q
 
 from rules import predicate
 
@@ -8,7 +9,7 @@ from aleksis.apps.chronos.models import Event, ExtraLesson, LessonPeriod
 from aleksis.core.models import Group, Person
 from aleksis.core.util.predicates import check_object_permission
 
-from ..models import PersonalNote
+from ..models import Instruction, PersonalNote
 
 
 @predicate
@@ -281,3 +282,22 @@ def is_group_role_assignment_group_owner(user: User, obj: Union[Group, Person])
 def is_owner_of_any_group(user: User, obj):
     """Predicate which checks if the person is group owner of any group."""
     return Group.objects.filter(owners=user.person).exists()
+
+
+@predicate
+def has_any_instruction(user: User, obj):
+    """Predicate which checks if the user has any instruction."""
+    return Instruction.objects.filter(
+        Q(groups__members=user.person) | Q(groups__owners=user.person)
+    ).exists()
+
+
+@predicate
+def is_instruction_for_person(user: User, obj: Instruction):
+    """Predicate which checks if the instruction is for the person."""
+    return (
+        user
+        in Person.objects.filter(
+            Q(member_of__instructions=obj) | Q(owner_of__instructions=obj)
+        ).exists()
+    )
diff --git a/aleksis/apps/alsijil/views.py b/aleksis/apps/alsijil/views.py
index 17b6e2dee..f05b90fb4 100644
--- a/aleksis/apps/alsijil/views.py
+++ b/aleksis/apps/alsijil/views.py
@@ -51,6 +51,7 @@ from .forms import (
     FilterRegisterObjectForm,
     GroupRoleAssignmentEditForm,
     GroupRoleForm,
+    InstructionForm,
     LessonDocumentationForm,
     PersonalNoteFormSet,
     PersonOverviewForm,
@@ -1405,8 +1406,56 @@ class AllRegisterObjectsView(PermissionRequiredMixin, View):
 
 
 class InstructionsListView(PermissionRequiredMixin, ListView):
-    """Table of all excuse types."""
+    """Table of all instructions."""
 
     model = Instruction
     permission_required = "alsijil.view_instructions_rule"
     template_name = "alsijil/instruction/list.html"
+
+    def get_queryset(self):
+        if self.request.user.has_perm("alsijil.view_instruction"):
+            return super().get_queryset()
+        return (
+            super()
+            .get_queryset()
+            .filter(
+                Q(groups__members=self.request.user.person)
+                | Q(groups__owners=self.request.user.person)
+            )
+            .distinct()
+        )
+
+
+@method_decorator(never_cache, name="dispatch")
+class InstructionCreateView(PermissionRequiredMixin, AdvancedCreateView):
+    """Create view for instructions."""
+
+    model = Instruction
+    form_class = InstructionForm
+    permission_required = "alsijil.add_instruction_rule"
+    template_name = "alsijil/instruction/create.html"
+    success_url = reverse_lazy("instructions")
+    success_message = _("The instruction has been created.")
+
+
+@method_decorator(never_cache, name="dispatch")
+class InstructionEditView(PermissionRequiredMixin, AdvancedEditView):
+    """Edit view for instructions."""
+
+    model = Instruction
+    form_class = InstructionForm
+    permission_required = "alsijil.edit_instruction_rule"
+    template_name = "alsijil/instruction/edit.html"
+    success_url = reverse_lazy("instructions")
+    success_message = _("The instruction has been saved.")
+
+
+@method_decorator(never_cache, "dispatch")
+class InstructionDeleteView(PermissionRequiredMixin, RevisionMixin, AdvancedDeleteView):
+    """Delete view for instructions."""
+
+    model = Instruction
+    permission_required = "alsijil.delete_instruction_rule"
+    template_name = "core/pages/delete.html"
+    success_url = reverse_lazy("instructions")
+    success_message = _("The instruction has been deleted.")
-- 
GitLab