from django.contrib import messages from django.contrib.messages.views import SuccessMessageMixin from django.http import Http404, 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, ) from .forms import ( ElectionForm, OptionForm, QuestionForm, UploadVotersForm, VoterMailForm, ) from .mixins import ( AdminOnlyMixin, ClosedElectionMixin, CreatorOnlyEditMixin, CreatorOnlyMixin, NotArchivedMixin, 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(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} ) 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 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(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_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 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().prefetch_related("questions__options") class ElectionVotersView(NotArchivedMixin, 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 # 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["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 = 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))