kadenios/elections/views.py

572 lines
19 KiB
Python

import csv
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, HttpResponse, HttpResponseRedirect
from django.urls import reverse
from django.utils import timezone
from django.utils.decorators import method_decorator
from django.utils.text import slugify
from django.utils.translation import gettext_lazy as _
from django.views.decorators.http import require_POST
from django.views.generic import (
CreateView,
DetailView,
FormView,
ListView,
RedirectView,
UpdateView,
View,
)
from django.views.generic.detail import SingleObjectMixin
from .forms import (
DeleteVoteForm,
ElectionForm,
OptionForm,
QuestionForm,
UploadVotersForm,
VoterMailForm,
)
from .mixins import (
AdminOnlyMixin,
ClosedElectionMixin,
CreatorOnlyEditMixin,
CreatorOnlyMixin,
NotArchivedMixin,
OpenElectionOnlyMixin,
)
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*
# #############################################################################
# Utils Views
# #############################################################################
class BackgroundUpdateView(RedirectView):
success_message = ""
def get_success_message(self):
return self.success_message
def get(self, request, *args, **kwargs):
success_message = self.get_success_message()
if success_message:
messages.success(self.request, success_message)
return super().get(request, *args, **kwargs)
# #############################################################################
# Administration Views
# #############################################################################
class ElectionCreateView(AdminOnlyMixin, SuccessMessageMixin, CreateView):
model = Election
form_class = ElectionForm
success_message = _("Élection créée avec succès !")
template_name = "elections/election_create.html"
def get_success_url(self):
return reverse("election.admin", args=[self.object.pk])
def form_valid(self, form):
# We need to add the short name and the creator od the election
form.instance.short_name = slugify(
form.instance.start_date.strftime("%Y-%m-%d") + "_" + form.instance.name
)[:50]
# TODO: Change this if we modify the user model
form.instance.created_by = self.request.user
return super().form_valid(form)
class ElectionAdminView(CreatorOnlyMixin, DetailView):
model = Election
template_name = "elections/election_admin.html"
def get_next_url(self):
return reverse("election.view", args=[self.object.pk])
def get_context_data(self, **kwargs):
kwargs.update(
{
"current_time": timezone.now(),
"question_types": QUESTION_TYPES,
"o_form": OptionForm,
"q_form": QuestionForm,
}
)
return super().get_context_data(**kwargs)
def get_queryset(self):
return super().get_queryset().prefetch_related("questions__options")
class ExportVotersView(CreatorOnlyMixin, SingleObjectMixin, View):
model = Election
def get(self, request, *args, **kwargs):
response = HttpResponse(content_type="text/csv")
writer = csv.writer(response)
response["Content-Disposition"] = "attachment; filename=voters.csv"
writer.writerow(["Nom", "login"])
for v in self.get_object().voters.all():
writer.writerow([v.full_name, v.username])
return response
class ElectionUploadVotersView(CreatorOnlyEditMixin, SuccessMessageMixin, FormView):
model = Election
form_class = UploadVotersForm
success_message = _("Liste de votant·e·s importée avec succès !")
template_name = "elections/upload_voters.html"
def get_queryset(self):
# On ne peut ajouter une liste d'électeurs que sur une élection restreinte
return super().get_queryset().filter(restricted=True)
def get_success_url(self):
return reverse("election.upload-voters", args=[self.object.pk])
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context["voters"] = self.object.registered_voters.all()
return context
def get(self, request, *args, **kwargs):
self.object = self.get_object()
return super().get(request, *args, **kwargs)
def post(self, request, *args, **kwargs):
self.object = self.get_object()
return super().post(request, *args, **kwargs)
def form_valid(self, form):
# On crée les comptes nécessaires à l'élection, en supprimant ceux
# existant déjà pour ne pas avoir de doublons
self.object.registered_voters.all().delete()
create_users(self.object, form.cleaned_data["csv_file"])
return super().form_valid(form)
class ElectionMailVotersView(CreatorOnlyEditMixin, SuccessMessageMixin, FormView):
model = Election
form_class = VoterMailForm
success_message = _("Mail d'annonce envoyé avec succès !")
template_name = "elections/mail_voters.html"
def get_queryset(self):
# On ne peut envoyer un mail que sur une élection restreinte qui n'a pas
# déjà vu son mail envoyé
return super().get_queryset().filter(restricted=True, sent_mail=False)
def get_success_url(self):
return reverse("election.upload-voters", args=[self.object.pk])
def get_initial(self):
return {"objet": f"Vote : {self.object.name}", "message": MAIL_VOTERS}
def get(self, request, *args, **kwargs):
self.object = self.get_object()
return super().get(request, *args, **kwargs)
def post(self, request, *args, **kwargs):
self.object = self.get_object()
return super().post(request, *args, **kwargs)
def form_valid(self, form):
self.object.sent_mail = True
send_mail(self.object, form)
self.object.save()
return super().form_valid(form)
class ElectionUpdateView(CreatorOnlyEditMixin, SuccessMessageMixin, UpdateView):
model = Election
form_class = ElectionForm
success_message = _("Élection modifiée avec succès !")
template_name = "elections/election_update.html"
def get_form(self, form_class=None):
form = super().get_form(form_class)
if self.object.sent_mail:
form.fields["restricted"].disabled = True
return form
def get_success_url(self):
return reverse("election.admin", args=[self.object.pk])
def form_valid(self, form):
# Si on ouvre l'élection à tout le monde, on supprime les votant·e·s
# pré-enregistré·e·s
if not form.cleaned_data["restricted"]:
self.object.registered_voters.all().delete()
return super().form_valid(form)
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"
success_message = _("Élection dépouillée avec succès !")
def get_queryset(self):
return (
super()
.get_queryset()
.filter(tallied=False)
.prefetch_related("questions__options")
)
def get(self, request, *args, **kwargs):
election = self.get_object()
for q in election.questions.all():
q.tally()
election.tallied = True
election.save()
return super().get(request, *args, **kwargs)
class ElectionChangePublicationView(ClosedElectionMixin, BackgroundUpdateView):
model = Election
pattern_name = "election.admin"
def get_success_message(self):
if self.election.results_public:
return _("Élection publiée avec succès !")
return _("Élection dépubliée avec succès !")
def get(self, request, *args, **kwargs):
self.election = self.get_object()
self.election.results_public = not self.election.results_public
self.election.save()
return super().get(request, *args, **kwargs)
class ElectionArchiveView(ClosedElectionMixin, BackgroundUpdateView):
model = Election
pattern_name = "election.admin"
success_message = _("Élection archivée avec succès !")
def get(self, request, *args, **kwargs):
election = self.get_object()
election.archived = True
election.save()
return super().get(request, *args, **kwargs)
# #############################################################################
# Question Views
# #############################################################################
@method_decorator(require_POST, name="dispatch")
class AddQuestionView(CreatorOnlyEditMixin, CreateView):
model = Election
form_class = QuestionForm
def get_success_url(self):
return reverse("election.admin", args=[self.election.pk]) + "#q_add"
def form_valid(self, form):
self.election = self.get_object()
# On ajoute l'élection voulue à la question créée
form.instance.election = self.election
return super().form_valid(form)
class ModQuestionView(CreatorOnlyEditMixin, SuccessMessageMixin, UpdateView):
model = Question
form_class = QuestionForm
success_message = _("Question modifiée avec succès !")
template_name = "elections/question_update.html"
def get_success_url(self):
return (
reverse("election.admin", args=[self.object.election.pk])
+ f"#q_{self.object.pk}"
)
class DelQuestionView(CreatorOnlyEditMixin, BackgroundUpdateView):
model = Question
success_message = _("Question supprimée !")
def get_redirect_url(self, *args, **kwargs):
return reverse("election.admin", args=[self.election.pk]) + "#q_add"
def get(self, request, *args, **kwargs):
question = self.get_object()
self.election = question.election
question.delete()
return super().get(request, *args, **kwargs)
# #############################################################################
# Option Views
# #############################################################################
@method_decorator(require_POST, name="dispatch")
class AddOptionView(CreatorOnlyEditMixin, CreateView):
model = Question
form_class = OptionForm
def get_success_url(self):
return (
reverse("election.admin", args=[self.question.election.pk])
+ f"#q_{self.question.pk}"
)
def form_valid(self, form):
self.question = self.get_object()
# On ajoute l'élection voulue à la question créée
form.instance.question = self.question
return super().form_valid(form)
class ModOptionView(CreatorOnlyEditMixin, SuccessMessageMixin, UpdateView):
model = Option
form_class = OptionForm
success_message = _("Option modifiée avec succès !")
template_name = "elections/option_update.html"
def get_success_url(self):
return (
reverse("election.admin", args=[self.object.question.election.pk])
+ f"#o_{self.object.pk}"
)
class DelOptionView(CreatorOnlyEditMixin, BackgroundUpdateView):
model = Option
success_message = _("Option supprimée !")
def get_redirect_url(self, *args, **kwargs):
return reverse("election.admin", args=[self.election.pk]) + "#q_add"
def get(self, request, *args, **kwargs):
option = self.get_object()
self.election = option.question.election
option.delete()
return super().get(request, *args, **kwargs)
# #############################################################################
# Public Views
# #############################################################################
class ElectionListView(NotArchivedMixin, ListView):
model = Election
template_name = "elections/election_list.html"
class ElectionView(NotArchivedMixin, DetailView):
model = Election
template_name = "elections/election.html"
def get_next_url(self):
return self.request.path
def get_context_data(self, **kwargs):
user = self.request.user
context = super().get_context_data(**kwargs)
context["current_time"] = timezone.now()
if user.is_authenticated:
context["can_vote"] = user.can_vote(self.request, context["election"])
context["cast_questions"] = user.cast_questions.all()
context["has_voted"] = user.cast_elections.filter(
pk=context["election"].pk
).exists()
return context
def get_queryset(self):
return (
super()
.get_queryset()
.select_related("created_by")
.prefetch_related("questions__options", "questions__duels")
)
class ElectionVotersView(NotArchivedMixin, DetailView):
model = Election
template_name = "elections/election_voters.html"
def get_context_data(self, **kwargs):
user = self.request.user
context = super().get_context_data(**kwargs)
election = context["election"]
voters = list(election.voters.all())
if user.is_authenticated:
can_delete = (
not election.restricted
and election.created_by == user
and election.end_date < timezone.now()
and not election.tallied
)
if can_delete:
# On rajoute le formulaire pour supprimer le vote
for v in voters:
v.form = DeleteVoteForm()
context["can_delete"] = can_delete
context["voters"] = voters
return context
class ElectionBallotsView(NotArchivedMixin, DetailView):
model = Election
template_name = "elections/election_ballots.html"
def get_queryset(self):
return (
super()
.get_queryset()
.filter(tallied=True)
.prefetch_related("questions__options")
)
class VoteView(OpenElectionOnlyMixin, DetailView):
model = Question
def dispatch(self, request, *args, **kwargs):
# Si l'utilisateur n'est pas connecté on renvoie sur la vue de l'élection
if not request.user.is_authenticated:
return HttpResponseRedirect(
reverse("election.view", args=[super().get_object().election.pk])
)
return super().dispatch(request, *args, **kwargs)
def get_template_names(self):
return [f"elections/vote/{self.object.vote_type}.html"]
def get_next_url(self):
return reverse("election.view", args=[self.object.election.pk])
def get_success_url(self):
questions = list(self.object.election.questions.all())
q_index = questions.index(self.object)
if q_index + 1 == len(questions):
# On était à la dernière question
# On enregistre le vote pour l'élection
self.object.election.voters.add(self.request.user)
return reverse("election.view", args=[self.object.election.pk])
# On récupère l'id de la prochaine question
q_next = questions[q_index + 1].pk
return reverse("election.vote", args=[q_next])
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
questions = list(self.object.election.questions.all())
context["q_index"] = questions.index(self.object) + 1
context["q_total"] = len(questions)
context["nb_options"] = self.object.options.count()
context["range_options"] = range(1, context["nb_options"] + 1)
context["vote_rule"] = VOTE_RULES[self.object.type].format(**context)
return context
def get_object(self):
question = super().get_object()
# Seulement les utilisateur·ice·s ayant le droit de voter dans l'élection
# peuvent voir la page
if not self.request.user.can_vote(self.request, question.election):
raise Http404
return question
def get(self, request, *args, **kwargs):
self.object = self.get_object()
vote_form = self.object.get_formset()(instance=self.object)
return self.render_to_response(self.get_context_data(formset=vote_form))
def post(self, request, *args, **kwargs):
self.object = self.get_object()
vote_form = self.object.get_formset()(self.request.POST, instance=self.object)
if self.object.is_form_valid(vote_form):
# On enregistre le vote
self.object.cast_ballot(self.request.user, vote_form)
self.object.voters.add(self.request.user)
messages.success(self.request, _("Votre choix a bien été enregistré !"))
return HttpResponseRedirect(self.get_success_url())
else:
return self.render_to_response(self.get_context_data(formset=vote_form))