From b076b854fe1aad1f73a90942470defce0fa75304 Mon Sep 17 00:00:00 2001 From: imperosol Date: Fri, 14 Mar 2025 18:22:17 +0100 Subject: [PATCH 1/6] move forms to their own file --- election/forms.py | 164 ++++++++++++++++++++++++++++++++++++++++++++ election/views.py | 169 ++-------------------------------------------- 2 files changed, 171 insertions(+), 162 deletions(-) create mode 100644 election/forms.py diff --git a/election/forms.py b/election/forms.py new file mode 100644 index 00000000..5032e859 --- /dev/null +++ b/election/forms.py @@ -0,0 +1,164 @@ +from django import forms +from django.db import transaction +from django.db.models import QuerySet +from django.shortcuts import get_object_or_404 +from django.urls import reverse_lazy +from django.utils.translation import gettext_lazy as _ +from django.views.generic import FormView + +from core.auth.mixins import CanCreateMixin +from core.views.forms import SelectDateTime +from core.views.widgets.ajax_select import ( + AutoCompleteSelect, + AutoCompleteSelectMultipleGroup, + AutoCompleteSelectUser, +) +from core.views.widgets.markdown import MarkdownInput +from election.models import Candidature, Election, ElectionList, Role, Vote + + +class LimitedCheckboxField(forms.ModelMultipleChoiceField): + """A `ModelMultipleChoiceField`, with a max limit of selectable inputs.""" + + def __init__(self, queryset, max_choice, **kwargs): + self.max_choice = max_choice + super().__init__(queryset, **kwargs) + + def clean(self, value): + qs = super().clean(value) + self.validate(qs) + return qs + + def validate(self, qs): + if qs.count() > self.max_choice: + raise forms.ValidationError( + _("You have selected too much candidates."), code="invalid" + ) + + +class CandidateForm(forms.ModelForm): + """Form to candidate.""" + + class Meta: + model = Candidature + fields = ["user", "role", "program", "election_list"] + labels = { + "user": _("User to candidate"), + } + widgets = { + "program": MarkdownInput, + "user": AutoCompleteSelectUser, + "role": AutoCompleteSelect, + "election_list": AutoCompleteSelect, + } + + def __init__(self, *args, **kwargs): + election_id = kwargs.pop("election_id", None) + can_edit = kwargs.pop("can_edit", False) + super().__init__(*args, **kwargs) + if election_id: + self.fields["role"].queryset = Role.objects.filter( + election__id=election_id + ).all() + self.fields["election_list"].queryset = ElectionList.objects.filter( + election__id=election_id + ).all() + if not can_edit: + self.fields["user"].widget = forms.HiddenInput() + + +class VoteForm(forms.Form): + def __init__(self, election, user, *args, **kwargs): + super().__init__(*args, **kwargs) + if not election.has_voted(user): + for role in election.roles.all(): + cand = role.candidatures + if role.max_choice > 1: + self.fields[role.title] = LimitedCheckboxField( + cand, role.max_choice, required=False + ) + else: + self.fields[role.title] = forms.ModelChoiceField( + cand, + required=False, + widget=forms.RadioSelect(), + empty_label=_("Blank vote"), + ) + + +class RoleForm(forms.ModelForm): + """Form for creating a role.""" + + class Meta: + model = Role + fields = ["title", "election", "description", "max_choice"] + widgets = {"election": AutoCompleteSelect} + + def __init__(self, *args, **kwargs): + election_id = kwargs.pop("election_id", None) + super().__init__(*args, **kwargs) + if election_id: + self.fields["election"].queryset = Election.objects.filter( + id=election_id + ).all() + + def clean(self): + cleaned_data = super().clean() + title = cleaned_data.get("title") + election = cleaned_data.get("election") + if Role.objects.filter(title=title, election=election).exists(): + raise forms.ValidationError( + _("This role already exists for this election"), code="invalid" + ) + + +class ElectionListForm(forms.ModelForm): + class Meta: + model = ElectionList + fields = ("title", "election") + widgets = {"election": AutoCompleteSelect} + + def __init__(self, *args, **kwargs): + election_id = kwargs.pop("election_id", None) + super().__init__(*args, **kwargs) + if election_id: + self.fields["election"].queryset = Election.objects.filter( + id=election_id + ).all() + + +class ElectionForm(forms.ModelForm): + class Meta: + model = Election + fields = [ + "title", + "description", + "archived", + "start_candidature", + "end_candidature", + "start_date", + "end_date", + "edit_groups", + "view_groups", + "vote_groups", + "candidature_groups", + ] + widgets = { + "edit_groups": AutoCompleteSelectMultipleGroup, + "view_groups": AutoCompleteSelectMultipleGroup, + "vote_groups": AutoCompleteSelectMultipleGroup, + "candidature_groups": AutoCompleteSelectMultipleGroup, + } + + start_date = forms.DateTimeField( + label=_("Start date"), widget=SelectDateTime, required=True + ) + end_date = forms.DateTimeField( + label=_("End date"), widget=SelectDateTime, required=True + ) + start_candidature = forms.DateTimeField( + label=_("Start candidature"), widget=SelectDateTime, required=True + ) + end_candidature = forms.DateTimeField( + label=_("End candidature"), widget=SelectDateTime, required=True + ) diff --git a/election/views.py b/election/views.py index 25866422..ed0570e5 100644 --- a/election/views.py +++ b/election/views.py @@ -1,183 +1,28 @@ from typing import TYPE_CHECKING -from django import forms from django.contrib.auth.mixins import LoginRequiredMixin, PermissionRequiredMixin from django.core.exceptions import PermissionDenied from django.db import transaction -from django.db.models.query import QuerySet +from django.db.models import QuerySet from django.shortcuts import get_object_or_404, redirect from django.urls import reverse, reverse_lazy -from django.utils.translation import gettext_lazy as _ from django.views.generic import DetailView, ListView from django.views.generic.edit import CreateView, DeleteView, FormView, UpdateView from core.auth.mixins import CanCreateMixin, CanEditMixin, CanViewMixin -from core.views.forms import SelectDateTime -from core.views.widgets.ajax_select import ( - AutoCompleteSelect, - AutoCompleteSelectMultipleGroup, - AutoCompleteSelectUser, +from election.forms import ( + CandidateForm, + ElectionForm, + ElectionListForm, + RoleForm, + VoteForm, ) -from core.views.widgets.markdown import MarkdownInput from election.models import Candidature, Election, ElectionList, Role, Vote if TYPE_CHECKING: from core.models import User -# Custom form field - - -class LimitedCheckboxField(forms.ModelMultipleChoiceField): - """A `ModelMultipleChoiceField`, with a max limit of selectable inputs.""" - - def __init__(self, queryset, max_choice, **kwargs): - self.max_choice = max_choice - super().__init__(queryset, **kwargs) - - def clean(self, value): - qs = super().clean(value) - self.validate(qs) - return qs - - def validate(self, qs): - if qs.count() > self.max_choice: - raise forms.ValidationError( - _("You have selected too much candidates."), code="invalid" - ) - - -# Forms - - -class CandidateForm(forms.ModelForm): - """Form to candidate.""" - - class Meta: - model = Candidature - fields = ["user", "role", "program", "election_list"] - labels = { - "user": _("User to candidate"), - } - widgets = { - "program": MarkdownInput, - "user": AutoCompleteSelectUser, - "role": AutoCompleteSelect, - "election_list": AutoCompleteSelect, - } - - def __init__(self, *args, **kwargs): - election_id = kwargs.pop("election_id", None) - can_edit = kwargs.pop("can_edit", False) - super().__init__(*args, **kwargs) - if election_id: - self.fields["role"].queryset = Role.objects.filter( - election__id=election_id - ).all() - self.fields["election_list"].queryset = ElectionList.objects.filter( - election__id=election_id - ).all() - if not can_edit: - self.fields["user"].widget = forms.HiddenInput() - - -class VoteForm(forms.Form): - def __init__(self, election, user, *args, **kwargs): - super().__init__(*args, **kwargs) - if not election.has_voted(user): - for role in election.roles.all(): - cand = role.candidatures - if role.max_choice > 1: - self.fields[role.title] = LimitedCheckboxField( - cand, role.max_choice, required=False - ) - else: - self.fields[role.title] = forms.ModelChoiceField( - cand, - required=False, - widget=forms.RadioSelect(), - empty_label=_("Blank vote"), - ) - - -class RoleForm(forms.ModelForm): - """Form for creating a role.""" - - class Meta: - model = Role - fields = ["title", "election", "description", "max_choice"] - widgets = {"election": AutoCompleteSelect} - - def __init__(self, *args, **kwargs): - election_id = kwargs.pop("election_id", None) - super().__init__(*args, **kwargs) - if election_id: - self.fields["election"].queryset = Election.objects.filter( - id=election_id - ).all() - - def clean(self): - cleaned_data = super().clean() - title = cleaned_data.get("title") - election = cleaned_data.get("election") - if Role.objects.filter(title=title, election=election).exists(): - raise forms.ValidationError( - _("This role already exists for this election"), code="invalid" - ) - - -class ElectionListForm(forms.ModelForm): - class Meta: - model = ElectionList - fields = ("title", "election") - widgets = {"election": AutoCompleteSelect} - - def __init__(self, *args, **kwargs): - election_id = kwargs.pop("election_id", None) - super().__init__(*args, **kwargs) - if election_id: - self.fields["election"].queryset = Election.objects.filter( - id=election_id - ).all() - - -class ElectionForm(forms.ModelForm): - class Meta: - model = Election - fields = [ - "title", - "description", - "archived", - "start_candidature", - "end_candidature", - "start_date", - "end_date", - "edit_groups", - "view_groups", - "vote_groups", - "candidature_groups", - ] - widgets = { - "edit_groups": AutoCompleteSelectMultipleGroup, - "view_groups": AutoCompleteSelectMultipleGroup, - "vote_groups": AutoCompleteSelectMultipleGroup, - "candidature_groups": AutoCompleteSelectMultipleGroup, - } - - start_date = forms.DateTimeField( - label=_("Start date"), widget=SelectDateTime, required=True - ) - end_date = forms.DateTimeField( - label=_("End date"), widget=SelectDateTime, required=True - ) - start_candidature = forms.DateTimeField( - label=_("Start candidature"), widget=SelectDateTime, required=True - ) - end_candidature = forms.DateTimeField( - label=_("End candidature"), widget=SelectDateTime, required=True - ) - - # Display elections From 6c8743a403d98a6bd58d3e42dcd2fea48e02c52d Mon Sep 17 00:00:00 2001 From: imperosol Date: Fri, 14 Mar 2025 19:19:20 +0100 Subject: [PATCH 2/6] refactor CandidatureForm --- election/forms.py | 25 +++----- ...didature_program_alter_candidature_user.py | 30 ++++++++++ election/models.py | 42 ++++--------- election/views.py | 60 +++++-------------- 4 files changed, 66 insertions(+), 91 deletions(-) create mode 100644 election/migrations/0005_alter_candidature_program_alter_candidature_user.py diff --git a/election/forms.py b/election/forms.py index 5032e859..0e901c4c 100644 --- a/election/forms.py +++ b/election/forms.py @@ -1,12 +1,6 @@ from django import forms -from django.db import transaction -from django.db.models import QuerySet -from django.shortcuts import get_object_or_404 -from django.urls import reverse_lazy from django.utils.translation import gettext_lazy as _ -from django.views.generic import FormView -from core.auth.mixins import CanCreateMixin from core.views.forms import SelectDateTime from core.views.widgets.ajax_select import ( AutoCompleteSelect, @@ -14,7 +8,7 @@ from core.views.widgets.ajax_select import ( AutoCompleteSelectUser, ) from core.views.widgets.markdown import MarkdownInput -from election.models import Candidature, Election, ElectionList, Role, Vote +from election.models import Candidature, Election, ElectionList, Role class LimitedCheckboxField(forms.ModelMultipleChoiceField): @@ -39,6 +33,8 @@ class LimitedCheckboxField(forms.ModelMultipleChoiceField): class CandidateForm(forms.ModelForm): """Form to candidate.""" + required_css_class = "required" + class Meta: model = Candidature fields = ["user", "role", "program", "election_list"] @@ -52,17 +48,12 @@ class CandidateForm(forms.ModelForm): "election_list": AutoCompleteSelect, } - def __init__(self, *args, **kwargs): - election_id = kwargs.pop("election_id", None) - can_edit = kwargs.pop("can_edit", False) + def __init__( + self, *args, election: Election | None, can_edit: bool = False, **kwargs + ): super().__init__(*args, **kwargs) - if election_id: - self.fields["role"].queryset = Role.objects.filter( - election__id=election_id - ).all() - self.fields["election_list"].queryset = ElectionList.objects.filter( - election__id=election_id - ).all() + self.fields["role"].queryset = election.roles.select_related("election") + self.fields["election_list"].queryset = election.election_lists.all() if not can_edit: self.fields["user"].widget = forms.HiddenInput() diff --git a/election/migrations/0005_alter_candidature_program_alter_candidature_user.py b/election/migrations/0005_alter_candidature_program_alter_candidature_user.py new file mode 100644 index 00000000..92930b2d --- /dev/null +++ b/election/migrations/0005_alter_candidature_program_alter_candidature_user.py @@ -0,0 +1,30 @@ +# Generated by Django 4.2.20 on 2025-03-14 18:18 + +import django.db.models.deletion +from django.conf import settings +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ("election", "0004_auto_20191006_0049"), + ] + + operations = [ + migrations.AlterField( + model_name="candidature", + name="program", + field=models.TextField(blank=True, default="", verbose_name="description"), + ), + migrations.AlterField( + model_name="candidature", + name="user", + field=models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="candidates", + to=settings.AUTH_USER_MODEL, + verbose_name="user", + ), + ), + ] diff --git a/election/models.py b/election/models.py index dc6ab474..a3abd019 100644 --- a/election/models.py +++ b/election/models.py @@ -22,21 +22,18 @@ class Election(models.Model): verbose_name=_("edit groups"), blank=True, ) - view_groups = models.ManyToManyField( Group, related_name="viewable_elections", verbose_name=_("view groups"), blank=True, ) - vote_groups = models.ManyToManyField( Group, related_name="votable_elections", verbose_name=_("vote groups"), blank=True, ) - candidature_groups = models.ManyToManyField( Group, related_name="candidate_elections", @@ -45,7 +42,7 @@ class Election(models.Model): ) voters = models.ManyToManyField( - User, verbose_name=("voters"), related_name="voted_elections" + User, verbose_name=_("voters"), related_name="voted_elections" ) archived = models.BooleanField(_("archived"), default=False) @@ -55,20 +52,20 @@ class Election(models.Model): @property def is_vote_active(self): now = timezone.now() - return bool(now <= self.end_date and now >= self.start_date) + return self.start_date <= now <= self.end_date @property def is_vote_finished(self): - return bool(timezone.now() > self.end_date) + return timezone.now() > self.end_date @property def is_candidature_active(self): now = timezone.now() - return bool(now <= self.end_candidature and now >= self.start_candidature) + return self.start_candidature <= now <= self.end_candidature @property def is_vote_editable(self): - return bool(timezone.now() <= self.end_candidature) + return timezone.now() <= self.end_candidature def can_candidate(self, user): for group_id in self.candidature_groups.values_list("pk", flat=True): @@ -95,12 +92,6 @@ class Election(models.Model): results[role.title] = role.results(total_vote) return results - def delete(self, *args, **kwargs): - self.election_lists.all().delete() - super().delete(*args, **kwargs) - - # Permissions - class Role(OrderedModel): """This class allows to create a new role avaliable for a candidature.""" @@ -115,6 +106,9 @@ class Role(OrderedModel): description = models.TextField(_("description"), null=True, blank=True) max_choice = models.IntegerField(_("max choice"), default=1) + def __str__(self): + return f"{self.title} - {self.election.title}" + def results(self, total_vote): results = {} total_vote *= self.max_choice @@ -142,9 +136,6 @@ class Role(OrderedModel): def edit_groups(self): return self.election.edit_groups - def __str__(self): - return ("%s : %s") % (self.election.title, self.title) - class ElectionList(models.Model): """To allow per list vote.""" @@ -163,11 +154,6 @@ class ElectionList(models.Model): def can_be_edited_by(self, user): return user.can_edit(self.election) - def delete(self, *args, **kwargs): - for candidature in self.candidatures.all(): - candidature.delete() - super().delete(*args, **kwargs) - class Candidature(models.Model): """This class is a component of responsability.""" @@ -182,10 +168,9 @@ class Candidature(models.Model): User, verbose_name=_("user"), related_name="candidates", - blank=True, on_delete=models.CASCADE, ) - program = models.TextField(_("description"), null=True, blank=True) + program = models.TextField(_("description"), default="", blank=True) election_list = models.ForeignKey( ElectionList, related_name="candidatures", @@ -196,13 +181,10 @@ class Candidature(models.Model): def __str__(self): return f"{self.role.title} : {self.user.username}" - def delete(self): - for vote in self.votes.all(): - vote.delete() - super().delete() - def can_be_edited_by(self, user): - return (user == self.user) or user.can_edit(self.role.election) + return ( + (user == self.user) or user.can_edit(self.role.election) + ) and self.role.election.is_vote_editable class Vote(models.Model): diff --git a/election/views.py b/election/views.py index ed0570e5..84587a9e 100644 --- a/election/views.py +++ b/election/views.py @@ -155,26 +155,22 @@ class CandidatureCreateView(LoginRequiredMixin, CreateView): def dispatch(self, request, *arg, **kwargs): self.election = get_object_or_404(Election, pk=kwargs["election_id"]) + self.can_edit = self.request.user.can_edit(self.election) return super().dispatch(request, *arg, **kwargs) def get_initial(self): - init = {} - self.can_edit = self.request.user.can_edit(self.election) - init["user"] = self.request.user.id - return init + return {"user": self.request.user.id} def get_form_kwargs(self): - kwargs = super().get_form_kwargs() - kwargs["election_id"] = self.election.id - kwargs["can_edit"] = self.can_edit - return kwargs + return super().get_form_kwargs() | { + "election": self.election, + "can_edit": self.can_edit, + } - def form_valid(self, form): + def form_valid(self, form: CandidateForm): """Verify that the selected user is in candidate group.""" obj = form.instance obj.election = self.election - if not hasattr(obj, "user"): - obj.user = self.request.user if (obj.election.can_candidate(obj.user)) and ( obj.user == self.request.user or self.can_edit ): @@ -182,9 +178,7 @@ class CandidatureCreateView(LoginRequiredMixin, CreateView): raise PermissionDenied def get_context_data(self, **kwargs): - kwargs = super().get_context_data(**kwargs) - kwargs["election"] = self.election - return kwargs + return super().get_context_data(**kwargs) | {"election": self.election} def get_success_url(self, **kwargs): return reverse_lazy("election:detail", kwargs={"election_id": self.election.id}) @@ -302,45 +296,23 @@ class ElectionUpdateView(CanEditMixin, UpdateView): return reverse_lazy("election:detail", kwargs={"election_id": self.object.id}) -class CandidatureUpdateView(CanEditMixin, UpdateView): +class CandidatureUpdateView(LoginRequiredMixin, CanEditMixin, UpdateView): model = Candidature form_class = CandidateForm template_name = "core/edit.jinja" pk_url_kwarg = "candidature_id" - def dispatch(self, request, *arg, **kwargs): - self.object = self.get_object() - if not self.object.role.election.is_vote_editable: - raise PermissionDenied - return super().dispatch(request, *arg, **kwargs) - - def remove_fields(self): - self.form.fields.pop("role", None) - - def get(self, request, *args, **kwargs): - self.form = self.get_form() - self.remove_fields() - return self.render_to_response(self.get_context_data(form=self.form)) - - def post(self, request, *args, **kwargs): - self.form = self.get_form() - self.remove_fields() - if ( - request.user.is_authenticated - and request.user.can_edit(self.object) - and self.form.is_valid() - ): - return super().form_valid(self.form) - return self.form_invalid(self.form) + def get_form(self, *args, **kwargs): + form = super().get_form(*args, **kwargs) + form.fields.pop("role", None) + return form def get_form_kwargs(self): - kwargs = super().get_form_kwargs() - kwargs["election_id"] = self.object.role.election.id - return kwargs + return super().get_form_kwargs() | {"election": self.object.role.election} def get_success_url(self, **kwargs): - return reverse_lazy( - "election:detail", kwargs={"election_id": self.object.role.election.id} + return reverse( + "election:detail", kwargs={"election_id": self.object.role.election_id} ) From 57522d89c2c7b15169b6615ac165052da4fabed6 Mon Sep 17 00:00:00 2001 From: imperosol Date: Sun, 16 Mar 2025 18:43:43 +0100 Subject: [PATCH 3/6] feat: add x-limited-choices directive --- core/static/bundled/alpine-index.js | 3 +- core/static/bundled/alpine/limited-choices.ts | 69 +++++++++++++++++++ .../templates/election/election_detail.jinja | 35 ++-------- 3 files changed, 78 insertions(+), 29 deletions(-) create mode 100644 core/static/bundled/alpine/limited-choices.ts diff --git a/core/static/bundled/alpine-index.js b/core/static/bundled/alpine-index.js index 211600a5..4e04e7dd 100644 --- a/core/static/bundled/alpine-index.js +++ b/core/static/bundled/alpine-index.js @@ -1,7 +1,8 @@ +import { limitedChoices } from "#core:alpine/limited-choices"; import sort from "@alpinejs/sort"; import Alpine from "alpinejs"; -Alpine.plugin(sort); +Alpine.plugin([sort, limitedChoices]); window.Alpine = Alpine; window.addEventListener("DOMContentLoaded", () => { diff --git a/core/static/bundled/alpine/limited-choices.ts b/core/static/bundled/alpine/limited-choices.ts new file mode 100644 index 00000000..211441d0 --- /dev/null +++ b/core/static/bundled/alpine/limited-choices.ts @@ -0,0 +1,69 @@ +import type { Alpine as AlpineType } from "alpinejs"; + +export function limitedChoices(Alpine: AlpineType) { + /** + * Directive to limit the number of elements + * that can be selected in a group of checkboxes. + * + * When the max numbers of selectable elements is reached, + * new elements will still be inserted, but oldest ones will be deselected. + * For example, if checkboxes A, B and C have been selected and the max + * number of selections is 3, then selecting D will result in having + * B, C and D selected. + * + * # Example in template + * ```html + *
+ * + * + * + * + * + *
+ * ``` + */ + Alpine.directive( + "limited-choices", + (el, { expression }, { evaluateLater, effect }) => { + const getMaxChoices = evaluateLater(expression); + let maxChoices: number; + const inputs: HTMLInputElement[] = Array.from( + el.querySelectorAll("input[type='checkbox']"), + ); + const checked = [] as HTMLInputElement[]; + + const manageDequeue = () => { + if (checked.length <= maxChoices) { + // There isn't too many checkboxes selected. Nothing to do + return; + } + const popped = checked.splice(0, checked.length - maxChoices); + for (const p of popped) { + p.checked = false; + } + }; + + for (const input of inputs) { + input.addEventListener("change", (_e) => { + if (input.checked) { + checked.push(input); + } else { + checked.splice(checked.indexOf(input), 1); + } + manageDequeue(); + }); + } + effect(() => { + getMaxChoices((value: string) => { + const previousValue = maxChoices; + maxChoices = Number.parseInt(value); + if (maxChoices < previousValue) { + // The maximum number of selectable items has been lowered. + // Some currently selected elements may need to be removed + manageDequeue(); + } + }); + }); + }, + ); +} diff --git a/election/templates/election/election_detail.jinja b/election/templates/election/election_detail.jinja index 28b2a956..2c628211 100644 --- a/election/templates/election/election_detail.jinja +++ b/election/templates/election/election_detail.jinja @@ -63,7 +63,13 @@ {%- for role in role_list %} {%- set count = [0] %} {%- set role_data = election_form.data.getlist(role.title) if role.title in election_form.data else [] %} - + + 1 -%} + x-data x-limited-choices="{{ role.max_choice }}" + {%- endif %} + class="role {% if role.title in election_form.errors %}role_error{% endif %}" + >
@@ -197,30 +203,3 @@ {%- endif %} {% endblock %} - -{% block script %} - {{ super() }} - -{% endblock %} From e222ac67621320655ff8bbbc7e1906d250b43dc4 Mon Sep 17 00:00:00 2001 From: imperosol Date: Sun, 16 Mar 2025 19:11:08 +0100 Subject: [PATCH 4/6] refactor election detail view --- election/forms.py | 32 ++++---- election/models.py | 3 +- .../templates/election/election_detail.jinja | 67 ++++++++-------- election/views.py | 76 +++++++++--------- locale/fr/LC_MESSAGES/django.po | 78 ++++++++++--------- 5 files changed, 133 insertions(+), 123 deletions(-) diff --git a/election/forms.py b/election/forms.py index 0e901c4c..8a4f7601 100644 --- a/election/forms.py +++ b/election/forms.py @@ -1,6 +1,7 @@ from django import forms from django.utils.translation import gettext_lazy as _ +from core.models import User from core.views.forms import SelectDateTime from core.views.widgets.ajax_select import ( AutoCompleteSelect, @@ -59,22 +60,23 @@ class CandidateForm(forms.ModelForm): class VoteForm(forms.Form): - def __init__(self, election, user, *args, **kwargs): + def __init__(self, election: Election, user: User, *args, **kwargs): super().__init__(*args, **kwargs) - if not election.has_voted(user): - for role in election.roles.all(): - cand = role.candidatures - if role.max_choice > 1: - self.fields[role.title] = LimitedCheckboxField( - cand, role.max_choice, required=False - ) - else: - self.fields[role.title] = forms.ModelChoiceField( - cand, - required=False, - widget=forms.RadioSelect(), - empty_label=_("Blank vote"), - ) + if election.can_vote(user): + return + for role in election.roles.all(): + cand = role.candidatures + if role.max_choice > 1: + self.fields[role.title] = LimitedCheckboxField( + cand, role.max_choice, required=False + ) + else: + self.fields[role.title] = forms.ModelChoiceField( + cand, + required=False, + widget=forms.RadioSelect(), + empty_label=_("Blank vote"), + ) class RoleForm(forms.ModelForm): diff --git a/election/models.py b/election/models.py index a3abd019..fc3a90dc 100644 --- a/election/models.py +++ b/election/models.py @@ -1,5 +1,6 @@ from django.db import models from django.utils import timezone +from django.utils.functional import cached_property from django.utils.translation import gettext_lazy as _ from ordered_model.models import OrderedModel @@ -84,7 +85,7 @@ class Election(models.Model): def has_voted(self, user): return self.voters.filter(id=user.id).exists() - @property + @cached_property def results(self): results = {} total_vote = self.voters.count() diff --git a/election/templates/election/election_detail.jinja b/election/templates/election/election_detail.jinja index 2c628211..b93ab9b7 100644 --- a/election/templates/election/election_detail.jinja +++ b/election/templates/election/election_detail.jinja @@ -31,7 +31,7 @@ {% trans %} at {% endtrans %}

- {%- if election.has_voted(user) %} + {%- if user_has_voted %}

{%- if election.is_vote_active %} {% trans %}You already have submitted your vote.{% endtrans %} @@ -45,12 +45,11 @@

{% csrf_token %} - {%- set election_lists = election.election_lists.all() -%} - + {%- for election_list in election_lists %} - - {%- set role_list = election.roles.order_by('order').all() %} - {%- for role in role_list %} - {%- set count = [0] %} + {%- for role in election_roles %} {%- set role_data = election_form.data.getlist(role.title) if role.title in election_form.data else [] %}

{{ role.title }}

{{ role.description }}

- {%- if role.max_choice > 1 and not election.has_voted(user) and election.can_vote(user) %} - {% trans %}You may choose up to{% endtrans %} {{ role.max_choice }} {% trans %}people.{% endtrans %} + {%- if role.max_choice > 1 and show_vote_buttons %} + + {% trans trimmed nb_choices=role.max_choice %} + You may choose up to {{ nb_choices }} people. + {% endtrans %} + {%- endif %} {%- if election_form.errors[role.title] is defined %} @@ -87,36 +88,40 @@ {% if user.can_edit(role) and election.is_vote_editable -%}
- - - {%- if role == role_list.last() %} + ️ + + + + + + {%- if loop.last -%} - {%- else %} + {%- else -%} - {%- endif %} - {% if role == role_list.first() %} + {%- endif -%} + {%- if loop.first -%} - {% else %} + {%- else -%} - {% endif %} + {%- endif -%}
{%- endif -%} - {%- for election_list in election_lists %} -
{% trans %}Blank vote{% endtrans %}{% trans %}Blank vote{% endtrans %} + {{ election_list.title }} {% if user.can_edit(election_list) and election.is_vote_editable -%} @@ -59,9 +58,7 @@ {%- endfor %}
- {%- if role.max_choice == 1 and election.can_vote(user) %} + + {%- if role.max_choice == 1 and show_vote_buttons %}
- -
- {%- set _ = count.append(count.pop() + 1) %} {%- endif %} {%- if election.is_vote_finished %} {%- set results = election_results[role.title]['blank vote'] %} @@ -126,13 +131,14 @@ {%- endif %}
+
    - {%- for candidature in election_list.candidatures.filter(role=role) %} + {%- for candidature in election_list.candidatures.select_related("user", "user__profile_pict").filter(role=role) %}
  • - {%- if election.can_vote(user) %} - -