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