From d3f826e460cf0245ff0739903682e30140f7cc41 Mon Sep 17 00:00:00 2001
From: George Katsikas <giorgakis.katsikas@gmail.com>
Date: Wed, 21 Feb 2024 13:00:28 +0100
Subject: [PATCH] refactor profile email adding and related actions

add profile emails related permissions

fix #176
---
 scipost_django/profiles/forms.py              |  2 +-
 .../profiles/_hx_add_profile_email_form.html  |  3 +
 .../profiles/_hx_profile_emails_table.html    | 16 ++++
 .../_hx_profile_emails_table_row.html         | 70 ++++++++++++++
 .../templates/profiles/_profile_card.html     | 52 ++---------
 scipost_django/profiles/urls.py               | 25 ++++-
 scipost_django/profiles/views.py              | 91 ++++++++++++-------
 .../commands/add_groups_and_permissions.py    | 39 ++++++++
 8 files changed, 216 insertions(+), 82 deletions(-)
 create mode 100644 scipost_django/profiles/templates/profiles/_hx_add_profile_email_form.html
 create mode 100644 scipost_django/profiles/templates/profiles/_hx_profile_emails_table.html
 create mode 100644 scipost_django/profiles/templates/profiles/_hx_profile_emails_table_row.html

diff --git a/scipost_django/profiles/forms.py b/scipost_django/profiles/forms.py
index f84ca99f7..5bb4ce029 100644
--- a/scipost_django/profiles/forms.py
+++ b/scipost_django/profiles/forms.py
@@ -269,7 +269,7 @@ class AddProfileEmailForm(forms.ModelForm):
         )
         self.helper.attrs = {
             "hx-post": reverse(
-                "profiles:add_profile_email", kwargs={"profile_id": self.profile.id}
+                "profiles:_hx_add_profile_email", kwargs={"profile_id": self.profile.id}
             ),
             "hx-target": "#email-action-container",
         }
