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}
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):
def __init__(self, **kwargs):
super().__init__(**kwargs)

View file

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

View file

@ -13,6 +13,15 @@ MAIL_VOTERS = (
"Kadenios"
)
MAIL_VOTE_DELETED = (
"Dear {full_name},\n"
"\n"
"Your vote for {election_name} has been removed."
"\n"
"-- \n"
"Kadenios"
)
CONNECTION_METHODS = {
"pwd": _("mot de passe"),
"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>
<tr>
<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>
</thead>
<tbody>
@ -53,8 +56,8 @@
{% if election.restricted %}
{% for v in election.registered_voters.all %}
<tr>
<td>{{ v.full_name }}</td>
<td>
<td>{{ v.full_name }} ({{ v.get_username }})</td>
<td class="has-text-centered">
<span class="icon">
{% if v in voters %}
<i class="fas fa-check"></i>
@ -67,13 +70,21 @@
{% endfor %}
{% else %}
{% for v in voters %}
<tr>
<td>{{ v.full_name }}</td>
<td>
<tr id="v_{{ forloop.counter }}">
<td>{{ v.full_name }} ({{ v.get_username }})</td>
<td class="has-text-centered">
<span class="icon">
<i class="fas fa-check"></i>
</span>
</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>
{% endfor %}
{% endif %}

View file

@ -16,6 +16,11 @@ urlpatterns = [
views.ElectionUploadVotersView.as_view(),
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("tally/<int:pk>", views.ElectionTallyView.as_view(), name="election.tally"),
path(

View file

@ -1,5 +1,7 @@
from django.contrib import messages
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.urls import reverse
from django.utils import timezone
@ -17,6 +19,7 @@ from django.views.generic import (
)
from .forms import (
DeleteVoteForm,
ElectionForm,
OptionForm,
QuestionForm,
@ -31,8 +34,8 @@ from .mixins import (
NotArchivedMixin,
OpenElectionOnlyMixin,
)
from .models import Election, Option, Question
from .staticdefs import MAIL_VOTERS, QUESTION_TYPES, VOTE_RULES
from .models import Election, Option, Question, User, Vote
from .staticdefs import MAIL_VOTE_DELETED, MAIL_VOTERS, QUESTION_TYPES, VOTE_RULES
from .utils import create_users, send_mail
# TODO: access control *everywhere*
@ -172,6 +175,66 @@ class ElectionUpdateView(CreatorOnlyEditMixin, SuccessMessageMixin, UpdateView):
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):
model = Election
pattern_name = "election.admin"
@ -280,9 +343,6 @@ class AddOptionView(CreatorOnlyEditMixin, CreateView):
model = Question
form_class = OptionForm
def get_queryset(self):
return super().get_queryset()
def get_success_url(self):
return (
reverse("election.admin", args=[self.question.election.pk])
@ -366,6 +426,17 @@ class ElectionVotersView(NotArchivedMixin, DetailView):
model = Election
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):
model = Question