diff --git a/elections/forms.py b/elections/forms.py index 9d3c56c..faa23af 100644 --- a/elections/forms.py +++ b/elections/forms.py @@ -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) diff --git a/elections/models.py b/elections/models.py index fb05ee0..47cfe7d 100644 --- a/elections/models.py +++ b/elections/models.py @@ -209,3 +209,4 @@ class User(AbstractUser): permissions = [ ("is_admin", _("Peut administrer des élections")), ] + ordering = ["username"] diff --git a/elections/staticdefs.py b/elections/staticdefs.py index 8f8a7c5..9d7b8d3 100644 --- a/elections/staticdefs.py +++ b/elections/staticdefs.py @@ -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"), diff --git a/elections/templates/elections/delete_vote.html b/elections/templates/elections/delete_vote.html new file mode 100644 index 0000000..787b28c --- /dev/null +++ b/elections/templates/elections/delete_vote.html @@ -0,0 +1,40 @@ +{% extends "base.html" %} +{% load i18n %} + + +{% block content %} + +

{% trans "Supprimer un vote" %}

+
+ +
+
+
+ {% csrf_token %} + + {% include "forms/form.html" %} + +
+
+ +
+ + +
+
+
+
+ +{% endblock %} diff --git a/elections/templates/elections/election_voters.html b/elections/templates/elections/election_voters.html index 2e702d5..5a219ef 100644 --- a/elections/templates/elections/election_voters.html +++ b/elections/templates/elections/election_voters.html @@ -45,7 +45,10 @@ {% trans "Nom" %} - {% trans "Vote enregistré" %} + {% trans "Vote enregistré" %} + {% if can_delete %} + {% trans "Supprimer" %} + {% endif %} @@ -53,8 +56,8 @@ {% if election.restricted %} {% for v in election.registered_voters.all %} - {{ v.full_name }} - + {{ v.full_name }} ({{ v.get_username }}) + {% if v in voters %} @@ -67,13 +70,21 @@ {% endfor %} {% else %} {% for v in voters %} - - {{ v.full_name }} - + + {{ v.full_name }} ({{ v.get_username }}) + + {% if can_delete %} + + + + + + + {% endif %} {% endfor %} {% endif %} diff --git a/elections/urls.py b/elections/urls.py index 2440d38..df3ccf3 100644 --- a/elections/urls.py +++ b/elections/urls.py @@ -16,6 +16,11 @@ urlpatterns = [ views.ElectionUploadVotersView.as_view(), name="election.upload-voters", ), + path( + "delete-vote///", + views.DeleteVoteView.as_view(), + name="election.delete-vote", + ), path("update/", views.ElectionUpdateView.as_view(), name="election.update"), path("tally/", views.ElectionTallyView.as_view(), name="election.tally"), path( diff --git a/elections/views.py b/elections/views.py index 1179c49..e2b31da 100644 --- a/elections/views.py +++ b/elections/views.py @@ -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