From d863c691bfc41c9d73a48ff1cca0482853f89947 Mon Sep 17 00:00:00 2001
From: Tom Teichler <tom.teichler@teckids.org>
Date: Wed, 13 May 2020 16:05:50 +0200
Subject: [PATCH] Add management of additional fields

This MR includes:
- List additional fields
- Create additional fields
- Edit additional fields
- Delete additional fields
- Add fields to group
---
 aleksis/core/forms.py                         | 12 +++-
 aleksis/core/menus.py                         | 11 +++
 aleksis/core/migrations/0001_initial.py       | 11 +--
 aleksis/core/rules.py                         | 23 ++++++-
 aleksis/core/tables.py                        | 14 ++++
 .../templates/core/additional_fields.html     | 18 +++++
 .../templates/core/edit_additional_field.html | 17 +++++
 aleksis/core/urls.py                          | 16 +++++
 aleksis/core/views.py                         | 67 ++++++++++++++++++-
 9 files changed, 181 insertions(+), 8 deletions(-)
 create mode 100644 aleksis/core/templates/core/additional_fields.html
 create mode 100644 aleksis/core/templates/core/edit_additional_field.html

diff --git a/aleksis/core/forms.py b/aleksis/core/forms.py
index 888926e58..3e5f0f60b 100644
--- a/aleksis/core/forms.py
+++ b/aleksis/core/forms.py
@@ -10,7 +10,7 @@ from dynamic_preferences.forms import PreferenceForm
 from material import Fieldset, Layout, Row
 
 from .mixins import ExtensibleForm
-from .models import Announcement, Group, Person
+from .models import AdditionalField, Announcement, Group, Person
 from .registries import (
     group_preferences_registry,
     person_preferences_registry,
@@ -127,6 +127,7 @@ class EditGroupForm(ExtensibleForm):
     layout = Layout(
         Fieldset(_("Common data"), "name", "short_name"),
         Fieldset(_("Persons"), "members", "owners", "parent_groups"),
+        Fieldset(_("Additional data"), "additional_fields"),
     )
 
     class Meta:
@@ -150,6 +151,7 @@ class EditGroupForm(ExtensibleForm):
             "parent_groups": ModelSelect2MultipleWidget(
                 search_fields=["name__icontains", "short_name__icontains"]
             ),
+            "additional_fields": ModelSelect2MultipleWidget(search_fields=["title__icontains",]),
         }
 
 
@@ -280,3 +282,11 @@ class GroupPreferenceForm(PreferenceForm):
     """Form to edit preferences valid for members of a group."""
 
     registry = group_preferences_registry
+
+
+class EditAdditionalFieldForm(forms.ModelForm):
+    """Form to manage group types."""
+
+    class Meta:
+        model = AdditionalField
+        exclude = []
diff --git a/aleksis/core/menus.py b/aleksis/core/menus.py
index c91287d01..8bbc074c9 100644
--- a/aleksis/core/menus.py
+++ b/aleksis/core/menus.py
@@ -175,6 +175,17 @@ MENUS = {
                         )
                     ],
                 },
+                {
+                    "name": _("Additional fields"),
+                    "url": "additional_fields",
+                    "icon": "style",
+                    "validators": [
+                        (
+                            "aleksis.core.util.predicates.permission_validator",
+                            "core.view_additionalfield",
+                        )
+                    ],
+                },
             ],
         },
     ],
diff --git a/aleksis/core/migrations/0001_initial.py b/aleksis/core/migrations/0001_initial.py
index 4dd6486f4..413fc88b1 100644
--- a/aleksis/core/migrations/0001_initial.py
+++ b/aleksis/core/migrations/0001_initial.py
@@ -1,16 +1,19 @@
 # Generated by Django 3.0.5 on 2020-05-04 14:16
 
-import aleksis.core.mixins
-import aleksis.core.util.core_helpers
 import datetime
-from django.conf import settings
+
 import django.contrib.postgres.fields.jsonb
 import django.contrib.sites.managers
-from django.db import migrations, models
 import django.db.models.deletion
+from django.conf import settings
+from django.db import migrations, models
+
 import image_cropping.fields
 import phonenumber_field.modelfields
 
+import aleksis.core.mixins
+import aleksis.core.util.core_helpers
+
 
 class Migration(migrations.Migration):
 
diff --git a/aleksis/core/rules.py b/aleksis/core/rules.py
index 5753bbc53..1320e1410 100644
--- a/aleksis/core/rules.py
+++ b/aleksis/core/rules.py
@@ -1,6 +1,6 @@
 from rules import add_perm, always_allow
 
