SciPost Code Repository

Skip to content
Snippets Groups Projects
Commit 86425b5c authored by Jean-Sébastien Caux's avatar Jean-Sébastien Caux
Browse files

Refactor motion voting

parent 2a9b5e4e
No related branches found
No related tags found
No related merge requests found
......@@ -5,11 +5,12 @@ __license__ = "AGPL v3"
import datetime
from django import forms
from django.contrib.auth.models import Group
from django.contrib.auth.models import User, Group
from django.utils import timezone
from crispy_forms.helper import FormHelper
from crispy_forms.layout import Layout, Div, Field, Hidden, ButtonHolder, Submit
from crispy_forms.bootstrap import FieldWithButtons, StrictButton, InlineRadios
from crispy_bootstrap5.bootstrap5 import FloatingField
from dal import autocomplete
......@@ -162,3 +163,61 @@ class MotionForm(PostForm):
Field("voting_deadline", type="hidden"),
Submit("submit", "Submit"),
)
class MotionVoteForm(forms.Form):
user = forms.ModelChoiceField(queryset=User.objects.all())
motion = forms.ModelChoiceField(queryset=Motion.objects.all())
vote = forms.ChoiceField(
choices=(
("Y", "Agree"),
("M", "Doubt"),
("N", "Disagree"),
("A", "Abstain"),
),
)
def clean_motion(self):
print(f"in clean_motion: {self.cleaned_data = }")
print(f"{self.cleaned_data['motion'] = }")
if datetime.date.today() > self.cleaned_data["motion"].voting_deadline:
self.add_error("motion", "Motion is not open for voting anymore")
return None
return self.cleaned_data["motion"]
def clean_vote(self):
if self.cleaned_data["vote"] not in ["Y", "M", "N", "A"]:
self.add_error("vote", "Invalid vote")
return self.cleaned_data["vote"]
def clean(self):
self.cleaned_data = super().clean()
print(f"in clean: {self.cleaned_data = }")
if (
hasattr(self.cleaned_data, "user") and
hasattr(self.cleaned_data, "motion") and
(
self.cleaned_data["user"] not in
self.cleaned_data["motion"].eligible_for_voting.all()
)
):
self.add_error("", "Not eligible to vote on this Motion")
return self.cleaned_data
def save(self):
user = self.cleaned_data["user"]
motion = self.cleaned_data["motion"]
vote = self.cleaned_data["vote"]
motion.in_agreement.remove(user)
motion.in_doubt.remove(user)
motion.in_disagreement.remove(user)
motion.in_abstain.remove(user)
if vote == "Y":
motion.in_agreement.add(user)
elif vote == "M":
motion.in_doubt.add(user)
elif vote == "N":
motion.in_disagreement.add(user)
elif vote == "A":
motion.in_abstain.add(user)
motion.save()
{% load bootstrap %}
{% load crispy_forms_tags %}
<div class="align-self-center px-2 py-1">
Voting results&emsp;
<span class="text-white-50">
{% if request.user in motion.in_agreement.all %}
You have voted: <strong class="text-success">Agree</strong>
{% elif request.user in motion.in_doubt.all %}
You have voted: <strong class="text-warning">Doubt</strong>
{% elif request.user in motion.in_disagreement.all %}
You have voted: <strong class="text-danger">Disagree</strong>
{% elif request.user in motion.in_abstain.all %}
You have <strong class="text-white">Abstained</strong>
{% elif request.user in motion.eligible_for_voting.all %}
[click to vote]
{% endif %}
</span>
</div>
{% if form.errors %}
{% for field in form %}
{% for error in field.errors %}
<div class="alert alert-danger">
<strong>{{ field.name }} - {{ error|escape }}</strong>
</div>
{% endfor %}
{% endfor %}
{% for error in form.non_field_errors %}
<div class="alert alert-danger">
<strong>{{ error|escape }}</strong>
</div>
{% endfor %}
{% endif %}
<form hx-post="{% url 'forums:_hx_motion_voting' slug=forum.slug motion_id=motion.id %}"
hx-target="#motion-{{ motion.id }}-voting"
hx-trigger="change"
>
{% csrf_token %}
<input type="hidden" name="user" value="{{ request.user.id }}" id="id_user">
<input type="hidden" name="motion" value="{{ motion.id }}" id="id_motion">
<div class="btn-group" role="group" aria-label="motion voting buttons">
<input type="radio" class="btn-check" name="vote"
id="vote-Y" value="Y"
{% if request.user in motion.in_agreement.all %}checked
{% elif request.user not in motion.eligible_for_voting.all %}disabled
{% endif %}
>
{% if request.user in motion.in_agreement.all %}
<label class="btn btn-success text-white" for="vote-Y">
<strong>Agree&nbsp;{% include "bi/check-square-fill.html" %}</strong>
</label>
{% else %}
<label class="btn btn-success" for="vote-Y">Agree</label>
{% endif %}
<input type="radio" class="btn-check" name="vote"
id="vote-M" value="M"
{% if request.user in motion.in_doubt.all %}checked
{% elif request.user not in motion.eligible_for_voting.all %}disabled
{% endif %}
>
{% if request.user in motion.in_doubt.all %}
<label class="btn btn-warning text-white" for="vote-M">
<strong>Doubt&nbsp;{% include "bi/check-square-fill.html" %}</strong>
</label>
{% else %}
<label class="btn btn-warning" for="vote-M">Doubt</label>
{% endif %}
<input type="radio" class="btn-check" name="vote"
id="vote-N" value="N"
{% if request.user in motion.in_disagreement.all %}checked
{% elif request.user not in motion.eligible_for_voting.all %}disabled
{% endif %}
>
{% if request.user in motion.in_disagreement.all %}
<label class="btn btn-danger text-white" for="vote-N">
<strong>Disagree&nbsp;{% include "bi/check-square-fill.html" %}</strong>
</label>
{% else %}
<label class="btn btn-danger text-dark" for="vote-N">Disagree </label>
{% endif %}
<input type="radio" class="btn-check" name="vote"
id="vote-A" value="A"
{% if request.user in motion.in_abstain.all %}checked
{% elif request.user not in motion.eligible_for_voting.all %}disabled
{% endif %}
>
<label class="btn btn-secondary" for="vote-A">
Abstain{% if request.user in motion.in_abstain.all %} {% include "bi/check-square-fill.html" %}{% endif %}
</label>
</div>
</form>
<div class="align-self-center px-2 py-1">
Voting deadline:&emsp;{{ motion.voting_deadline|date:'Y-m-d' }}
</div>
......@@ -29,44 +29,22 @@
</div>
{% if post.motion %}
<div class="align-self-center px-2">
Voting results
<span class="text-white-50">
{% if request.user in post.motion.in_agreement.all %}
<br/>You have voted <span class="text-success">Agree</span>
{% elif request.user in post.motion.in_doubt.all %}
<br/>You have voted <span class="text-warning">Doubt</span>
{% elif request.user in post.motion.in_disagreement.all %}
<br/>You have voted <span class="text-danger">Disagree</span>
{% elif request.user in post.motion.in_abstain.all %}
<br/>You have <span class="text-white-50">Abstained</span>
{% elif request.user in post.motion.eligible_for_voting.all %}
<br/>[click to vote]
{% endif %}
</span>
</div>
<div>
<form action="{% url 'forums:motion_vote' slug=forum.slug motion_id=post.motion.id vote='Y' %}" method="post">{% csrf_token %}
<input type="submit" class="btn btn-success" data-bs-toggle="tooltip" data-bs-placement="top" data-bs-html="true" title="Agree{% if "can_administer_forum" in forum_user_perms %}<ul>{% for f in post.motion.in_agreement.all %}<li>{{ f.last_name }}, {{ f.first_name }}</li>{% endfor %}</ul>{% endif %}" value="{{ post.motion.in_agreement.all|length }}">
</form>
</div>
<div>
<form action="{% url 'forums:motion_vote' slug=forum.slug motion_id=post.motion.id vote='M' %}" method="post">{% csrf_token %}
<input type="submit" class="btn btn-warning" data-bs-toggle="tooltip" data-bs-placement="top" data-bs-html="true" title="Doubt{% if "can_administer_forum" in forum_user_perms %}<ul>{% for f in post.motion.in_doubt.all %}<li>{{ f.last_name }}, {{ f.first_name }}</li>{% endfor %}</ul>{% endif %}" value="{{ post.motion.in_doubt.all|length }}">
</form>
</div>
<div>
<form action="{% url 'forums:motion_vote' slug=forum.slug motion_id=post.motion.id vote='N' %}" method="post">{% csrf_token %}
<input type="submit" class="btn btn-danger" data-bs-toggle="tooltip" data-bs-placement="top" data-bs-html="true" title="Disagree{% if "can_administer_forum" in forum_user_perms %}<ul>{% for f in post.motion.in_disagreement.all %}<li>{{ f.last_name }}, {{ f.first_name }}</li>{% endfor %}</ul>{% endif %}" value="{{ post.motion.in_disagreement.all|length }}">
</form>
</div>
<div>
<form action="{% url 'forums:motion_vote' slug=forum.slug motion_id=post.motion.id vote='A' %}" method="post">{% csrf_token %}
<input type="submit" class="btn btn-secondary" data-bs-toggle="tooltip" data-bs-placement="top" data-bs-html="true" title="Abstain{% if "can_administer_forum" in forum_user_perms %}<ul>{% for f in post.motion.in_abstain.all %}<li>{{ f.last_name }}, {{ f.first_name }}</li>{% endfor %}</ul>{% endif %}" value="{{ post.motion.in_abstain.all|length }}">
</form>
<div id="motion-{{ post.motion.id }}-voting-indicator"
class="htmx-indicator p-2"
>
<button class="btn btn-warning" type="button" disabled>
<strong>Loading voting data...</strong>
<div class="spinner-grow spinner-grow-sm ms-2" role="status" aria-hidden="true"></div>
</button>
</div>
<div class="align-self-center px-2">
Voting deadline:<br/>{{ post.motion.voting_deadline|date:'Y-m-d' }}
<div id="motion-{{ post.motion.id }}-voting"
class="align-self-center px-2"
hx-get="{% url 'forums:_hx_motion_voting' slug=forum.slug motion_id=post.motion.id %}"
hx-trigger="load"
hx-target="#motion-{{ post.motion.id }}-voting"
hx-swap="innerHTML"
hx-indicator="#motion-{{ post.motion.id }}-voting-indicator"
>
</div>
{% endif %}
</div>
......
......@@ -113,8 +113,18 @@ urlpatterns = [
),
path("", views.ForumListView.as_view(), name="forums"),
path(
"<slug:slug>/motion/<int:motion_id>/<str:vote>/",
views.motion_vote,
name="motion_vote",
"<slug:slug>/motion/<int:motion_id>/",
include([
path(
"<str:vote>/",
views._hx_motion_voting,
name="_hx_motion_voting",
),
path(
"",
views._hx_motion_voting,
name="_hx_motion_voting",
),
]),
),
]
......@@ -39,6 +39,7 @@ from .forms import (
MeetingForm,
PostForm,
MotionForm,
MotionVoteForm,
)
from scipost.mixins import PermissionsMixin
......@@ -401,26 +402,16 @@ def _hx_thread_from_post(request, slug, post_id):
@permission_required_or_403("forums.can_post_to_forum", (Forum, "slug", "slug"))
def motion_vote(request, slug, motion_id, vote):
def _hx_motion_voting(request, slug, motion_id, vote=None):
forum = get_object_or_404(Forum, slug=slug)
motion = get_object_or_404(Motion, pk=motion_id)
if datetime.date.today() > motion.voting_deadline:
messages.warning(request, "The voting deadline on this Motion has passed.")
elif motion.eligible_for_voting.filter(pk=request.user.id).exists():
motion.in_agreement.remove(request.user)
motion.in_doubt.remove(request.user)
motion.in_disagreement.remove(request.user)
motion.in_abstain.remove(request.user)
if vote == "Y":
motion.in_agreement.add(request.user)
elif vote == "M":
motion.in_doubt.add(request.user)
elif vote == "N":
motion.in_disagreement.add(request.user)
elif vote == "A":
motion.in_abstain.add(request.user)
else:
raise Http404
motion.save()
else:
messages.warning(request, "You do not have voting rights on this Motion.")
return redirect(motion.get_absolute_url())
initial = {
"user": request.user.id,
"motion": motion.id,
}
form = MotionVoteForm(request.POST or None, initial=initial)
if form.is_valid():
form.save()
motion.refresh_from_db()
context = { "forum": forum, "motion": motion, "form": form }
return render(request, "forums/_hx_motion_voting.html", context)
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment