From 717996ca1c13bed802dfbf1edf4a81a65f1dc398 Mon Sep 17 00:00:00 2001
From: George Katsikas <giorgakis.katsikas@gmail.com>
Date: Wed, 17 Apr 2024 10:06:11 +0200
Subject: [PATCH] htmxify recommendation vote form add warning about decoupling
 of votes and remarks

fixes #141
---
 .../pool/_hx_recommendation_vote_form.html    |  66 ++++++
 .../_hx_recommendation_remarks.html           |   2 +-
 .../submissions/pool/recommendation.html      | 126 +++++------
 scipost_django/submissions/urls/__init__.py   |   5 +
 scipost_django/submissions/views/__init__.py  | 201 ++++++++++--------
 5 files changed, 243 insertions(+), 157 deletions(-)
 create mode 100644 scipost_django/submissions/templates/submissions/pool/_hx_recommendation_vote_form.html

diff --git a/scipost_django/submissions/templates/submissions/pool/_hx_recommendation_vote_form.html b/scipost_django/submissions/templates/submissions/pool/_hx_recommendation_vote_form.html
new file mode 100644
index 000000000..a720869ae
--- /dev/null
+++ b/scipost_django/submissions/templates/submissions/pool/_hx_recommendation_vote_form.html
@@ -0,0 +1,66 @@
+{% load bootstrap %}
+
+<h3 class="mt-4" id="votingForm">Your position on this recommendation</h3>
+
+{% if form.errors %}
+  <h1 class="text-danger">Warning: there was an error filling the voting form</h1>
+
+  {% for field in form %}
+
+    {% for error in field.errors %}
+      <div class="alert alert-danger">
+        <strong>{{ error|escape }}</strong>
+      </div>
+    {% endfor %}
+  {% endfor %}
+
+  {% for error in form.non_field_errors %}
+    <div class="alert alert-danger">
+      <strong>{{ error|escape }}</strong>
+    </div>
+  {% endfor %}
+
+  <p class="text-danger">
+    Please <a href="#votingForm">go back to the form</a> and try again!
+  </p>
+{% endif %}
+
+{% if previous_vote %}
+  <p>
+    You had previously voted <span class="text-danger">{{ previous_vote }}</span>; you can use the form below to change your vote:
+  </p>
+{% endif %}
+
+<form hx-post="{{ request.get_full_path }}" hx-target="closest section">
+  <p id="agree_instructions">
+
+    {% if recommendation.recommendation == 1 %}
+      <strong class="text-success">If you agree with a recommendation to publish, you can provide your ballpark quality tiering below</strong>
+      <br />
+      (this is not compulsory, but most welcome)
+    {% endif %}
+
+  </p>
+  <p id="disagree_instructions" class="text-danger">
+    <strong>If you vote disagree, please provide an alternative recommendation below</strong>
+  </p>
+  {% csrf_token %}
+  {{ form|bootstrap }}
+  <div class="row">
+    <div class="col-12 col-md">
+      <p class="bg-warning bg-opacity-10 p-2">
+        <span class="text-warning">{% include "bi/exclamation-triangle-fill.html" %}</span>
+        <strong>Important:</strong> Since October 2023, the remarks have been decoupled from the recommendation vote, allowing the two to be submitted independently.
+        <br />
+        <strong>Add your remarks before casting your vote</strong>, otherwise your vote will be submitted without any remarks.
+      </p>
+    </div>
+    <div class="col-12 col-md-auto">
+      <input type="submit"
+             name="submit"
+             value="Cast your vote"
+             class="btn btn-primary w-100"
+             id="submit-id-submit" />
+    </div>
+  </div>
+</form>
diff --git a/scipost_django/submissions/templates/submissions/pool/decisionmaking/_hx_recommendation_remarks.html b/scipost_django/submissions/templates/submissions/pool/decisionmaking/_hx_recommendation_remarks.html
index 0222c4c7b..d618e9b18 100644
--- a/scipost_django/submissions/templates/submissions/pool/decisionmaking/_hx_recommendation_remarks.html
+++ b/scipost_django/submissions/templates/submissions/pool/decisionmaking/_hx_recommendation_remarks.html
@@ -15,7 +15,7 @@
     </li>
   {% endfor %}
 