diff --git a/scipost_django/profiles/templates/profiles/_hx_add_profile_email_form.html b/scipost_django/profiles/templates/profiles/_hx_add_profile_email_form.html
new file mode 100644
index 000000000..75272438f
--- /dev/null
+++ b/scipost_django/profiles/templates/profiles/_hx_add_profile_email_form.html
@@ -0,0 +1,3 @@
+{% load crispy_forms_tags %}
+
+{% crispy form %}
diff --git a/scipost_django/profiles/templates/profiles/_hx_profile_emails_table.html b/scipost_django/profiles/templates/profiles/_hx_profile_emails_table.html
new file mode 100644
index 000000000..87ada40c2
--- /dev/null
+++ b/scipost_django/profiles/templates/profiles/_hx_profile_emails_table.html
@@ -0,0 +1,16 @@
+<table id="profile-emails-table" class="table table-sm table-borderless">
+  <thead>
+    <tr>
+      <th colspan="2">Email</th>
+      <th>Valid</th>
+      <th>Verified</th>
+      <th>Added by</th>
+      <th>Mark as</th>
+    </tr>
+  </thead>
+
+  {% for profile_mail in profile.emails.all %}
+    {% include "profiles/_hx_profile_emails_table_row.html" %}
+  {% endfor %}
+
+</table>
diff --git a/scipost_django/profiles/templates/profiles/_hx_profile_emails_table_row.html b/scipost_django/profiles/templates/profiles/_hx_profile_emails_table_row.html
new file mode 100644
index 000000000..8898e567c
--- /dev/null
+++ b/scipost_django/profiles/templates/profiles/_hx_profile_emails_table_row.html
@@ -0,0 +1,70 @@
+<tr>
+  <td class=" 
+    {% if profile_mail.primary %}fw-bold{% endif %}
+     ">{{ profile_mail.email }}</td>
+  <td>{{ profile_mail.primary|yesno:'Primary,Alternative' }}</td>
+  <td>
+
+    {% if profile_mail.still_valid %}
+      <span class="text-success">{% include "bi/check-circle-fill.html" %}</span>
+    {% else %}
+      <span class="text-danger">{% include "bi/x-circle-fill.html" %}</span>
+    {% endif %}
+
+  </td>
+  <td>
+
+    {% if profile_mail.verified %}
+      <span class="text-success">{% include "bi/check-circle-fill.html" %}</span>
+    {% else %}
+      <span class="text-danger">{% include "bi/x-circle-fill.html" %}</span>
+    {% endif %}
+
+  </td>
+  <td>
+
+    {% if profile_mail.added_by %}{{ profile_mail.added_by }}{% endif %}
+
+  </td>
+
+  {% if perms.scipost.can_validate_profile_emails %}
+    <td class="d-flex justify-content-between">
+      <button type="button"
+              class="btn btn-sm btn-light py-0"
+              hx-target="closest tr"
+              hx-swap="outerHTML"
+              hx-patch="{% url 'profiles:_hx_profile_email_toggle_valid' profile_mail.id %}">
+        {{ profile_mail.still_valid|yesno:'Depr.,Valid' }}
+      </button>
+    {% endif %}
+
+    {% if perms.scipost.can_verify_profile_emails %}
+      <button type="button"
+              class="btn btn-sm btn-light py-0"
+              hx-target="closest tr"
+              hx-swap="outerHTML"
+              hx-patch="{% url 'profiles:_hx_profile_email_toggle_verified' profile_mail.id %}">
+        {{ profile_mail.verified|yesno:'Unverified,Verified' }}
+      </button>
+    {% endif %}
+
+    {% if perms.scipost.can_mark_profile_emails_primary %}
+      <button type="button"
+              class="btn btn-sm btn-light py-0"
+              hx-target="closest table"
+              hx-swap="outerHTML"
+              hx-patch="{% url 'profiles:_hx_profile_email_mark_primary' profile_mail.id %}">Primary</button>
+    {% endif %}
+
+    {% if perms.scipost.can_delete_profile_emails %}
+      <button type="button"
+              class="btn py-0"
+              hx-target="closest tr"
+              hx-delete="{% url 'profiles:_hx_profile_email_delete' profile_mail.id %}"
+              hx-confirm="Are you sure you want to delete this email?">
+        <span class="text-danger">{% include 'bi/trash-fill.html' %}</span>
+      </button>
+    {% endif %}
+
+  </td>
+</tr>
diff --git a/scipost_django/profiles/templates/profiles/_profile_card.html b/scipost_django/profiles/templates/profiles/_profile_card.html
index c721561e1..e617a636c 100644
--- a/scipost_django/profiles/templates/profiles/_profile_card.html
+++ b/scipost_django/profiles/templates/profiles/_profile_card.html
@@ -28,50 +28,16 @@
 	  {% include 'profiles/_affiliations_table.html' with profile=profile actions=True %}
 	</td>
 	<tr>
-	  <td>Email(s)</td>
+    <td>Email(s)
+      <ul>
+        {% if perms.scipost.can_add_profile_emails %}
+	        <li><a role="button" type="button" class="btn-link" hx-get="{% url 'profiles:_hx_add_profile_email' profile_id=profile.id %}" hx-target="#email-action-container">Add a new Email</a></li>
+        {% endif %}
+      </ul>
+      <div id="email-action-container"></div>
+    </td>
 	  <td>
