On rajoute la possibilité de supprimer des votes lors d'une élection ouverte à tout le monde

This commit is contained in:
Tom Hubrecht 2021-03-31 13:16:10 +02:00
parent 07573ce866
commit f5960c9e01
7 changed files with 159 additions and 11 deletions

View file

@ -61,6 +61,17 @@ class OptionForm(forms.ModelForm):
widgets = {"text": forms.TextInput} widgets = {"text": forms.TextInput}
class DeleteVoteForm(forms.Form):
def __init__(self, **kwargs):
voter = kwargs.pop("voter")
super().__init__(**kwargs)
self.fields["delete"].label = _("Supprimer le vote de {} ({}) ?").format(
voter.full_name, voter.get_username()
)
delete = forms.ChoiceField(choices=(("non", _("Non")), ("oui", _("Oui"))))
class SelectVoteForm(forms.ModelForm): class SelectVoteForm(forms.ModelForm):
def __init__(self, **kwargs): def __init__(self, **kwargs):
super().__init__(**kwargs) super().__init__(**kwargs)

View file

@ -209,3 +209,4 @@ class User(AbstractUser):
permissions = [ permissions = [
("is_admin", _("Peut administrer des élections")), ("is_admin", _("Peut administrer des élections")),
] ]
ordering = ["username"]

View file

