kadenios/elections/views.py

572 lines
19 KiB
Python
Raw Normal View History

import csv
from django.contrib import messages
2021-05-29 12:05:47 +02:00
from django.contrib.auth import get_user_model
2020-11-20 17:46:53 +01:00
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
2020-12-18 00:19:18 +01:00
from django.urls import reverse
2020-12-19 16:57:28 +01:00
from django.utils import timezone
from django.utils.decorators import method_decorator
2020-12-19 20:58:38 +01:00
from django.utils.text import slugify
2020-12-18 00:19:18 +01:00
from django.utils.translation import gettext_lazy as _
from django.views.decorators.http import require_POST
2020-12-19 23:48:18 +01:00
from django.views.generic import (
CreateView,
DetailView,
FormView,
2020-12-19 23:48:18 +01:00
ListView,
UpdateView,
View,
2020-12-19 23:48:18 +01:00
)
from django.views.generic.detail import SingleObjectMixin
2020-11-20 17:46:53 +01:00
from shared.views import BackgroundUpdateView
from .forms import (
DeleteVoteForm,
ElectionForm,
OptionForm,
QuestionForm,
UploadVotersForm,
VoterMailForm,
)
2021-03-20 20:21:48 +01:00
from .mixins import (
AdminOnlyMixin,
ClosedElectionMixin,
2021-03-20 20:21:48 +01:00
CreatorOnlyEditMixin,
CreatorOnlyMixin,
NotArchivedMixin,
2021-03-20 20:21:48 +01:00
OpenElectionOnlyMixin,
)
2021-05-29 12:05:47 +02:00
from .models import Election, Option, Question, Vote
from .staticdefs import MAIL_VOTE_DELETED, MAIL_VOTERS, QUESTION_TYPES, VOTE_RULES
from .utils import create_users, send_mail
2020-11-20 17:46:53 +01:00
2021-05-29 12:05:47 +02:00
User = get_user_model()
2020-11-20 17:46:53 +01:00
# TODO: access control *everywhere*
2020-12-19 23:48:18 +01:00
# #############################################################################
# Administration Views
# #############################################################################
2020-11-20 17:46:53 +01:00
2021-03-20 20:21:48 +01:00
class ElectionCreateView(AdminOnlyMixin, SuccessMessageMixin, CreateView):
2020-11-20 17:46:53 +01:00
model = Election
form_class = ElectionForm
2020-12-19 22:22:23 +01:00
success_message = _("Élection créée avec succès !")
2020-12-20 10:49:39 +01:00
template_name = "elections/election_create.html"
2020-11-20 17:46:53 +01:00
2020-12-19 20:58:38 +01:00
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(
2020-12-19 22:22:23 +01:00
form.instance.start_date.strftime("%Y-%m-%d") + "_" + form.instance.name
2020-12-19 20:58:38 +01:00
)[:50]
# TODO: Change this if we modify the user model
form.instance.created_by = self.request.user
return super().form_valid(form)
2020-11-20 17:46:53 +01:00
2020-12-19 23:48:18 +01:00
class ElectionAdminView(CreatorOnlyMixin, DetailView):
2020-12-19 15:04:04 +01:00
model = Election
template_name = "elections/election_admin.html"
def get_next_url(self):
return reverse("election.view", args=[self.object.pk])
2020-12-19 16:57:28 +01:00
def get_context_data(self, **kwargs):
kwargs.update(
{
"current_time": timezone.now(),
"question_types": QUESTION_TYPES,
"o_form": OptionForm,
"q_form": QuestionForm,
}
)
2020-12-19 16:57:28 +01:00
return super().get_context_data(**kwargs)
2020-12-19 15:04:04 +01:00
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"
2020-12-24 01:10:05 +01:00
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)
2020-12-23 17:09:04 +01:00
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"
2020-12-24 01:10:05 +01:00
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):
2020-11-20 17:46:53 +01:00
model = Election
form_class = ElectionForm
2020-11-20 17:46:53 +01:00
success_message = _("Élection modifiée avec succès !")
2020-12-19 16:57:28 +01:00
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
2020-12-19 16:57:28 +01:00
def get_success_url(self):
return reverse("election.admin", args=[self.object.pk])
2020-11-20 17:46:53 +01:00
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)
2020-11-20 17:46:53 +01:00
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):
2020-12-19 16:57:28 +01:00
model = Election
pattern_name = "election.admin"
success_message = _("Élection dépouillée avec succès !")
def get_queryset(self):
2020-12-19 22:22:23 +01:00
return (
super()
.get_queryset()
.filter(tallied=False)
2020-12-19 22:22:23 +01:00
.prefetch_related("questions__options")
)
2020-12-19 16:57:28 +01:00
def get(self, request, *args, **kwargs):
election = self.get_object()
for q in election.questions.all():
2021-03-19 16:08:02 +01:00
q.tally()
election.tallied = True
election.save()
2020-12-19 16:57:28 +01:00
return super().get(request, *args, **kwargs)
class ElectionChangePublicationView(ClosedElectionMixin, BackgroundUpdateView):
2020-12-19 16:57:28 +01:00
model = Election
pattern_name = "election.admin"
2020-12-19 16:57:28 +01:00
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 !")
2020-12-19 16:57:28 +01:00
def get(self, request, *args, **kwargs):
self.election = self.get_object()
self.election.results_public = not self.election.results_public
self.election.save()
2020-12-19 16:57:28 +01:00
return super().get(request, *args, **kwargs)
class DownloadResultsView(CreatorOnlyMixin, SingleObjectMixin, View):
model = Election
def get_queryset(self):
return super().get_queryset().filter(tallied=True)
def get(self, request, *args, **kwargs):
content = "\n".join([q.results for q in self.get_object().questions.all()])
response = HttpResponse(content, content_type="text/plain")
response["Content-Disposition"] = "attachment; filename=results.txt"
return response
class ElectionArchiveView(ClosedElectionMixin, BackgroundUpdateView):
2020-12-19 16:57:28 +01:00
model = Election
pattern_name = "election.admin"
success_message = _("Élection archivée avec succès !")
2020-12-19 16:57:28 +01:00
def get(self, request, *args, **kwargs):
election = self.get_object()
election.archived = True
election.save()
2020-12-19 16:57:28 +01:00
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):
2020-11-20 17:46:53 +01:00
model = Election
2020-12-18 00:19:18 +01:00
template_name = "elections/election.html"
2020-11-20 17:46:53 +01:00
2020-12-21 00:07:07 +01:00
def get_next_url(self):
return self.request.path
2020-11-20 17:46:53 +01:00
def get_context_data(self, **kwargs):
2020-12-21 00:07:07 +01:00
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()
2020-12-21 00:07:07 +01:00
return context
2020-12-18 00:19:18 +01:00
def get_queryset(self):
return (
super()
.get_queryset()
.select_related("created_by")
.prefetch_related("questions__options", "questions__duels")
)
2020-12-18 00:19:18 +01:00
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:
context["d_form"] = DeleteVoteForm()
context["can_delete"] = can_delete
context["voters"] = voters
return context
2021-04-17 00:23:33 +02:00
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")
)
2020-12-20 18:50:38 +01:00
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):
2020-12-21 00:07:07 +01:00
return reverse("election.view", args=[self.object.election.pk])
2020-12-20 10:49:39 +01:00
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
2021-03-19 14:25:13 +01:00
# On enregistre le vote pour l'élection
self.object.election.voters.add(self.request.user)
2020-12-20 10:49:39 +01:00
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
2020-12-20 18:50:38 +01:00
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):
2020-12-20 18:50:38 +01:00
raise Http404
return question
2020-12-18 00:19:18 +01:00
def get(self, request, *args, **kwargs):
self.object = self.get_object()
vote_form = self.object.get_formset()(instance=self.object)
2020-12-18 00:19:18 +01:00
2020-12-18 17:38:44 +01:00
return self.render_to_response(self.get_context_data(formset=vote_form))
2020-12-18 00:19:18 +01:00
def post(self, request, *args, **kwargs):
self.object = self.get_object()
vote_form = self.object.get_formset()(self.request.POST, instance=self.object)
2020-12-18 00:19:18 +01:00
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))