Compare commits

..

2 Commits

Author SHA1 Message Date
TitouanDor
8d74d18a25 add test_election_form 2026-01-20 22:14:56 +01:00
Sli
2744282fd8 fix: bad value for blank vote and better flow for invalid form
* Add an error message when looking at a public election without being logged in
* Add correct value for blank vote on single vote field
* Redirect to view with an error message if an invalid form has been submitted
2026-01-14 11:45:12 +01:00
7 changed files with 126 additions and 31 deletions

View File

@@ -104,7 +104,7 @@
</div>
<ul>
<li x-show="getBasketSize() === 0">{% trans %}This basket is empty{% endtrans %}</li>
<template x-for="(item, index) in Object.values(basket)" :key="item.product.id">
<template x-for="(item, index) in Object.values(basket)">
<li>
<template x-for="error in item.errors">
<div class="alert alert-red" x-text="error">

View File

@@ -60,8 +60,6 @@ class CandidateForm(forms.ModelForm):
class VoteForm(forms.Form):
def __init__(self, election: Election, user: User, *args, **kwargs):
super().__init__(*args, **kwargs)
if not election.can_vote(user):
return
for role in election.roles.all():
cand = role.candidatures
if role.max_choice > 1:
@@ -74,6 +72,7 @@ class VoteForm(forms.Form):
required=False,
widget=forms.RadioSelect(),
empty_label=_("Blank vote"),
blank=True,
)

View File

@@ -14,6 +14,11 @@
{% block content %}
<h3 class="election__title">{{ election.title }}</h3>
{% if user.is_anonymous %}
<div class="alert alert-red">
{% trans %}You are not logged in, candidate pictures won't display for privacy reasons.{% endtrans %}
</div>
{% endif %}
<p class="election__description">{{ election.description }}</p>
<hr>
<section class="election_details">
@@ -117,7 +122,7 @@
{%- if role.max_choice == 1 and show_vote_buttons %}
<div class="radio-btn">
{% set input_id = "blank_vote_" + role.id|string %}
<input id="{{ input_id }}" type="radio" name="{{ role.title }}">
<input id="{{ input_id }}" type="radio" name="{{ role.title }}" value="" checked>
<label for="{{ input_id }}">
<span>{% trans %}Choose blank vote{% endtrans %}</span>
</label>
@@ -185,6 +190,7 @@
</table>
</form>
</section>
{% if not user.is_anonymous %}
<section class="buttons">
{%- if (election.can_candidate(user) and election.is_candidature_active) or (user.can_edit(election) and election.is_vote_editable) %}
<a class="button" href="{{ url('election:candidate', election_id=object.id) }}">{% trans %}Candidate{% endtrans %}</a>
@@ -207,4 +213,5 @@
<button class="button button_send" form="vote-form">{% trans %}Submit the vote !{% endtrans %}</button>
</section>
{%- endif %}
{% endif %}
{% endblock %}

View File

@@ -115,3 +115,72 @@ def test_election_results():
"total vote": 100,
},
}
@pytest.mark.django_db
def test_election_form(client : Client):
election = baker.make(
Election,
end_date = now() + timedelta(days=1),
)
group = baker.make(Group)
election.vote_groups.add(group)
lists = baker.make(ElectionList, election=election, _quantity=2, _bulk_create=True)
roles = baker.make(Role, election=election, _quantity=2, _bulk_create=True)
users = baker.make(User, _quantity=4, _bulk_create=True)
cand = [
baker.make(Candidature, role=roles[0], user=users[0], election_list=lists[0]),
baker.make(Candidature, role=roles[0], user=users[1], election_list=lists[1]),
baker.make(Candidature, role=roles[1], user=users[2], election_list=lists[0]),
baker.make(Candidature, role=roles[1], user=users[3], election_list=lists[1]),
]
url = reverse("election:vote", kwargs={"election_id": election.id})
votes = [
{
roles[0].title : "",
roles[1].title : str(cand[2].id),
},
{
roles[0].title : "",
roles[1].title : "",
},
{
roles[0].title : str(cand[0].id),
roles[1].title : str(cand[2].id),
},
{
roles[0].title : str(cand[0].id),
roles[1].title : str(cand[3].id),
},
]
NB_VOTER = len(votes)
voters = [subscriber_user.make() for _ in range(NB_VOTER)]
for i in range(NB_VOTER):
voter = voters[i]
voter.groups.add(group)
if not election.can_vote(voter):
assert False
client.force_login(voter)
reponse = client.post(url, data = votes[i])
assert reponse.status_code == 302
assert election.results == {
roles[0].title: {
cand[0].user.username: {"percent": 50.0, "vote": 2},
cand[1].user.username: {"percent": 0.0, "vote": 0},
"blank vote": {"percent": 50.0, "vote": 2},
"total vote": 4,
},
roles[1].title: {
cand[2].user.username: {"percent": 50.0, "vote": 2},
cand[3].user.username: {"percent": 25.0, "vote": 1},
"blank vote": {"percent": 25.0, "vote": 1},
"total vote": 4,
},
}

