430 lines
14 KiB
Python
430 lines
14 KiB
Python
from django.contrib import messages
|
|
from django.contrib.messages.views import SuccessMessageMixin
|
|
|
|
# from django.db.models import Count, Prefetch
|
|
from django.http import Http404, HttpResponseRedirect
|
|
from django.urls import reverse
|
|
from django.utils import timezone
|
|
from django.utils.text import slugify
|
|
from django.utils.translation import gettext_lazy as _
|
|
from django.views.generic import (
|
|
CreateView,
|
|
DetailView,
|
|
FormView,
|
|
ListView,
|
|
RedirectView,
|
|
UpdateView,
|
|
)
|
|
|
|
from .forms import (
|
|
ElectionForm,
|
|
OptionForm,
|
|
OptionFormSet,
|
|
QuestionForm,
|
|
UploadVotersForm,
|
|
VoterMailForm,
|
|
)
|
|
from .mixins import CreatorOnlyEditMixin, CreatorOnlyMixin, OpenElectionOnlyMixin
|
|
from .models import Election, Option, Question
|
|
from .staticdefs import 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(self, request, *args, **kwargs)
|
|
|
|
|
|
# #############################################################################
|
|
# Administration Views
|
|
# #############################################################################
|
|
|
|
|
|
class ElectionCreateView(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}
|
|
)
|
|
return super().get_context_data(**kwargs)
|
|
|
|
def get_queryset(self):
|
|
return super().get_queryset().prefetch_related("questions__options")
|
|
|
|
|
|
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 ElectionListView(CreatorOnlyMixin, ListView):
|
|
model = Election
|
|
template_name = "elections/election_list.html"
|
|
|
|
|
|
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_success_url(self):
|
|
return reverse("election.admin", args=[self.object.pk])
|
|
|
|
|
|
class ElectionTallyView(CreatorOnlyEditMixin, 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(end_date__lt=timezone.now())
|
|
.prefetch_related("questions__options")
|
|
)
|
|
|
|
def get(self, request, *args, **kwargs):
|
|
election = self.get_object()
|
|
options, questions = [], []
|
|
for q in election.questions.all():
|
|
max_votes = 0
|
|
for o in q.options.all():
|
|
o.nb_votes = o.voters.count()
|
|
max_votes = max(max_votes, o.nb_votes)
|
|
options.append(o)
|
|
|
|
q.max_votes = max_votes
|
|
questions.append(q)
|
|
|
|
Option.objects.bulk_update(options, ["nb_votes"])
|
|
Question.objects.bulk_update(questions, ["max_votes"])
|
|
election.tallied = True
|
|
election.save()
|
|
return super().get(request, *args, **kwargs)
|
|
|
|
|
|
class ElectionChangePublicationView(CreatorOnlyEditMixin, 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_queryset(self):
|
|
return super().get_queryset().filter(end_date__lt=timezone.now())
|
|
|
|
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(CreatorOnlyEditMixin, BackgroundUpdateView):
|
|
model = Election
|
|
pattern_name = "election.admin"
|
|
success_message = _("Élection archivée avec succès !")
|
|
|
|
def get_queryset(self):
|
|
return super().get_queryset().filter(end_date__lt=timezone.now())
|
|
|
|
def get(self, request, *args, **kwargs):
|
|
election = self.get_object()
|
|
election.archived = True
|
|
election.save()
|
|
return super().get(request, *args, **kwargs)
|
|
|
|
|
|
# #############################################################################
|
|
# Question Views
|
|
# #############################################################################
|
|
|
|
|
|
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
|
|
# #############################################################################
|
|
|
|
|
|
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])
|
|
+ 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 ElectionView(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()
|
|
context["can_vote"] = user.is_authenticated and user.can_vote(
|
|
self.request, context["election"]
|
|
)
|
|
context["cast_questions"] = user.cast_questions.all()
|
|
return context
|
|
|
|
def get_queryset(self):
|
|
return (
|
|
super()
|
|
.get_queryset()
|
|
.filter(archived=False)
|
|
.prefetch_related("questions__options")
|
|
)
|
|
|
|
|
|
class ElectionVotersView(DetailView):
|
|
model = Election
|
|
template_name = "elections/election_voters.html"
|
|
|
|
|
|
class VoteView(OpenElectionOnlyMixin, DetailView):
|
|
model = Question
|
|
template_name = "elections/vote.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
|
|
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["vote_rule"] = VOTE_RULES[self.object.type]
|
|
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 = OptionFormSet(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 = OptionFormSet(self.request.POST, instance=self.object)
|
|
|
|
# We record the cast votes
|
|
for v in vote_form:
|
|
v.record_vote(self.request.user)
|
|
|
|
# On enregistre le vote pour la question et l'élection
|
|
self.object.voters.add(self.request.user)
|
|
self.object.election.voters.add(self.request.user)
|
|
|
|
messages.success(self.request, _("Votre choix a bien été enregistré !"))
|
|
|
|
return HttpResponseRedirect(self.get_success_url())
|