-  <h5 class="mt-4">Add a new Remark to this recommendation</h5>
+  <h6 class="mt-4">Add a new Remark to this recommendation</h6>
   {% crispy form %}
 
 </ul>
diff --git a/scipost_django/submissions/templates/submissions/pool/recommendation.html b/scipost_django/submissions/templates/submissions/pool/recommendation.html
index 37a45c8d1..a4ded134b 100644
--- a/scipost_django/submissions/templates/submissions/pool/recommendation.html
+++ b/scipost_django/submissions/templates/submissions/pool/recommendation.html
@@ -1,6 +1,5 @@
 {% extends 'submissions/pool/base.html' %}
 
-{% load bootstrap %}
 {% load scipost_extras %}
 
 {% block breadcrumb_items %}
@@ -8,27 +7,13 @@
   <span class="breadcrumb-item">Editorial Recommendation</span>
 {% endblock %}
 
-{% block pagetitle %}: Editorial Recommendation{% endblock pagetitle %}
+{% block pagetitle %}
+  : Editorial Recommendation
+{% endblock pagetitle %}
 
 {% block content %}
 
 
-  {% if voting_form.errors %}
-    <h1 class="text-danger">Warning: there was an error filling the voting form</h1>
-    {% for field in voting_form %}
-      {% for error in field.errors %}
-        <div class="alert alert-danger">
-          <strong>{{ error|escape }}</strong>
-        </div>
-      {% endfor %}
-    {% endfor %}
-    {% for error in voting_form.non_field_errors %}
-      <div class="alert alert-danger">
-        <strong>{{ error|escape }}</strong>
-      </div>
-    {% endfor %}
-    <p class="text-danger">Please <a href="#votingForm">go back to the form</a> and try again!</p>
-  {% endif %}
 
 
   <h1 class="highlight">Editorial Recommendation</h1>
@@ -36,73 +21,70 @@
   <h2>Concerning Submission:</h2>
   {% include 'submissions/_submission_li.html' with submission=recommendation.submission %}
 
-  <a class="d-inline-block mb-3" href="{{ recommendation.submission.get_absolute_url }}" target="_blank">View Reports and Submission details</a>
+  <a class="d-inline-block mb-3"
+     href="{{ recommendation.submission.get_absolute_url }}"
+     target="_blank">View Reports and Submission details</a>
 
   {% include 'submissions/pool/_submission_info_table.html' with submission=recommendation.submission %}
-  <br>
+  <br />
 
   {% include 'submissions/_previous_recommendations_card_fellow_content.html' with recommendation=recommendation %}
 
   <h2 class="highlight">Editorial Recommendation (latest)</h2>
   {% include 'submissions/_recommendation_fellow_content.html' with recommendation=recommendation %}
 
+  <section id="recommendation-vote-form-section"
+           hx-get="{% url "submissions:_hx_recommendation_vote_form" rec_id=recommendation.id %}"
+           hx-trigger="load once">
+  </section>
 
-  {% if voting_form %}
-    <h3 class="mt-4" id="votingForm">Your position on this recommendation</h3>
-    {% if previous_vote %}
-      <p>You had previously voted <span class="text-danger">{{ previous_vote }}</span>; you can use the form below to change your vote:</p>
-    {% endif %}
-    <form action="{% url 'submissions:vote_on_rec' rec_id=recommendation.id %}" method="post">
-      <p id="agree_instructions">
-	{% if recommendation.recommendation == 1 %}
-	  <strong class="text-success">If you agree with a recommendation to publish, you can provide your ballpark quality tiering below</strong><br>(this is not compulsory, but most welcome)
-	{% endif %}
-      </p>
-      <p id="disagree_instructions" class="text-danger"><strong>If you vote disagree, please provide an alternative recommendation below</strong></p>
-      {% csrf_token %}
-      {{ voting_form|bootstrap }}
-      <input type="submit" name="submit" value="Cast your vote" class="btn btn-primary" id="submit-id-submit">
-    </form>
-  {% endif %}
 
 {% endblock %}
 
 {% block footer_script %}
   <script nonce="{{ request.csp_nonce }}">
