From 0debfe0b7eb9ca6a32591e54566eb9f8926a97ed Mon Sep 17 00:00:00 2001
From: Jonathan Weth <git@jonathanweth.de>
Date: Sun, 20 Dec 2020 15:27:33 +0100
Subject: [PATCH] Add frontend for managing dashboard widgets

Important: Doesn't include support for ordering or sizing yet
---
 aleksis/core/menus.py                         | 11 +++
 aleksis/core/rules.py                         | 13 +++
 aleksis/core/tables.py                        | 21 +++++
 .../core/dashboard_widget/create.html         | 23 ++++++
 .../templates/core/dashboard_widget/edit.html | 23 ++++++
 .../templates/core/dashboard_widget/list.html | 22 ++++++
 aleksis/core/urls.py                          | 16 ++++
 aleksis/core/views.py                         | 79 ++++++++++++++++++-
 8 files changed, 206 insertions(+), 2 deletions(-)
 create mode 100644 aleksis/core/templates/core/dashboard_widget/create.html
 create mode 100644 aleksis/core/templates/core/dashboard_widget/edit.html
 create mode 100644 aleksis/core/templates/core/dashboard_widget/list.html

diff --git a/aleksis/core/menus.py b/aleksis/core/menus.py
index 2e71c3573..9b1c42e93 100644
--- a/aleksis/core/menus.py
+++ b/aleksis/core/menus.py
@@ -93,6 +93,17 @@ MENUS = {
                         ),
                     ],
                 },
