From 6450ff00b3fd6736ab04ed0a7c81dafab121eb55 Mon Sep 17 00:00:00 2001
From: Jonathan Weth <git@jonathanweth.de>
Date: Fri, 22 Jan 2021 11:49:24 +0100
Subject: [PATCH] Add views for managing class roles

---
 aleksis/apps/alsijil/forms.py                 | 10 +++-
 aleksis/apps/alsijil/menus.py                 | 11 +++++
 aleksis/apps/alsijil/preferences.py           |  8 +++
 aleksis/apps/alsijil/rules.py                 | 23 +++++++++
 aleksis/apps/alsijil/tables.py                | 32 ++++++++++++
 .../templates/alsijil/class_role/create.html  | 15 ++++++
 .../templates/alsijil/class_role/edit.html    | 17 +++++++
 .../templates/alsijil/class_role/list.html    | 22 +++++++++
 .../templates/alsijil/class_role/warning.html | 10 ++++
 aleksis/apps/alsijil/urls.py                  |  8 +++
 aleksis/apps/alsijil/views.py                 | 49 ++++++++++++++++++-
 11 files changed, 202 insertions(+), 3 deletions(-)
 create mode 100644 aleksis/apps/alsijil/templates/alsijil/class_role/create.html
 create mode 100644 aleksis/apps/alsijil/templates/alsijil/class_role/edit.html
 create mode 100644 aleksis/apps/alsijil/templates/alsijil/class_role/list.html
 create mode 100644 aleksis/apps/alsijil/templates/alsijil/class_role/warning.html

diff --git a/aleksis/apps/alsijil/forms.py b/aleksis/apps/alsijil/forms.py
index 9624e5208..5d0c16129 100644
--- a/aleksis/apps/alsijil/forms.py
+++ b/aleksis/apps/alsijil/forms.py
@@ -15,7 +15,7 @@ from aleksis.apps.chronos.models import TimePeriod
 from aleksis.core.models import Group, Person
 from aleksis.core.util.predicates import check_global_permission
 
-from .models import ExcuseType, ExtraMark, LessonDocumentation, PersonalNote
+from .models import ClassRole, ExcuseType, ExtraMark, LessonDocumentation, PersonalNote
 
 
 class LessonDocumentationForm(forms.ModelForm):
@@ -162,3 +162,11 @@ class ExcuseTypeForm(forms.ModelForm):
     class Meta:
         model = ExcuseType
         fields = ["short_name", "name"]
+
+
+class ClassRoleForm(forms.ModelForm):
+    layout = Layout("name", "icon", "colour")
+
+    class Meta:
+        model = ClassRole
+        fields = ["name", "icon", "colour"]
diff --git a/aleksis/apps/alsijil/menus.py b/aleksis/apps/alsijil/menus.py
index f90052a76..a0ac455ca 100644
--- a/aleksis/apps/alsijil/menus.py
+++ b/aleksis/apps/alsijil/menus.py
@@ -89,6 +89,17 @@ MENUS = {
                         ),
                     ],
                 },
+                {
+                    "name": _("Manage class roles"),
+                    "url": "class_roles",
+                    "icon": "support_agent",
+                    "validators": [
+                        (
+                            "aleksis.core.util.predicates.permission_validator",
+                            "alsijil.view_classroles",
+                        ),
+                    ],
+                },
             ],
         }
     ]
diff --git a/aleksis/apps/alsijil/preferences.py b/aleksis/apps/alsijil/preferences.py
index 4ced04f41..055d5c997 100644
--- a/aleksis/apps/alsijil/preferences.py
+++ b/aleksis/apps/alsijil/preferences.py
@@ -66,3 +66,11 @@ class AllowEntriesInHolidays(BooleanPreference):
     name = "allow_entries_in_holidays"
     default = False
     verbose_name = _("Allow teachers to add data for lessons in holidays")
+
+
+@site_preferences_registry.register
+class ActivateClassRoles(BooleanPreference):
+    section = alsijil
+    name = "activate_class_roles"
+    default = True
+    verbose_name = _("Activate support for creating and assigning class roles")
diff --git a/aleksis/apps/alsijil/rules.py b/aleksis/apps/alsijil/rules.py
index f553c2773..0d74385a3 100644
--- a/aleksis/apps/alsijil/rules.py
+++ b/aleksis/apps/alsijil/rules.py
@@ -218,3 +218,26 @@ add_perm("alsijil.edit_extramark", edit_extramark_predicate)
 # Delete extra mark
 delete_extramark_predicate = view_extramarks_predicate & has_global_perm("alsijil.delete_extramark")
 add_perm("alsijil.delete_extramark", delete_extramark_predicate)