-	    <table class="table table-sm">
-              <thead>
-		<tr>
-                  <th colspan="2">Email</th>
-                  <th>Still valid</th>
-                  <th></th>
-		</tr>
-              </thead>
-    	      {% for profile_mail in profile.emails.all %}
-		<tr>
-		  <td>{% if profile_mail.primary %}<strong>{% endif %}{{ profile_mail.email }}{% if profile_mail.primary %}</strong>{% endif %}</td>
-		  <td>{{ profile_mail.primary|yesno:'Primary,Alternative' }}</td>
-		  <td>
-		    {% if profile_mail.still_valid %}<span class="text-success">{% include 'bi/check-circle-fill.html' %}</span>{% else %}<span class="text-danger">{% include 'bi/x-circle-fill.html' %}</span>{% endif %}
-		  </td>
-		  <td class="d-flex">
-                    <form method="post" action="{% url 'profiles:toggle_email_status' profile_mail.id %}">{% csrf_token %}<button type="submit" class="btn btn-link py-0">{{ profile_mail.still_valid|yesno:'Deprecate,Mark valid' }}</button></form>
-		    <form method="post" action="{% url 'profiles:email_make_primary' profile_mail.id %}">{% csrf_token %}<button type="submit" class="btn btn-link py-0">Make primary</button></form>
-		    <a type="button" class="btn py-0" data-bs-toggle="modal" data-bs-target="#confirmDeleteEmailModal-{{ profile_mail.id }}"><span class="text-danger">{% include 'bi/trash-fill.html' %}</span></a>
-		    <div class="modal" id="confirmDeleteEmailModal-{{ profile_mail.id }}" tabindex="-1" role="dialog">
-		      <div class="modal-dialog" role="document">
-			<div class="modal-content">
-			  <div class="modal-header">
-			    <h4 class="modal-title">Confirm email deletion</h4>
-			    <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close">
-			      <span aria-hidden="true">&times;</span>
-			    </button>
-			  </div>
-			  <div class="modal-body">
-			    Are you sure you want to delete {{ profile_mail.email }}?
-			  </div>
-			  <div class="modal-footer">
-			    <button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
-			    <form method="post" action="{% url 'profiles:delete_profile_email' profile_mail.id %}">{% csrf_token %}<button type="submit" class="btn btn-danger">Confirm</button></form>
-			  </div>
-			</div>
-		      </div>
-		    </div>
-		  </td>
-		</tr>
-    	      {% endfor %}
-            </table>
+      {% include 'profiles/_hx_profile_emails_table.html' %}
 	  </td>
 	</tr>
 	<tr>
diff --git a/scipost_django/profiles/urls.py b/scipost_django/profiles/urls.py
index e5bfef128..b64b911a8 100644
--- a/scipost_django/profiles/urls.py
+++ b/scipost_django/profiles/urls.py
@@ -75,17 +75,34 @@ urlpatterns = [
     ),
     # Emails
     path(
-        "<int:profile_id>/add_email", views.add_profile_email, name="add_profile_email"
+        "<int:profile_id>/add_email",
+        views._hx_add_profile_email,
+        name="_hx_add_profile_email",
     ),
     path(
         "emails/<int:email_id>/",
         include(
             [
                 path(
-                    "make_primary", views.email_make_primary, name="email_make_primary"
+                    "make_primary",
+                    views._hx_profile_email_mark_primary,
+                    name="_hx_profile_email_mark_primary",
+                ),
+                path(
+                    "toggle_valid",
+                    views._hx_profile_email_toggle_valid,
+                    name="_hx_profile_email_toggle_valid",
+                ),
+                path(
+                    "toggle_verified",
+                    views._hx_profile_email_toggle_verified,
+                    name="_hx_profile_email_toggle_verified",
+                ),
+                path(
+                    "delete",
+                    views._hx_profile_email_delete,
+                    name="_hx_profile_email_delete",
                 ),
-                path("toggle", views.toggle_email_status, name="toggle_email_status"),
-                path("delete", views.delete_profile_email, name="delete_profile_email"),
             ]
         ),
     ),
diff --git a/scipost_django/profiles/views.py b/scipost_django/profiles/views.py
index 06d72a8a4..249d5ca5d 100644
--- a/scipost_django/profiles/views.py
+++ b/scipost_django/profiles/views.py
@@ -2,17 +2,15 @@ __copyright__ = "Copyright © Stichting SciPost (SciPost Foundation)"
 __license__ = "AGPL v3"
 
 
-from functools import reduce
-import re
 from django.contrib import messages
 from django.contrib.auth.decorators import login_required
 from django.contrib.auth.mixins import UserPassesTestMixin