View File

@@ -2,6 +2,7 @@ from typing import TYPE_CHECKING
from cryptography.utils import cached_property
from django.conf import settings
from django.contrib import messages
from django.contrib.auth.mixins import (
LoginRequiredMixin,
PermissionRequiredMixin,
@@ -10,8 +11,9 @@ from django.contrib.auth.mixins import (
from django.core.exceptions import PermissionDenied
from django.db import transaction
from django.db.models import QuerySet
from django.shortcuts import get_object_or_404
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
@@ -53,7 +55,7 @@ class ElectionListArchivedView(CanViewMixin, ListView):
class ElectionDetailView(CanViewMixin, DetailView):
"""Details an election responsability by responsability."""
"""Details an election responsibility by responsibility."""
model = Election
template_name = "election/election_detail.jinja"
@@ -83,7 +85,7 @@ class ElectionDetailView(CanViewMixin, DetailView):
return super().get(request, *arg, **kwargs)
def get_context_data(self, **kwargs):
"""Add additionnal data to the template."""
"""Add additional data to the template."""
user: User = self.request.user
return super().get_context_data(**kwargs) | {
"election_form": VoteForm(self.object, user),
@@ -101,7 +103,7 @@ class ElectionDetailView(CanViewMixin, DetailView):
class VoteFormView(LoginRequiredMixin, UserPassesTestMixin, FormView):
"""Alows users to vote."""
"""Allows users to vote."""
form_class = VoteForm
template_name = "election/election_detail.jinja"
@@ -111,6 +113,9 @@ class VoteFormView(LoginRequiredMixin, UserPassesTestMixin, FormView):
return get_object_or_404(Election, pk=self.kwargs["election_id"])
def test_func(self):
if not self.election.can_vote(self.request.user):
return False
groups = set(self.election.vote_groups.values_list("id", flat=True))
if (
settings.SITH_GROUP_SUBSCRIBERS_ID in groups
@@ -150,11 +155,17 @@ class VoteFormView(LoginRequiredMixin, UserPassesTestMixin, FormView):
self.vote(data)
return super().form_valid(form)
def form_invalid(self, form):
messages.error(self.request, _("Form is invalid"))
return redirect(
reverse("election:detail", kwargs={"election_id": self.election.id}),
)
def get_success_url(self, **kwargs):
return reverse_lazy("election:detail", kwargs={"election_id": self.election.id})
def get_context_data(self, **kwargs):
"""Add additionnal data to the template."""
"""Add additional data to the template."""
kwargs = super().get_context_data(**kwargs)
kwargs["object"] = self.election
kwargs["election"] = self.election

View File

@@ -6,7 +6,7 @@
msgid ""
msgstr ""
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2025-12-19 23:10+0100\n"
"POT-Creation-Date: 2026-01-14 11:34+0100\n"
"PO-Revision-Date: 2016-07-18\n"
"Last-Translator: Maréchal <thomas.girod@utbm.fr\n"
"Language-Team: AE info <ae.info@utbm.fr>\n"
@@ -4108,6 +4108,11 @@ msgstr "Candidater"
msgid "Candidature are closed for this election"
msgstr "Les candidatures sont fermées pour cette élection"
#: election/templates/election/election_detail.jinja
msgid ""
"You are not logged in, candidate pictures won't display for privacy reasons."
msgstr "Vous n'êtes pas connecté, les photos des candidats ne s'afficheront pas pour des raisons de respect de la vie privée."
#: election/templates/election/election_detail.jinja
msgid "Polls close "
msgstr "Votes fermés"
@@ -4183,6 +4188,10 @@ msgstr "au"
msgid "Polls open from"
msgstr "Votes ouverts du"
#: election/views.py
msgid "Form is invalid"
msgstr "Formulaire invalide"
#: forum/models.py
msgid "is a category"
msgstr "est une catégorie"

View File

@@ -31,7 +31,7 @@ document.addEventListener("alpine:init", () => {
await Promise.all(
this.downloadPictures.map(async (p: PictureSchema) => {
const imgName = `${p.album.name}/IMG_${p.id}_${p.date.replace(/[:\-]/g, "_")}${p.name.slice(p.name.lastIndexOf("."))}`;
const imgName = `${p.album}/IMG_${p.date.replace(/[:\-]/g, "_")}${p.name.slice(p.name.lastIndexOf("."))}`;
return zipWriter.add(imgName, new HttpReader(p.full_size_url), {
level: 9,
lastModDate: new Date(p.date),