@ -13,6 +13,15 @@ MAIL_VOTERS = (
"Kadenios" "Kadenios"
) )
MAIL_VOTE_DELETED = (
"Dear {full_name},\n"
"\n"
"Your vote for {election_name} has been removed."
"\n"
"-- \n"
"Kadenios"
)
CONNECTION_METHODS = { CONNECTION_METHODS = {
"pwd": _("mot de passe"), "pwd": _("mot de passe"),
"cas": _("CAS"), "cas": _("CAS"),

View file

@ -0,0 +1,40 @@
{% extends "base.html" %}
{% load i18n %}
{% block content %}
<h1 class="title">{% trans "Supprimer un vote" %}</h1>
<hr>
<div class="columns is-centered">
<div class="column is-half">
<form method="post">
{% csrf_token %}
{% include "forms/form.html" %}
<div class="field is-grouped is-centered">
<div class="control is-expanded">
<button class="button is-fullwidth is-outlined is-primary is-light">
<span class="icon is-small">
<i class="fas fa-check"></i>
</span>
<span>{% trans "Enregistrer" %}</span>
</button>
</div>
<div class="control">
<a class="button is-primary" href="{% url 'election.voters' election.pk %}#v_{{ anchor }}">
<span class="icon is-small">
<i class="fas fa-undo-alt"></i>
</span>
<span>{% trans "Retour" %}</span>
</a>
</div>
</div>
</form>
</div>
</div>
{% endblock %}

View file

@ -45,7 +45,10 @@
<thead> <thead>
<tr> <tr>
<th>{% trans "Nom" %}</th> <th>{% trans "Nom" %}</th>
<th>{% trans "Vote enregistré" %}</th> <th class="has-text-centered">{% trans "Vote enregistré" %}</th>
{% if can_delete %}
<th class="has-text-centered">{% trans "Supprimer" %}</th>
{% endif %}
<tr> <tr>
</thead> </thead>
<tbody> <tbody>
@ -53,8 +56,8 @@
{% if election.restricted %} {% if election.restricted %}
{% for v in election.registered_voters.all %} {% for v in election.registered_voters.all %}
<tr> <tr>
<td>{{ v.full_name }}</td> <td>{{ v.full_name }} ({{ v.get_username }})</td>
<td> <td class="has-text-centered">
<span class="icon"> <span class="icon">
{% if v in voters %} {% if v in voters %}
<i class="fas fa-check"></i> <i class="fas fa-check"></i>
@ -67,13 +70,21 @@
{% endfor %} {% endfor %}
{% else %} {% else %}
{% for v in voters %} {% for v in voters %}
<tr> <tr id="v_{{ forloop.counter }}">
<td>{{ v.full_name }}</td> <td>{{ v.full_name }} ({{ v.get_username }})</td>
<td> <td class="has-text-centered">
<span class="icon"> <span class="icon">
<i class="fas fa-check"></i> <i class="fas fa-check"></i>
</span> </span>
</td> </td>
{% if can_delete %}
<td class="has-text-centered">
<a class="tag is-danger has-tooltip-primary" href="{% url 'election.delete-vote' election.pk v.pk forloop.counter %}" data-tooltip="{% trans "Supprimer le vote de " %}{{ v.full_name }}">
<span class="icon">
<i class="fas fa-user-minus"></i>
</span>
</a>
{% endif %}
</tr> </tr>
{% endfor %} {% endfor %}
{% endif %} {% endif %}

View file

@ -16,6 +16,11 @@ urlpatterns = [
views.ElectionUploadVotersView.as_view(), views.ElectionUploadVotersView.as_view(),
name="election.upload-voters", name="election.upload-voters",
), ),
path(
"delete-vote/<int:pk>/<int:user_pk>/<int:anchor>",
views.DeleteVoteView.as_view(),
name="election.delete-vote",
),
path("update/<int:pk>", views.ElectionUpdateView.as_view(), name="election.update"), path("update/<int:pk>", views.ElectionUpdateView.as_view(), name="election.update"),
path("tally/<int:pk>", views.ElectionTallyView.as_view(), name="election.tally"), path("tally/<int:pk>", views.ElectionTallyView.as_view(), name="election.tally"),
path( path(

View file

@ -1,5 +1,7 @@
from django.contrib import messages from django.contrib import messages
from django.contrib.messages.views import SuccessMessageMixin from django.contrib.messages.views import SuccessMessageMixin
from django.core.mail import EmailMessage
from django.db import transaction
from django.http import Http404, HttpResponseRedirect from django.http import Http404, HttpResponseRedirect
from django.urls import reverse from django.urls import reverse
from django.utils import timezone from django.utils import timezone
@ -17,6 +19,7 @@ from django.views.generic import (
) )
from .forms import ( from .forms import (
DeleteVoteForm,
ElectionForm, ElectionForm,
OptionForm, OptionForm,
QuestionForm, QuestionForm,
@ -31,8 +34,8 @@ from .mixins import (
NotArchivedMixin, NotArchivedMixin,
OpenElectionOnlyMixin, OpenElectionOnlyMixin,
) )
from .models import Election, Option, Question from .models import Election, Option, Question, User, Vote
from .staticdefs import MAIL_VOTERS, QUESTION_TYPES, VOTE_RULES from .staticdefs import MAIL_VOTE_DELETED, MAIL_VOTERS, QUESTION_TYPES, VOTE_RULES
from .utils import create_users, send_mail from .utils import create_users, send_mail
# TODO: access control *everywhere* # TODO: access control *everywhere*
@ -172,6 +175,66 @@ class ElectionUpdateView(CreatorOnlyEditMixin, SuccessMessageMixin, UpdateView):
return reverse("election.admin", args=[self.object.pk]) return reverse("election.admin", args=[self.object.pk])
class DeleteVoteView(ClosedElectionMixin, FormView):
model = Election
template_name = "elections/delete_vote.html"
form_class = DeleteVoteForm
def get_success_url(self):
return reverse("election.voters", args=[self.object.pk]) + "#v_{anchor}".format(
**self.kwargs
)
def get_form_kwargs(self):
kwargs = super().get_form_kwargs()
kwargs["voter"] = self.voter
return kwargs
def get_queryset(self):
# On n'affiche la page que pour les élections ouvertes à toustes
return super().get_queryset().filter(restricted=False)
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context["anchor"] = self.kwargs["anchor"]
return context
def get(self, request, *args, **kwargs):
self.object = super().get_object()
self.voter = User.objects.get(pk=self.kwargs["user_pk"])
return super().get(request, *args, **kwargs)
def post(self, request, *args, **kwargs):
self.object = super().get_object()
self.voter = User.objects.get(pk=self.kwargs["user_pk"])
return super().post(request, *args, **kwargs)
@transaction.atomic
def form_valid(self, form):
if form.cleaned_data["delete"] == "oui":
# On envoie un mail à la personne lui indiquant que le vote est supprimé
EmailMessage(
subject="Vote removed",
body=MAIL_VOTE_DELETED.format(
full_name=self.voter.full_name,
election_name=self.object.name,
),
to=[self.voter.email],
).send()
# On supprime les votes
Vote.objects.filter(
user=self.voter,
option__question__election=self.object,
).delete()
# On marque les questions comme non votées
self.voter.cast_elections.remove(self.object)
self.voter.cast_questions.remove(*list(self.object.questions.all()))
return super().form_valid(form)
class ElectionTallyView(ClosedElectionMixin, BackgroundUpdateView): class ElectionTallyView(ClosedElectionMixin, BackgroundUpdateView):
model = Election model = Election
pattern_name = "election.admin" pattern_name = "election.admin"
@ -280,9 +343,6 @@ class AddOptionView(CreatorOnlyEditMixin, CreateView):
model = Question model = Question
form_class = OptionForm form_class = OptionForm
def get_queryset(self):
return super().get_queryset()
def get_success_url(self): def get_success_url(self):
return ( return (
reverse("election.admin", args=[self.question.election.pk]) reverse("election.admin", args=[self.question.election.pk])
@ -366,6 +426,17 @@ class ElectionVotersView(NotArchivedMixin, DetailView):
model = Election model = Election
template_name = "elections/election_voters.html" template_name = "elections/election_voters.html"
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
election = context["election"]
context["can_delete"] = (
not election.restricted
and election.created_by == self.request.user
and election.end_date < timezone.now()
and not election.tallied
)
return context
class VoteView(OpenElectionOnlyMixin, DetailView): class VoteView(OpenElectionOnlyMixin, DetailView):
model = Question model = Question