+
+
+# View class role list
+view_class_roles_predicate = (
+    has_person
+    & is_site_preference_set("alsijil", "activate_class_roles")
+    & has_global_perm("alsijil.view_classrole")
+)
+add_perm("alsijil.view_classroles", view_class_roles_predicate)
+
+# Add class role
+add_class_role_predicate = view_class_roles_predicate & has_global_perm("alsijil.add_classrole")
+add_perm("alsijil.add_classrole", add_class_role_predicate)
+
+# Edit class role
+edit_class_role_predicate = view_class_roles_predicate & has_global_perm("alsijil.change_classrole")
+add_perm("alsijil.edit_classrole", edit_class_role_predicate)
+
+# Delete class role
+delete_class_role_predicate = view_class_roles_predicate & has_global_perm(
+    "alsijil.delete_classrole"
+)
+add_perm("alsijil.delete_classrole", delete_class_role_predicate)
diff --git a/aleksis/apps/alsijil/tables.py b/aleksis/apps/alsijil/tables.py
index b9a8e6840..0cfdc1ab2 100644
--- a/aleksis/apps/alsijil/tables.py
+++ b/aleksis/apps/alsijil/tables.py
@@ -1,3 +1,4 @@
+from django.template.loader import render_to_string
 from django.utils.translation import gettext_lazy as _
 
 import django_tables2 as tables
@@ -48,3 +49,34 @@ class ExcuseTypeTable(tables.Table):
             self.columns.hide("edit")
         if not request.user.has_perm("alsijil.delete_excusetype"):
             self.columns.hide("delete")
