On rajoute la possibilité de supprimer des votes lors d'une élection ouverte à tout le monde
This commit is contained in:
parent
07573ce866
commit
f5960c9e01
7 changed files with 159 additions and 11 deletions
|
@ -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)
|
||||
|
|
|
@ -209,3 +209,4 @@ class User(AbstractUser):
|
|||
permissions = [
|
||||
("is_admin", _("Peut administrer des élections")),
|
||||
]
|
||||
ordering = ["username"]
|
||||
|
|
|
@ -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"),
|
||||
|
|
40
elections/templates/elections/delete_vote.html
Normal file
40
elections/templates/elections/delete_vote.html
Normal 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 %}
|
|
@ -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 %}
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in a new issue