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">×</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