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}
|
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)
|
||||||
|
|
|
@ -209,3 +209,4 @@ class User(AbstractUser):
|
||||||
permissions = [
|
permissions = [
|
||||||
("is_admin", _("Peut administrer des élections")),
|
("is_admin", _("Peut administrer des élections")),
|
||||||
]
|
]
|
||||||
|
ordering = ["username"]
|
||||||
|
|
|
@ -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"),
|
||||||
|
|
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>
|
<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 %}
|
||||||
|
|
|
@ -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(
|
||||||
|
|
|
@ -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
|
||||||
|
|
Loading…
Reference in a new issue