+from django.template.response import TemplateResponse
 from django.urls import reverse, reverse_lazy
 from django.db import transaction
 from django.db.models import Q
-from django.http import Http404, HttpResponseRedirect
+from django.http import Http404, HttpResponse
 from django.shortcuts import get_object_or_404, render, redirect
-from django.views.decorators.http import require_POST
 from django.views.generic.detail import DetailView
 from django.views.generic.edit import CreateView, UpdateView, DeleteView
 from django.views.generic.list import ListView
@@ -431,57 +429,82 @@ def _hx_profile_specialties(request, profile_id):
     return render(request, "profiles/_hx_profile_specialties.html", context)
 
 
-@permission_required("scipost.can_create_profiles")
-def add_profile_email(request, profile_id):
+@permission_required_htmx("scipost.can_add_profile_emails")
+def _hx_add_profile_email(request, profile_id):
     """
     Add an email address to a Profile.
     """
     profile = get_object_or_404(Profile, pk=profile_id)
     form = AddProfileEmailForm(request.POST or None, profile=profile, request=request)
     if form.is_valid():
-        form.save()
-        messages.success(request, "Email successfully added.")
-    else:
-        for field, err in form.errors.items():
-            messages.warning(request, err[0])
-    if request.POST.get("next", None):
-        return HttpResponseRedirect(request.POST.get("next"))
-    return redirect(profile.get_absolute_url())
+        profile_email = form.save()
+        response = TemplateResponse(
+            request,
+            "profiles/_hx_profile_emails_table_row.html",
+            {"profile_mail": profile_email},
+        )
+        response["HX-Retarget"] = "#profile-emails-table"
+        response["HX-Reswap"] = "beforeend"
 
+        return response
 
-@require_POST
-@permission_required("scipost.can_create_profiles")
-def email_make_primary(request, email_id):
+    return TemplateResponse(
+        request, "profiles/_hx_add_profile_email_form.html", {"form": form}
+    )
+
+
+@permission_required_htmx("scipost.can_mark_profile_emails_primary")
+def _hx_profile_email_mark_primary(request, email_id):
     """
     Make this email the primary one for this Profile.
     """
     profile_email = get_object_or_404(ProfileEmail, pk=email_id)
-    ProfileEmail.objects.filter(profile=profile_email.profile).update(primary=False)
-    profile_email.primary = True
-    profile_email.save()
-    return redirect(profile_email.profile.get_absolute_url())
+    if request.method == "PATCH":
+        ProfileEmail.objects.filter(profile=profile_email.profile).update(primary=False)
+        profile_email.primary = True
+        profile_email.save()
+    return TemplateResponse(
+        request,
+        "profiles/_hx_profile_emails_table.html",
+        {"profile": profile_email.profile},
+    )
 
 
-@require_POST
-@permission_required("scipost.can_create_profiles")
-def toggle_email_status(request, email_id):
+@permission_required_htmx("scipost.can_validate_profile_emails")
+def _hx_profile_email_toggle_valid(request, email_id):
     """Toggle valid/deprecated status of ProfileEmail."""
     profile_email = get_object_or_404(ProfileEmail, pk=email_id)