-   $(document).ready(function(){
-       $("#agree_instructions").hide()
-       $("#disagree_instructions").hide()
-       $("input[name=tier]").parents('.form-group').hide()
-       $("#id_alternative_for_journal").parents('.form-group').hide()
-       $("#id_alternative_recommendation").parents('.form-group').hide()
-       $('input[name=vote]').on('change', function(){
-	   var selection = $('input[name=vote]:checked').val();
-	   switch(selection){
-	       case "agree":
-		   $("#agree_instructions").show()
-		   $("#disagree_instructions").hide()
-		   {% if recommendation.recommendation == 1 %}
-		   $("input[name=tier]").parents('.form-group').show()
-		   {% endif %}
-		   $("#id_alternative_for_journal").parents('.form-group').hide()
-		   $("#id_alternative_recommendation").parents('.form-group').hide()
-		   break;
-	       case "disagree":
-		   $("#agree_instructions").hide()
-		   $("#disagree_instructions").show()
-		   $("input[name=tier]").parents('.form-group').hide()
-		   $("#id_alternative_for_journal").parents('.form-group').show()
-		   $("#id_alternative_recommendation").parents('.form-group').show()
-		   break;
-	       default:
-		   $("#agree_instructions").hide()
-		   $("#disagree_instructions").hide()
-		   $("input[name=tier]").parents('.form-group').hide()
-		   $("#id_alternative_for_journal").parents('.form-group').hide()
-		   $("#id_alternative_recommendation").parents('.form-group').hide()
-		   break;
-	   }
-       }).trigger('change');
-   });
+    function update_vote_form_fields(){
+      var selection = $('input[name=vote]:checked').val();
+      switch(selection){
+        case "agree":
+          $("#agree_instructions").show()
+          $("#disagree_instructions").hide()
+          {% if recommendation.recommendation == 1 %}
+          $("input[name=tier]").parents('.form-group').show()
+          {% endif %}
+          $("#id_alternative_for_journal").parents('.form-group').hide()
+          $("#id_alternative_recommendation").parents('.form-group').hide()
+          break;
+        case "disagree":
+          $("#agree_instructions").hide()
+          $("#disagree_instructions").show()
+          $("input[name=tier]").parents('.form-group').hide()
+          $("#id_alternative_for_journal").parents('.form-group').show()
+          $("#id_alternative_recommendation").parents('.form-group').show()
+          break;
+        default:
+          $("#agree_instructions").hide()
+          $("#disagree_instructions").hide()
+          $("input[name=tier]").parents('.form-group').hide()
+          $("#id_alternative_for_journal").parents('.form-group').hide()
+          $("#id_alternative_recommendation").parents('.form-group').hide()
+          break;
+      }
+    }
+    function add_interactivity(){
+      $("#agree_instructions").hide()
+      $("#disagree_instructions").hide()
+      $("input[name=tier]").parents('.form-group').hide()
+      $("#id_alternative_for_journal").parents('.form-group').hide()
+      $("#id_alternative_recommendation").parents('.form-group').hide()
+
+
+      setTimeout(function(){
+        $('input[name=vote]').on('change', update_vote_form_fields).trigger('change');
+     }, 1000);
+    }
+
+   $('#recommendation-vote-form-section').on('htmx:afterSettle', add_interactivity);
+   $(document).ready(setTimeout(add_interactivity, 1000));
   </script>
 {% endblock %}
diff --git a/scipost_django/submissions/urls/__init__.py b/scipost_django/submissions/urls/__init__.py
index dfa86aff3..e05af836e 100644
--- a/scipost_django/submissions/urls/__init__.py
+++ b/scipost_django/submissions/urls/__init__.py
@@ -432,6 +432,11 @@ urlpatterns = [
         name="prepare_for_voting",
     ),
     path("vote_on_rec/<int:rec_id>", views.vote_on_rec, name="vote_on_rec"),