+                {
+                    "name": _("Dashboard widgets"),
+                    "url": "dashboard_widgets",
+                    "icon": "dashboard",
+                    "validators": [
+                        (
+                            "aleksis.core.util.predicates.permission_validator",
+                            "core.view_dashboardwidget",
+                        ),
+                    ],
+                },
                 {
                     "name": _("Data management"),
                     "url": "data_management",
diff --git a/aleksis/core/rules.py b/aleksis/core/rules.py
index 4b0cf23d6..2e36bc08b 100644
--- a/aleksis/core/rules.py
+++ b/aleksis/core/rules.py
@@ -278,3 +278,16 @@ view_group_stats_predicate = has_person & (
     has_global_perm("core.view_group_stats") | has_object_perm("core.view_group_stats")
 )
 rules.add_perm("core.view_group_stats", view_group_stats_predicate)
+
+
+view_dashboard_widget_predicate = has_person & has_global_perm("core.view_dashboardwidget")
+rules.add_perm("core.view_dashboardwidget", view_dashboard_widget_predicate)
+
+create_dashboard_widget_predicate = has_person & has_global_perm("core.add_dashboardwidget")
+rules.add_perm("core.create_dashboardwidget", create_dashboard_widget_predicate)
+
+edit_dashboard_widget_predicate = has_person & has_global_perm("core.change_dashboardwidget")
+rules.add_perm("core.edit_dashboardwidget", edit_dashboard_widget_predicate)
+
+delete_dashboard_widget_predicate = has_person & has_global_perm("core.delete_dashboardwidget")
+rules.add_perm("core.delete_dashboardwidget", delete_dashboard_widget_predicate)
diff --git a/aleksis/core/tables.py b/aleksis/core/tables.py
index f562b61a1..54d3f3e5c 100644
--- a/aleksis/core/tables.py
+++ b/aleksis/core/tables.py
@@ -70,3 +70,24 @@ class GroupTypesTable(tables.Table):
     delete = tables.LinkColumn(
         "delete_group_type_by_id", args=[A("id")], verbose_name=_("Delete"), text=_("Delete")
     )
+
+
+class DashboardWidgetTable(tables.Table):
+    """Table to list dashboard widgets."""
+
+    class Meta:
+        attrs = {"class": "responsive-table highlight"}
+
+    widget_name = tables.Column(accessor="pk")
+    title = tables.LinkColumn("edit_dashboard_widget", args=[A("id")])
+    active = tables.BooleanColumn(yesno="check,cancel", attrs={"span": {"class": "material-icons"}})
+    delete = tables.LinkColumn(
+        "delete_dashboard_widget",
+        args=[A("id")],
+        text=_("Delete"),
+        attrs={"a": {"class": "btn-flat waves-effect waves-red red-text"}},
+        verbose_name=_("Actions"),
+    )
+
+    def render_widget_name(self, value, record):
+        return record._meta.verbose_name
diff --git a/aleksis/core/templates/core/dashboard_widget/create.html b/aleksis/core/templates/core/dashboard_widget/create.html
new file mode 100644
index 000000000..cfaa296ee
--- /dev/null
+++ b/aleksis/core/templates/core/dashboard_widget/create.html
@@ -0,0 +1,23 @@
+{# -*- engine:django -*- #}
+
+{% extends "core/base.html" %}
+{% load material_form i18n data_helpers %}
+
+{% block browser_title %}
+  {% verbose_name_object model as widget_title %}
+  {% blocktrans with widget=widget_title %}Create {{ widget }}{% endblocktrans %}
+{% endblock %}
+{% block page_title %}
+  {% verbose_name_object model as widget_title %}
+  {% blocktrans with widget=widget_title %}Create {{ widget }}{% 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/core/templates/core/dashboard_widget/edit.html b/aleksis/core/templates/core/dashboard_widget/edit.html
new file mode 100644
index 000000000..64dfe0eed
--- /dev/null
+++ b/aleksis/core/templates/core/dashboard_widget/edit.html
@@ -0,0 +1,23 @@
+{# -*- engine:django -*- #}
+
+{% extends "core/base.html" %}
+{% load material_form i18n data_helpers %}
+
+{% block browser_title %}
+  {% verbose_name_object object as widget_title %}
+  {% blocktrans with widget=widget_title %}Edit {{ widget }}{% endblocktrans %}
+{% endblock %}
+{% block page_title %}
+  {% verbose_name_object object as widget_title %}
+  {% blocktrans with widget=widget_title %}Edit {{ widget }}{% 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/core/templates/core/dashboard_widget/list.html b/aleksis/core/templates/core/dashboard_widget/list.html
new file mode 100644
index 000000000..ab384e862
--- /dev/null
+++ b/aleksis/core/templates/core/dashboard_widget/list.html
@@ -0,0 +1,22 @@
+{# -*- engine:django -*- #}
+
+{% extends "core/base.html" %}
+
+{% load i18n data_helpers %}
+{% load render_table from django_tables2 %}
+
+{% block browser_title %}{% blocktrans %}Dashboard widgets{% endblocktrans %}{% endblock %}
+{% block page_title %}{% blocktrans %}Dashboard widgets{% endblocktrans %}{% endblock %}
+
+{% block content %}
+
+  {% for ct, model in widget_types %}
+    <a class="btn green waves-effect waves-light" href="{% url 'create_dashboard_widget' ct.app_label ct.model  %}">
+      <i class="material-icons left">add</i>
+      {% verbose_name_object model as widget_name %}
+      {% blocktrans with name=widget_name %}Create {{ name }}{% endblocktrans %}
+    </a>
+  {% endfor %}
+
+  {% render_table table %}
+{% endblock %}
diff --git a/aleksis/core/urls.py b/aleksis/core/urls.py
index 6c989ddf7..9061962e6 100644
--- a/aleksis/core/urls.py
+++ b/aleksis/core/urls.py
@@ -154,6 +154,22 @@ urlpatterns = [
         name="preferences_group",
     ),
     path("health/", include(health_urls)),
+    path("dashboard_widgets/", views.DashboardWidgetListView.as_view(), name="dashboard_widgets"),
+    path(
+        "dashboard_widgets/<int:pk>/edit/",
+        views.DashboardWidgetEditView.as_view(),
+        name="edit_dashboard_widget",
+    ),
+    path(
+        "dashboard_widgets/<int:pk>/delete/",
+        views.DashboardWidgetDeleteView.as_view(),
+        name="delete_dashboard_widget",
+    ),
+    path(
+        "dashboard_widgets/<str:app>/<str:model>/new/",
+        views.DashboardWidgetCreateView.as_view(),
+        name="create_dashboard_widget",
+    ),
 ]
 
 # Serve static files from STATIC_ROOT to make it work with runserver
diff --git a/aleksis/core/views.py b/aleksis/core/views.py
index 6969d4148..d34aa8ae8 100644
--- a/aleksis/core/views.py
+++ b/aleksis/core/views.py
@@ -1,9 +1,11 @@
-from typing import Optional
+from typing import Any, Dict, Optional, Type
 
 from django.apps import apps
 from django.conf import settings
+from django.contrib.contenttypes.models import ContentType
 from django.core.exceptions import PermissionDenied
 from django.core.paginator import Paginator
+from django.forms.models import BaseModelForm, modelform_factory
 from django.http import HttpRequest, HttpResponse, HttpResponseNotFound
 from django.shortcuts import get_object_or_404, redirect, render
 from django.urls import reverse_lazy
@@ -36,7 +38,7 @@ from .forms import (
     SchoolTermForm,
     SitePreferenceForm,
 )
-from .mixins import AdvancedCreateView, AdvancedEditView
+from .mixins import AdvancedCreateView, AdvancedDeleteView, AdvancedEditView
 from .models import (
     AdditionalField,
     Announcement,
@@ -54,6 +56,7 @@ from .registries import (
 )
 from .tables import (
     AdditionalFieldsTable,
+    DashboardWidgetTable,
     GroupsTable,
     GroupTypesTable,
     PersonsTable,
@@ -699,3 +702,75 @@ def delete_group_type(request: HttpRequest, id_: int) -> HttpResponse:
     messages.success(request, _("The group type has been deleted."))
 
     return redirect("group_types")
+
+
+class DashboardWidgetListView(SingleTableView, PermissionRequiredMixin):
+    """Table of all dashboard widgets."""
+
+    model = DashboardWidget
+    table_class = DashboardWidgetTable
+    permission_required = "core.view_dashboardwidget"
+    template_name = "core/dashboard_widget/list.html"
+
+    def get_context_data(self, **kwargs: Any) -> Dict[str, Any]:
+        context = super().get_context_data(**kwargs)
+        context["widget_types"] = [
+            (ContentType.objects.get_for_model(m, False), m)
+            for m in DashboardWidget.__subclasses__()
+        ]
+        return context
+
+
+@method_decorator(never_cache, name="dispatch")
+class DashboardWidgetEditView(AdvancedEditView, PermissionRequiredMixin):
+    """Edit view for dashboard widgets."""
+
+    def get_form_class(self) -> Type[BaseModelForm]:
+        return modelform_factory(self.object.__class__, fields=self.fields)
+
+    model = DashboardWidget
+    fields = "__all__"
+    permission_required = "core.edit_dashboardwidget"
+    template_name = "core/dashboard_widget/edit.html"
+    success_url = reverse_lazy("dashboard_widgets")
+    success_message = _("The dashboard widget has been saved.")
+
+
+@method_decorator(never_cache, name="dispatch")
+class DashboardWidgetCreateView(AdvancedCreateView, PermissionRequiredMixin):
+    """Create view for dashboard widgets."""
+
+    def get_model(self, request, *args, **kwargs):
+        app_label = kwargs.get("app")
+        model = kwargs.get("model")
+        ct = get_object_or_404(ContentType, app_label=app_label, model=model)
+        return ct.model_class()
+
+    def get_context_data(self, **kwargs: Any) -> Dict[str, Any]:
+        context = super().get_context_data(**kwargs)
+        context["model"] = self.model
+        return context
+
+    def get(self, request, *args, **kwargs):
+        self.model = self.get_model(request, *args, **kwargs)
+        return super().get(request, *args, **kwargs)
+
+    def post(self, request, *args, **kwargs):
+        self.model = self.get_model(request, *args, **kwargs)
+        return super().post(request, *args, **kwargs)
+
+    fields = "__all__"
+    permission_required = "core.add_dashboardwidget"
+    template_name = "core/dashboard_widget/create.html"
+    success_url = reverse_lazy("dashboard_widgets")
+    success_message = _("The dashboard widget has been created.")
+
+
+class DashboardWidgetDeleteView(PermissionRequiredMixin, AdvancedDeleteView):
+    """Delete view for dashboard widgets."""
+
+    model = DashboardWidget
+    permission_required = "core.delete_dashboardwidget"
+    template_name = "core/pages/delete.html"
+    success_url = reverse_lazy("dashboard_widgets")
+    success_message = _("The dashboard widget has been deleted.")
-- 
GitLab