-    ProfileEmail.objects.filter(id=email_id).update(
-        still_valid=not profile_email.still_valid
+    if request.method == "PATCH":
+        profile_email.still_valid = not profile_email.still_valid
+        profile_email.save()
+    return TemplateResponse(
+        request,
+        "profiles/_hx_profile_emails_table_row.html",
+        {"profile_mail": profile_email},
     )
-    messages.success(request, "Email updated")
-    return redirect(profile_email.profile.get_absolute_url())
 
 
-@require_POST
-@permission_required("scipost.can_create_profiles")
-def delete_profile_email(request, email_id):
+@permission_required_htmx("scipost.can_verify_profile_emails")
+def _hx_profile_email_toggle_verified(request, email_id):
+    """Toggle verified/unverified status of ProfileEmail."""
+    profile_email = get_object_or_404(ProfileEmail, pk=email_id)
+    if request.method == "PATCH":
+        profile_email.verified = not profile_email.verified
+        profile_email.save()
+    return TemplateResponse(
+        request,
+        "profiles/_hx_profile_emails_table_row.html",
+        {"profile_mail": profile_email},
+    )
+
+
+@permission_required_htmx("scipost.can_delete_profile_emails")
+def _hx_profile_email_delete(request, email_id):
     """Delete ProfileEmail."""
     profile_email = get_object_or_404(ProfileEmail, pk=email_id)
-    profile_email.delete()
-    messages.success(request, "Email deleted")
-    return redirect(profile_email.profile.get_absolute_url())
+    if request.method == "DELETE":
+        profile_email.delete()
+    return HttpResponse("")
 
 
 class AffiliationCreateView(UserPassesTestMixin, CreateView):
diff --git a/scipost_django/scipost/management/commands/add_groups_and_permissions.py b/scipost_django/scipost/management/commands/add_groups_and_permissions.py
index 4bad7bd01..36199372d 100644
--- a/scipost_django/scipost/management/commands/add_groups_and_permissions.py
+++ b/scipost_django/scipost/management/commands/add_groups_and_permissions.py
@@ -124,6 +124,32 @@ class Command(BaseCommand):
             name="Can add affiliations to Profiles",
             content_type=content_type,
         )
+        can_add_profile_emails, created = Permission.objects.get_or_create(
+            codename="can_add_profile_emails",
+            name="Can add emails to Profiles",
+            content_type=content_type,
+        )
+        can_validate_profile_emails, created = Permission.objects.get_or_create(
+            codename="can_validate_profile_emails",
+            name="Can set emails of Profiles as still valid",
+            content_type=content_type,
+        )
+        can_verify_profile_emails, created = Permission.objects.get_or_create(
+            codename="can_verify_profile_emails",
+            name="Can verify emails of Profiles",
+            content_type=content_type,
+        )
+        can_mark_profile_emails_primary, created = Permission.objects.get_or_create(
+            codename="can_mark_profile_emails_primary",
+            name="Can mark emails of Profiles as primary",
+            content_type=content_type,
+        )
+        can_delete_profile_emails, created = Permission.objects.get_or_create(
+            codename="can_delete_profile_emails",
+            name="Can delete emails of Profiles",
+            content_type=content_type,
+        )
+
         can_merge_profiles, created = Permission.objects.get_or_create(
             codename="can_merge_profiles",
             name="Can merge Profiles",
@@ -469,6 +495,11 @@ class Command(BaseCommand):
                 can_create_profiles,
                 can_view_profiles,
                 can_add_profile_affiliations,
+                can_add_profile_emails,
+                can_verify_profile_emails,
+                can_validate_profile_emails,
+                can_mark_profile_emails_primary,
+                can_delete_profile_emails,
                 can_merge_profiles,
                 can_merge_contributors,
                 can_manage_ontology,
@@ -533,6 +564,11 @@ class Command(BaseCommand):
                 can_create_profiles,
                 can_view_profiles,
                 can_add_profile_affiliations,
+                can_add_profile_emails,
+                can_verify_profile_emails,
+                can_validate_profile_emails,
+                can_mark_profile_emails_primary,
+                can_delete_profile_emails,
                 can_merge_profiles,
                 can_merge_contributors,
                 can_manage_ontology,
@@ -552,6 +588,8 @@ class Command(BaseCommand):
                 can_create_profiles,
                 can_view_profiles,
                 can_add_profile_affiliations,
+                can_add_profile_emails,
+                can_validate_profile_emails,
                 can_attend_VGMs,
                 can_view_statistics,
                 can_manage_ontology,
@@ -623,6 +661,7 @@ class Command(BaseCommand):
             [
                 can_view_profiles,
                 can_add_profile_affiliations,
+                can_add_profile_emails,
                 can_assign_production_officer,
                 can_take_decisions_related_to_proofs,
                 # can_draft_publication,
-- 
GitLab