-from .models import Announcement, Group, Person
+from .models import AdditionalField, Announcement, Group, Person
 from .util.predicates import (
     has_any_object,
     has_global_perm,
@@ -194,3 +194,24 @@ change_group_preferences = has_person & (
     | is_group_owner
 )
 add_perm("core.change_group_preferences", change_group_preferences)
+
+# Edit additional field
+edit_additional_field_predicate = has_person & (
+    has_global_perm("core.change_additional_field")
+    | has_object_perm("core.change_additional_field")
+)
+add_perm("core.edit_additional_field", edit_additional_field_predicate)
+
+# Delete additional field
+delete_additional_field_predicate = has_person & (
+    has_global_perm("core.delete_additional_field")
+    | has_object_perm("core.delete_additional_field")
+)
+add_perm("core.delete_additional_field", delete_additional_field_predicate)
+
+# View additional fields
+view_additional_field_predicate = has_person & (
+    has_global_perm("core.view_additionalfield")
+    | has_any_object("core.view_additionalfield", AdditionalField)
+)
+add_perm("core.view_additionalfield", view_additional_field_predicate)
diff --git a/aleksis/core/tables.py b/aleksis/core/tables.py
index 7bc4e4e14..561b9e8b1 100644
--- a/aleksis/core/tables.py
+++ b/aleksis/core/tables.py
@@ -1,3 +1,5 @@
+from django.utils.translation import gettext_lazy as _
+
 import django_tables2 as tables
 from django_tables2.utils import A
 
@@ -20,3 +22,15 @@ class GroupsTable(tables.Table):
 
     name = tables.LinkColumn("group_by_id", args=[A("id")])
     short_name = tables.LinkColumn("group_by_id", args=[A("id")])
+
+
+class AdditionalFieldsTable(tables.Table):
+    """Table to list group types."""
+
+    class Meta:
+        attrs = {"class": "table table-striped table-bordered table-hover table-responsive-xl"}
+
+    title = tables.LinkColumn("edit_additional_field_by_id", args=[A("id")])
+    delete = tables.LinkColumn(
+        "delete_additional_field_by_id", args=[A("id")], verbose_name=_("Delete"), text=_("Delete")
+    )
diff --git a/aleksis/core/templates/core/additional_fields.html b/aleksis/core/templates/core/additional_fields.html
new file mode 100644
index 000000000..1ba2a77a3
--- /dev/null
+++ b/aleksis/core/templates/core/additional_fields.html
@@ -0,0 +1,18 @@
+{# -*- engine:django -*- #}
+
+{% extends "core/base.html" %}
+
+{% load i18n %}
+{% load render_table from django_tables2 %}
+
+{% block browser_title %}{% blocktrans %}Additional fields{% endblocktrans %}{% endblock %}
+{% block page_title %}{% blocktrans %}Additional fields{% endblocktrans %}{% endblock %}
+
+{% block content %}
+  <a class="btn green waves-effect waves-light" href="{% url 'create_additional_field' %}">
+    <i class="material-icons left">add</i>
+    {% trans "Create additional field" %}
+  </a>
+
+  {% render_table additional_fields_table %}
+{% endblock %}
diff --git a/aleksis/core/templates/core/edit_additional_field.html b/aleksis/core/templates/core/edit_additional_field.html
new file mode 100644
index 000000000..b1487eb25
--- /dev/null
+++ b/aleksis/core/templates/core/edit_additional_field.html
@@ -0,0 +1,17 @@
+{# -*- engine:django -*- #}
+
+{% extends "core/base.html" %}
+{% load material_form i18n %}
+
+{% block browser_title %}{% blocktrans %}Edit additional field{% endblocktrans %}{% endblock %}
+{% block page_title %}{% blocktrans %}Edit additional field{% endblocktrans %}{% endblock %}
+
+{% block content %}
+
+  <form method="post">
+    {% csrf_token %}
+    {% form form=edit_additional_field_form %}{% endform %}
+    {% include "core/save_button.html" %}
+  </form>
+
+{% endblock %}
diff --git a/aleksis/core/urls.py b/aleksis/core/urls.py
index ad35fa8c5..8f11b26c8 100644
--- a/aleksis/core/urls.py
+++ b/aleksis/core/urls.py
@@ -27,7 +27,23 @@ urlpatterns = [
     path("person/<int:id_>", views.person, name="person_by_id"),
     path("person/<int:id_>/edit", views.edit_person, name="edit_person_by_id"),
     path("groups", views.groups, name="groups"),
+    path("groups/additional_fields", views.additional_fields, name="additional_fields"),
     path("groups/child_groups/", views.groups_child_groups, name="groups_child_groups"),
+    path(
+        "groups/additional_field/<int:id_>/edit",
+        views.edit_additional_field,
+        name="edit_additional_field_by_id",
+    ),
+    path(
+        "groups/additional_field/create",
+        views.edit_additional_field,
+        name="create_additional_field",
+    ),
+    path(
+        "groups/additional_field/<int:id_>/delete",
+        views.delete_additional_field,
+        name="delete_additional_field_by_id",
+    ),
     path("group/create", views.edit_group, name="create_group"),
     path("group/<int:id_>", views.group, name="group_by_id"),
     path("group/<int:id_>/edit", views.edit_group, name="edit_group_by_id"),
diff --git a/aleksis/core/views.py b/aleksis/core/views.py
index 2b856a048..cbf63defe 100644
--- a/aleksis/core/views.py
+++ b/aleksis/core/views.py
@@ -20,6 +20,7 @@ from .filters import GroupFilter
 from .forms import (
     AnnouncementForm,
     ChildGroupsForm,
+    EditAdditionalFieldForm,
     EditGroupForm,
     EditPersonForm,
     GroupPreferenceForm,
@@ -27,13 +28,13 @@ from .forms import (
     PersonsAccountsFormSet,
     SitePreferenceForm,
 )
-from .models import Announcement, DashboardWidget, Group, Notification, Person
+from .models import AdditionalField, Announcement, DashboardWidget, Group, Notification, Person
 from .registries import (
     group_preferences_registry,
     person_preferences_registry,
     site_preferences_registry,
 )
-from .tables import GroupsTable, PersonsTable
+from .tables import AdditionalFieldsTable, GroupsTable, PersonsTable
 from .util import messages
 from .util.apps import AppConfig
 from .util.core_helpers import objectgetter_optional
@@ -444,3 +445,65 @@ def preferences(
     context["instance"] = instance
 
     return render(request, "dynamic_preferences/form.html", context)
+
+
+@permission_required(
+    "core.edit_additional_field", fn=objectgetter_optional(AdditionalField, None, False)
+)
+def edit_additional_field(request: HttpRequest, id_: Optional[int] = None) -> HttpResponse:
+    """View to edit or create a additional_field."""
+    context = {}
+
+    additional_field = objectgetter_optional(AdditionalField, None, False)(request, id_)
+    context["additional_field"] = additional_field
+
+    if id_:
+        # Edit form for existing additional_field
+        edit_additional_field_form = EditAdditionalFieldForm(
+            request.POST or None, instance=additional_field
+        )
+    else:
+        # Empty form to create a new additional_field
+        edit_additional_field_form = EditAdditionalFieldForm(request.POST or None)
+
+    if request.method == "POST":
+        if edit_additional_field_form.is_valid():
+            edit_additional_field_form.save(commit=True)
+
+            messages.success(request, _("The additional_field has been saved."))
+
+            return redirect("additional_fields")
+
+    context["edit_additional_field_form"] = edit_additional_field_form
+
+    return render(request, "core/edit_additional_field.html", context)
+
+
+@permission_required("core.view_additionalfield")
+def additional_fields(request: HttpRequest) -> HttpResponse:
+    """List view for listing all additional fields."""
+    context = {}
+
+    # Get all additional fields
+    additional_fields = get_objects_for_user(
+        request.user, "core.view_additionalfield", AdditionalField
+    )
+
+    # Build table
+    additional_fields_table = AdditionalFieldsTable(additional_fields)
+    RequestConfig(request).configure(additional_fields_table)
+    context["additional_fields_table"] = additional_fields_table
+
+    return render(request, "core/additional_fields.html", context)
+
+
+@permission_required(
+    "core.delete_additional_field", fn=objectgetter_optional(AdditionalField, None, False)
+)
+def delete_additional_field(request: HttpRequest, id_: int) -> HttpResponse:
+    """View to delete an additional_field."""
+    additional_field = objectgetter_optional(AdditionalField, None, False)(request, id_)
+    additional_field.delete()
+    messages.success(request, _("The additional field has been deleted."))
+
+    return redirect("additional_fields")
-- 
GitLab