+    path(
+        "_hx_recommendation_vote_form/<int:rec_id>",
+        views._hx_recommendation_vote_form,
+        name="_hx_recommendation_vote_form",
+    ),
     path(
         "claim_voting_right/<int:rec_id>",
         views.claim_voting_right,
diff --git a/scipost_django/submissions/views/__init__.py b/scipost_django/submissions/views/__init__.py
index 67e26e3e7..29eb22637 100644
--- a/scipost_django/submissions/views/__init__.py
+++ b/scipost_django/submissions/views/__init__.py
@@ -2500,93 +2500,9 @@ def vote_on_rec(request, rec_id):
                 previous_vote = "abstain"
         except EICRecommendation.DoesNotExist:
             raise Http404
-    initial = {"vote": previous_vote}
-
-    if request.POST:
-        form = RecommendationVoteForm(request.POST)
-    else:
-        form = RecommendationVoteForm(initial=initial)
-    if form.is_valid():
-        # Delete previous tierings and alternative recs, irrespective of the vote
-        SubmissionTiering.objects.filter(
-            submission=recommendation.submission, fellow=request.user.contributor
-        ).delete()
-        AlternativeRecommendation.objects.filter(
-            eicrec=recommendation, fellow=request.user.contributor
-        ).delete()
-        if form.cleaned_data["vote"] == "agree":
-            try:
-                recommendation.voted_for.add(request.user.contributor)
-            except IntegrityError:
-                messages.warning(
-                    request, "You have already voted for this Recommendation."
-                )
-            recommendation.voted_against.remove(request.user.contributor)
-            recommendation.voted_abstain.remove(request.user.contributor)
-            # Add a tiering if form filled in:
-            if (
-                recommendation.recommendation == EIC_REC_PUBLISH
-                and form.cleaned_data["tier"] != ""
-            ):
-                tiering = SubmissionTiering(
-                    submission=recommendation.submission,
-                    fellow=request.user.contributor,
-                    for_journal=recommendation.for_journal,
-                    tier=form.cleaned_data["tier"],
-                )
-                tiering.save()
-        elif form.cleaned_data["vote"] == "disagree":
-            recommendation.voted_for.remove(request.user.contributor)
-            try:
-                recommendation.voted_against.add(request.user.contributor)
-            except IntegrityError:
-                messages.warning(
-                    request, "You have already voted against this Recommendation."
-                )
-            recommendation.voted_abstain.remove(request.user.contributor)
-            # Create an alternative recommendation, if given
-            if (
-                form.cleaned_data["alternative_for_journal"]
-                and form.cleaned_data["alternative_recommendation"]
-            ):
-                altrec = AlternativeRecommendation(
-                    eicrec=recommendation,
-                    fellow=request.user.contributor,
-                    for_journal=form.cleaned_data["alternative_for_journal"],
-                    recommendation=form.cleaned_data["alternative_recommendation"],
-                )
-                altrec.save()
-        elif form.cleaned_data["vote"] == "abstain":
-            recommendation.voted_for.remove(request.user.contributor)
-            recommendation.voted_against.remove(request.user.contributor)
-            try:
-                recommendation.voted_abstain.add(request.user.contributor)
-            except IntegrityError:
-                messages.warning(
-                    request, "You have already abstained on this Recommendation."
-                )
-                pass
-        votechanged = previous_vote and form.cleaned_data["vote"] != previous_vote
-        if votechanged:
-            remark = Remark(
-                contributor=request.user.contributor,
-                recommendation=recommendation,
-                date=timezone.now(),
-                remark="Note from EdAdmin: {full_name} changed vote from {previous} to {current}".format(
-                    full_name=request.user.get_full_name(),
-                    previous=previous_vote,
-                    current=form.cleaned_data["vote"],
-                ),
-            )
-            remark.save()
-        recommendation.save()
-        messages.success(request, "Thank you for your vote.")
-        return redirect(reverse("submissions:pool:pool"))
 
     context = {
         "recommendation": recommendation,
-        "voting_form": form,
-        "previous_vote": previous_vote,
     }
     return render(request, "submissions/pool/recommendation.html", context)
 
@@ -2904,6 +2820,123 @@ class EICRecommendationDetailView(
         return self.submission.eicrecommendations.last()
 
 
+@login_required
+@fellowship_or_admin_required()
+@transaction.atomic
+def _hx_recommendation_vote_form(request, rec_id):
+    """Form view for Fellows to cast their vote on EICRecommendation."""
+    submissions = Submission.objects.in_pool(request.user)
+    previous_vote = None
+    try:
+        recommendation = EICRecommendation.objects.user_must_vote_on(request.user).get(
+            submission__in=submissions, id=rec_id
+        )
+        initial = {"vote": "abstain"}
+    except EICRecommendation.DoesNotExist:  # Try to find an EICRec already voted on:
+        try:
+            recommendation = EICRecommendation.objects.user_current_voted(
+                request.user
+            ).get(submission__in=submissions, id=rec_id)
+            if request.user.contributor in recommendation.voted_for.all():
+                previous_vote = "agree"
+            elif request.user.contributor in recommendation.voted_against.all():
+                previous_vote = "disagree"
+            elif request.user.contributor in recommendation.voted_abstain.all():
+                previous_vote = "abstain"
+        except EICRecommendation.DoesNotExist:
+            raise Http404
+    initial = {"vote": previous_vote}
+
+    if request.POST:
+        form = RecommendationVoteForm(request.POST)
+    else:
+        form = RecommendationVoteForm(initial=initial)
+    if form.is_valid():
+        # Delete previous tierings and alternative recs, irrespective of the vote
+        SubmissionTiering.objects.filter(
+            submission=recommendation.submission, fellow=request.user.contributor
+        ).delete()
+        AlternativeRecommendation.objects.filter(
+            eicrec=recommendation, fellow=request.user.contributor
+        ).delete()
+        if form.cleaned_data["vote"] == "agree":
+            try:
+                recommendation.voted_for.add(request.user.contributor)
+            except IntegrityError:
+                messages.warning(
+                    request, "You have already voted for this Recommendation."
+                )
+            recommendation.voted_against.remove(request.user.contributor)
+            recommendation.voted_abstain.remove(request.user.contributor)
+            # Add a tiering if form filled in:
+            if (
+                recommendation.recommendation == EIC_REC_PUBLISH
+                and form.cleaned_data["tier"] != ""
+            ):
+                tiering = SubmissionTiering(
+                    submission=recommendation.submission,
+                    fellow=request.user.contributor,
+                    for_journal=recommendation.for_journal,
+                    tier=form.cleaned_data["tier"],
+                )
+                tiering.save()
+        elif form.cleaned_data["vote"] == "disagree":
+            recommendation.voted_for.remove(request.user.contributor)
+            try:
+                recommendation.voted_against.add(request.user.contributor)
+            except IntegrityError:
+                messages.warning(
+                    request, "You have already voted against this Recommendation."
+                )
+            recommendation.voted_abstain.remove(request.user.contributor)
+            # Create an alternative recommendation, if given
+            if (
+                form.cleaned_data["alternative_for_journal"]
+                and form.cleaned_data["alternative_recommendation"]
+            ):
+                altrec = AlternativeRecommendation(
+                    eicrec=recommendation,
+                    fellow=request.user.contributor,
+                    for_journal=form.cleaned_data["alternative_for_journal"],
+                    recommendation=form.cleaned_data["alternative_recommendation"],
+                )
+                altrec.save()
+        elif form.cleaned_data["vote"] == "abstain":
+            recommendation.voted_for.remove(request.user.contributor)
+            recommendation.voted_against.remove(request.user.contributor)
+            try:
+                recommendation.voted_abstain.add(request.user.contributor)
+            except IntegrityError:
+                messages.warning(
+                    request, "You have already abstained on this Recommendation."
+                )
+                pass
+        votechanged = previous_vote and form.cleaned_data["vote"] != previous_vote
+        if votechanged:
+            remark = Remark(
+                contributor=request.user.contributor,
+                recommendation=recommendation,
+                date=timezone.now(),
+                remark="Note from EdAdmin: {full_name} changed vote from {previous} to {current}".format(
+                    full_name=request.user.get_full_name(),
+                    previous=previous_vote,
+                    current=form.cleaned_data["vote"],
+                ),
+            )
+            remark.save()
+        recommendation.save()
+        messages.success(request, "Thank you for your vote.")
+
+    context = {
+        "recommendation": recommendation,
+        "form": form,
+        "previous_vote": previous_vote,
+    }
+    return TemplateResponse(
+        request, "submissions/pool/_hx_recommendation_vote_form.html", context
+    )
+
+
 class EditorialDecisionCreateView(SubmissionMixin, PermissionsMixin, CreateView):
     """For EdAdmin to create the editorial decision on a Submission, after voting is completed.
 
-- 
GitLab