+
+
+class ClassRoleTable(tables.Table):
+    class Meta:
+        attrs = {"class": "highlight"}
+
+    name = tables.LinkColumn("edit_excuse_type", args=[A("id")])
+    edit = tables.LinkColumn(
+        "edit_class_role",
+        args=[A("id")],
+        text=_("Edit"),
+        attrs={"a": {"class": "btn-flat waves-effect waves-orange orange-text"}},
+    )
+    delete = tables.LinkColumn(
+        "delete_class_role",
+        args=[A("id")],
+        text=_("Delete"),
+        attrs={"a": {"class": "btn-flat waves-effect waves-red red-text"}},
+    )
+
+    def render_name(self, value, record):
+        colour = record.colour or "black"
+        icon_name = record.icon or "support_agent"
+        context = dict(content=value, icon=icon_name, classes=f"{colour} white-text")
+        return render_to_string("components/materialize-chips.html", context)
+
+    def before_render(self, request):
+        if not request.user.has_perm("alsijil.edit_classrole"):
+            self.columns.hide("edit")
+        if not request.user.has_perm("alsijil.delete_classrole"):
+            self.columns.hide("delete")
diff --git a/aleksis/apps/alsijil/templates/alsijil/class_role/create.html b/aleksis/apps/alsijil/templates/alsijil/class_role/create.html
new file mode 100644
index 000000000..096d34ed9
--- /dev/null
+++ b/aleksis/apps/alsijil/templates/alsijil/class_role/create.html
@@ -0,0 +1,15 @@
+{# -*- engine:django -*- #}
+
+{% extends "core/base.html" %}
+{% load material_form i18n %}
+
+{% block browser_title %}{% blocktrans %}Create class role{% endblocktrans %}{% endblock %}
+{% block page_title %}{% blocktrans %}Create class role{% endblocktrans %}{% endblock %}
+
+{% block content %}
+  <form method="post">
+    {% csrf_token %}
+    {% form form=form %}{% endform %}
+    {% include "core/partials/save_button.html" %}
+  </form>
+{% endblock %}
diff --git a/aleksis/apps/alsijil/templates/alsijil/class_role/edit.html b/aleksis/apps/alsijil/templates/alsijil/class_role/edit.html
new file mode 100644
index 000000000..b01aa3dc9
--- /dev/null
+++ b/aleksis/apps/alsijil/templates/alsijil/class_role/edit.html
@@ -0,0 +1,17 @@
+{# -*- engine:django -*- #}
+
+{% extends "core/base.html" %}
+{% load material_form i18n %}
+
+{% block browser_title %}{% blocktrans %}Edit class role{% endblocktrans %}{% endblock %}
+{% block page_title %}{% blocktrans %}Edit class role{% endblocktrans %}{% endblock %}
+
+{% block content %}
+
+  <form method="post">
+    {% csrf_token %}
+    {% form form=form %}{% endform %}
+    {% include "core/partials/save_button.html" %}
+  </form>
+
+{% endblock %}
diff --git a/aleksis/apps/alsijil/templates/alsijil/class_role/list.html b/aleksis/apps/alsijil/templates/alsijil/class_role/list.html
new file mode 100644
index 000000000..146b65013
--- /dev/null
+++ b/aleksis/apps/alsijil/templates/alsijil/class_role/list.html
@@ -0,0 +1,22 @@
+{# -*- engine:django -*- #}
+
+{% extends "core/base.html" %}
+
+{% load i18n rules %}
+{% load render_table from django_tables2 %}
+
+{% block browser_title %}{% blocktrans %}Class roles{% endblocktrans %}{% endblock %}
+{% block page_title %}{% blocktrans %}Class roles{% endblocktrans %}{% endblock %}
+
+{% block content %}
+  {% has_perm "alsijil.add_classrole" user as add_class_role %}
+  {% if add_class_role %}
+    <a class="btn green waves-effect waves-light" href="{% url 'create_class_role' %}">
+      <i class="material-icons left">add</i>
+      {% trans "Create class role" %}
+    </a>
+  {% endif %}
+
+  {% render_table table %}
+{% endblock %}
+
diff --git a/aleksis/apps/alsijil/templates/alsijil/class_role/warning.html b/aleksis/apps/alsijil/templates/alsijil/class_role/warning.html
new file mode 100644
index 000000000..d90d2e820
--- /dev/null
+++ b/aleksis/apps/alsijil/templates/alsijil/class_role/warning.html
@@ -0,0 +1,10 @@
+{% load i18n %}
+<div class="alert warning">
+  <p>
+    <i class="material-icons left">warning</i>
+    {% blocktrans %}
+      This function should only be used to define alternatives to the default excuse which also will be counted extra.
+      Don't use this to create a default excuse or if you don't divide between different types of excuse.
+    {% endblocktrans %}
+  </p>
+</div>
diff --git a/aleksis/apps/alsijil/urls.py b/aleksis/apps/alsijil/urls.py
index 673154f49..752f5b8e9 100644
--- a/aleksis/apps/alsijil/urls.py
+++ b/aleksis/apps/alsijil/urls.py
@@ -49,4 +49,12 @@ urlpatterns = [
         views.ExcuseTypeDeleteView.as_view(),
         name="delete_excuse_type",
     ),
+    path("class_roles/", views.ClassRoleListView.as_view(), name="class_roles"),
+    path("class_roles/create/", views.ClassRoleCreateView.as_view(), name="create_class_role"),
+    path("class_roles/<int:pk>/edit/", views.ClassRoleEditView.as_view(), name="edit_class_role",),
+    path(
+        "class_roles/<int:pk>/delete/",
+        views.ClassRoleDeleteView.as_view(),
+        name="delete_class_role",
+    ),
 ]
diff --git a/aleksis/apps/alsijil/views.py b/aleksis/apps/alsijil/views.py
index c5616940d..8803a8851 100644
--- a/aleksis/apps/alsijil/views.py
+++ b/aleksis/apps/alsijil/views.py
@@ -27,6 +27,7 @@ from aleksis.core.util import messages
 from aleksis.core.util.core_helpers import get_site_preferences, objectgetter_optional
 
 from .forms import (
+    ClassRoleForm,
     ExcuseTypeForm,
     ExtraMarkForm,
     LessonDocumentationForm,
@@ -34,8 +35,8 @@ from .forms import (
     RegisterAbsenceForm,
     SelectForm,
 )
-from .models import ExcuseType, ExtraMark, LessonDocumentation, PersonalNote
-from .tables import ExcuseTypeTable, ExtraMarkTable
+from .models import ClassRole, ExcuseType, ExtraMark, LessonDocumentation, PersonalNote
+from .tables import ClassRoleTable, ExcuseTypeTable, ExtraMarkTable
 from .util.alsijil_helpers import get_lesson_period_by_pk, get_timetable_instance_by_pk
 
 
@@ -849,3 +850,47 @@ class ExcuseTypeDeleteView(PermissionRequiredMixin, RevisionMixin, AdvancedDelet
     template_name = "core/pages/delete.html"
     success_url = reverse_lazy("excuse_types")
     success_message = _("The excuse type has been deleted.")
+
+
+class ClassRoleListView(PermissionRequiredMixin, SingleTableView):
+    """Table of all class roles."""
+
+    model = ClassRole
+    table_class = ClassRoleTable
+    permission_required = "alsijil.view_classroles"
+    template_name = "alsijil/class_role/list.html"
+
+
+@method_decorator(never_cache, name="dispatch")
+class ClassRoleCreateView(PermissionRequiredMixin, AdvancedCreateView):
+    """Create view for class roles."""
+
+    model = ClassRole
+    form_class = ClassRoleForm
+    permission_required = "alsijil.add_classrole"
+    template_name = "alsijil/class_role/create.html"
+    success_url = reverse_lazy("class_roles")
+    success_message = _("The class role has been created.")
+
+
+@method_decorator(never_cache, name="dispatch")
+class ClassRoleEditView(PermissionRequiredMixin, AdvancedEditView):
+    """Edit view for class roles."""
+
+    model = ClassRole
+    form_class = ClassRoleForm
+    permission_required = "alsijil.edit_classrole"
+    template_name = "alsijil/class_role/edit.html"
+    success_url = reverse_lazy("class_roles")
+    success_message = _("The class role has been saved.")
+
+
+@method_decorator(never_cache, "dispatch")
+class ClassRoleDeleteView(PermissionRequiredMixin, RevisionMixin, AdvancedDeleteView):
+    """Delete view for class roles."""
+
+    model = ClassRole
+    permission_required = "alsijil.delete_classrole"
+    template_name = "core/pages/delete.html"
+    success_url = reverse_lazy("class_roles")
+    success_message = _("The class role has been deleted.")
-- 
GitLab