add tests

This commit is contained in:
imperosol
2026-06-05 00:14:13 +02:00
parent 00fb8a719f
commit f3e78f229a
9 changed files with 356 additions and 61 deletions
-9
View File
@@ -106,15 +106,6 @@ class RoleForm(forms.ModelForm):
is_board=True, club__in=election.clubs.all() is_board=True, club__in=election.clubs.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 ElectionListForm(forms.ModelForm):
class Meta: class Meta:
@@ -50,4 +50,13 @@ class Migration(migrations.Migration):
default=1, verbose_name="max choice" default=1, verbose_name="max choice"
), ),
), ),
migrations.AddConstraint(
model_name="role",
constraint=models.UniqueConstraint(
fields=("title", "election"),
name="title_election_unique_constraint",
violation_error_code="invalid",
violation_error_message="This role already exists for this election",
),
),
] ]
+20 -1
View File
@@ -5,7 +5,7 @@ from django.utils.functional import cached_property
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from ordered_model.models import OrderedModel from ordered_model.models import OrderedModel
from club.models import Club, ClubRole from club.models import Club, ClubRole, Membership
from core.models import Group, User from core.models import Group, User
@@ -101,6 +101,15 @@ class Election(models.Model):
results[role.title] = role.results(total_vote) results[role.title] = role.results(total_vote)
return results return results
@cached_property
def results_applied(self) -> bool:
"""Returns True if one or more roles of this election have been applied."""
return Membership.objects.filter(
role__election_roles__election=self,
end_date=None,
start_date__gte=self.end_date,
).exists()
class Role(OrderedModel): class Role(OrderedModel):
"""This class allows to create a new role available for a candidature.""" """This class allows to create a new role available for a candidature."""
@@ -130,6 +139,16 @@ class Role(OrderedModel):
order_with_respect_to = "election" order_with_respect_to = "election"
class Meta(OrderedModel.Meta):
constraints = [
models.UniqueConstraint(
fields=["title", "election"],
name="title_election_unique_constraint",
violation_error_message=_("This role already exists for this election"),
violation_error_code="invalid",
)
]
def __str__(self): def __str__(self):
return f"{self.title} - {self.election.title}" return f"{self.title} - {self.election.title}"
@@ -7,7 +7,7 @@
was linked to a club role. was linked to a club role.
{% endtrans %} {% endtrans %}
</p> </p>
{% elif already_applied %} {% elif form.election.results_applied %}
<em> <em>
{%- trans trimmed -%} {%- trans trimmed -%}
The results of this election have been applied The results of this election have been applied
View File
+191
View File
@@ -0,0 +1,191 @@
import itertools
from datetime import timedelta
from bs4 import BeautifulSoup
from django.contrib.auth.models import Permission
from django.test import TestCase
from django.urls import reverse
from django.utils.timezone import localdate, now
from model_bakery import baker, seq
from model_bakery.recipe import Recipe
from pytest_django.asserts import assertRedirects
from club.models import Club, ClubRole, Membership
from core.baker_recipes import subscriber_user
from core.models import Group, User
from election.models import Candidature, Election, ElectionList, Role, Vote
class TestApplyResult(TestCase):
@classmethod
def setUpTestData(cls):
# setup is a little bit complicated, but we have to make a whole
# election to test result application, including the election,
# the lists, the roles, the candidates and the votes.
cls.club = baker.make(Club)
cls.club_roles = baker.make(
ClubRole,
club=cls.club,
is_presidency=iter([True, False, False]),
is_board=True,
_quantity=3,
_bulk_create=True,
)
cls.election = baker.make(
Election,
clubs=[cls.club],
edit_groups=[baker.make(Group)],
end_date=now() - timedelta(minutes=1),
)
lists = baker.make(
ElectionList, election=cls.election, _quantity=2, _bulk_create=True
)
role_recipe = Recipe(Role, election=cls.election, title=seq("election role "))
roles = [
*role_recipe.make(
club_role=iter(cls.club_roles), _quantity=len(cls.club_roles)
),
role_recipe.make(),
]
roles[1].max_choice = 2
roles[1].save()
cls.candidatures = baker.make(
Candidature,
election_list=itertools.chain(
itertools.repeat(lists[0], len(roles)),
itertools.repeat(lists[1], len(roles)),
),
role=itertools.cycle(roles),
user=iter(
baker.make(
User, username=seq("user "), _quantity=len(lists) * len(roles)
)
),
_quantity=len(lists) * len(roles),
_bulk_create=True,
)
votes = iter(
baker.make(
Vote,
role=itertools.cycle(roles),
_quantity=6 * len(roles),
_bulk_create=True,
)
)
through = []
for cand in cls.candidatures:
nb_voices = 4 if cand.election_list_id == lists[0].id else 2
through.extend(
[
Vote.candidature.through(candidature=cand, vote=v)
for v in itertools.islice(votes, nb_voices)
]
)
Vote.candidature.through.objects.bulk_create(through)
cls.election.voters.set(baker.make(User, _quantity=8, _bulk_create=True))
cls.url = reverse(
"election:apply_result", kwargs={"election_id": cls.election.id}
)
def test_election_result(self):
# we have made a complex setup, so testing the results is
# useful to be sure we didn't make mistake when generating data
assert self.election.results == {
"election role 1": {
"blank vote": {"percent": 25.0, "vote": 2},
"total vote": 8,
"user 1": {"percent": 50.0, "vote": 4},
"user 5": {"percent": 25.0, "vote": 2},
},
"election role 2": {
"blank vote": {"percent": 62.5, "vote": 10},
"total vote": 16,
"user 2": {"percent": 25.0, "vote": 4},
"user 6": {"percent": 12.5, "vote": 2},
},
"election role 3": {
"blank vote": {"percent": 25.0, "vote": 2},
"total vote": 8,
"user 3": {"percent": 50.0, "vote": 4},
"user 7": {"percent": 25.0, "vote": 2},
},
"election role 4": {
"blank vote": {"percent": 25.0, "vote": 2},
"total vote": 8,
"user 4": {"percent": 50.0, "vote": 4},
"user 8": {"percent": 25.0, "vote": 2},
},
}
def test_apply_result(self):
user = baker.make(
User, user_permissions=[Permission.objects.get(codename="add_membership")]
)
self.client.force_login(user)
response = self.client.get(self.url)
soup = BeautifulSoup(response.text, "lxml")
inputs = soup.find_all("input", attrs={"type": "checkbox"})
assert all("checked" in i.attrs for i in inputs)
ids = {int(i.attrs["value"]) for i in inputs}
assert ids == {
self.candidatures[0].id,
self.candidatures[1].id,
self.candidatures[2].id,
self.candidatures[5].id,
}
response = self.client.post(
self.url, data={"candidates": ids.difference({self.candidatures[5].id})}
)
assertRedirects(response, self.url)
for candidate in self.candidatures[0:3]:
assert Membership.objects.filter(
start_date=localdate(),
end_date=None,
user=candidate.user,
role=candidate.role.club_role,
).exists()
assert self.club.members_group.users.contains(candidate.user)
assert self.club.board_group.users.contains(candidate.user)
# candidatures[5] was unchecked, so it shouldn't receive a club role
assert not self.candidatures[5].user.memberships.exists()
# now that results are applied, it shouldn't be possible to replay the request
response = self.client.get(self.url)
assert "Les résultats de cette élection ont été appliqués" in response.text
response = self.client.post(self.url, data={"candidates": ids})
assert response.status_code == 403
def test_no_result_to_apply(self):
self.election.roles.update(club_role=None)
user = baker.make(
User, user_permissions=[Permission.objects.get(codename="add_membership")]
)
self.client.force_login(user)
response = self.client.get(self.url)
soup = BeautifulSoup(response.text, "lxml")
assert not soup.find("input", attrs={"type": "checkbox"})
assert "Pas de résultats à appliquer" in response.text
def test_access_denied(self):
user = subscriber_user.make()
self.client.force_login(user)
response = self.client.get(self.url)
assert response.status_code == 403
response = self.client.post(
self.url, data={"candidates": [self.candidatures[0].id]}
)
assert response.status_code == 403
def test_election_not_finished(self):
user = baker.make(
User, user_permissions=[Permission.objects.get(codename="add_membership")]
)
self.election.end_date = now() + timedelta(minutes=1)
self.election.save()
self.client.force_login(user)
response = self.client.get(self.url)
assert response.status_code == 403
response = self.client.post(
self.url, data={"candidates": [self.candidatures[0].id]}
)
assert response.status_code == 403
+110
View File
@@ -0,0 +1,110 @@
from datetime import timedelta
import pytest
from django.conf import settings
from django.contrib.auth.models import Permission
from django.test import TestCase
from django.urls import reverse
from django.utils.timezone import now
from model_bakery import baker
from pytest_django.asserts import assertRedirects
from club.models import Club, ClubRole
from core.baker_recipes import subscriber_user
from core.models import Group, User
from election.models import Election, Role
@pytest.mark.django_db
class TestCreateRole(TestCase):
@classmethod
def setUpTestData(cls):
cls.club = baker.make(Club)
cls.edit_group = baker.make(Group)
cls.election = baker.make(
Election,
clubs=[cls.club],
edit_groups=[cls.edit_group],
view_groups=[Group.objects.get(id=settings.SITH_GROUP_PUBLIC_ID)],
end_candidature=now() + timedelta(days=1),
)
cls.url = reverse(
"election:create_role", kwargs={"election_id": cls.election.id}
)
cls.election_url = reverse(
"election:detail", kwargs={"election_id": cls.election.id}
)
cls.permission = Permission.objects.get(codename="add_role")
def assert_role_creation_ok(self):
response = self.client.get(self.url)
assert response.status_code == 200
response = self.client.post(self.url, data={"title": "foo", "max_choice": 1})
assertRedirects(response, self.election_url)
roles = list(self.election.roles.all())
assert len(roles) == 1
assert roles[0].title == "foo"
def assert_role_creation_denied(self):
initial_role_count = self.election.roles.count()
response = self.client.get(self.url)
assert response.status_code == 403
response = self.client.post(self.url, data={"title": "foo", "max_choice": 1})
assert response.status_code == 403
assert self.election.roles.count() == initial_role_count
def test_admin(self):
user = baker.make(User, user_permissions=[self.permission])
self.client.force_login(user)
self.assert_role_creation_ok()
def test_edit_group(self):
user = baker.make(User, groups=[self.edit_group])
self.client.force_login(user)
self.assert_role_creation_ok()
def test_role_linked_to_club_role(self):
user = baker.make(User, user_permissions=[self.permission])
self.client.force_login(user)
club_role = baker.make(ClubRole, is_board=True, club=self.club)
response = self.client.post(
self.url, data={"title": "foo", "max_choice": 1, "club_role": club_role.id}
)
assertRedirects(response, self.election_url)
roles = list(self.election.roles.all())
assert len(roles) == 1
assert roles[0].title == "foo"
assert roles[0].club_role == club_role
def test_permission_denied(self):
user = subscriber_user.make()
self.client.force_login(user)
self.assert_role_creation_denied()
def test_election_not_editable(self):
user = baker.make(User, user_permissions=[self.permission])
self.election.end_candidature = now() - timedelta(minutes=1)
self.election.save()
self.client.force_login(user)
self.assert_role_creation_denied()
class TestUpdateRole(TestCreateRole):
@classmethod
def setUpTestData(cls):
# TestUpdateRole is just TestCreateRole, but with different parameters
cls.club = baker.make(Club)
cls.edit_group = baker.make(Group)
cls.election = baker.make(
Election,
clubs=[cls.club],
edit_groups=[cls.edit_group],
view_groups=[Group.objects.get(id=settings.SITH_GROUP_PUBLIC_ID)],
end_candidature=now() + timedelta(days=1),
)
cls.role = baker.make(Role, election=cls.election)
cls.url = reverse("election:update_role", kwargs={"role_id": cls.role.id})
cls.election_url = reverse(
"election:detail", kwargs={"election_id": cls.election.id}
)
cls.permission = Permission.objects.get(codename="change_role")
+25 -50
View File
@@ -16,9 +16,7 @@ from django.utils.translation import gettext_lazy as _
from django.views.generic import DetailView, ListView from django.views.generic import DetailView, ListView
from django.views.generic.edit import CreateView, DeleteView, FormView, UpdateView from django.views.generic.edit import CreateView, DeleteView, FormView, UpdateView
from club.models import Membership
from core.auth.mixins import CanEditMixin, CanViewMixin from core.auth.mixins import CanEditMixin, CanViewMixin
from core.views import FragmentMixin
from election.forms import ( from election.forms import (
ApplyElectionResultForm, ApplyElectionResultForm,
CandidateForm, CandidateForm,
@@ -232,11 +230,8 @@ class RoleCreateView(LoginRequiredMixin, UserPassesTestMixin, CreateView):
def test_func(self): def test_func(self):
if not self.election.is_vote_editable: if not self.election.is_vote_editable:
return False return False
if self.request.user.has_perm("election.add_role"): user = self.request.user
return True return user.has_perm("election.add_role") or user.can_edit(self.election)
return self.election.edit_groups.filter(
id__in=self.request.user.all_groups
).exists()
def get_form_kwargs(self): def get_form_kwargs(self):
return super().get_form_kwargs() | {"election": self.election} return super().get_form_kwargs() | {"election": self.election}
@@ -309,46 +304,30 @@ class CandidatureUpdateView(LoginRequiredMixin, CanEditMixin, UpdateView):
) )
class RoleUpdateView(CanEditMixin, UpdateView): class RoleUpdateView(UserPassesTestMixin, UpdateView):
model = Role model = Role
form_class = RoleForm form_class = RoleForm
template_name = "election/role_form.jinja" template_name = "election/role_form.jinja"
pk_url_kwarg = "role_id" pk_url_kwarg = "role_id"
def dispatch(self, request, *arg, **kwargs): @cached_property
self.object = self.get_object() def election(self):
if not self.object.election.is_vote_editable: return self.get_object().election
raise PermissionDenied
return super().dispatch(request, *arg, **kwargs)
def get(self, request, *args, **kwargs): def test_func(self):
self.object = self.get_object() if not self.election.is_vote_editable:
self.form = self.get_form() return False
return self.render_to_response(self.get_context_data(form=self.form)) user = self.request.user
return user.has_perm("election.change_role") or user.can_edit(self.election)
def post(self, request, *args, **kwargs):
self.object = self.get_object()
self.form = self.get_form()
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_kwargs(self):
kwargs = super().get_form_kwargs()
kwargs["election"] = self.object.election
return kwargs
def get_success_url(self, **kwargs):
return reverse_lazy(
"election:detail", kwargs={"election_id": self.object.election.id}
)
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
return super().get_context_data(**kwargs) | {"election": self.object.election} return super().get_context_data(**kwargs) | {"election": self.election}
def get_form_kwargs(self):
return super().get_form_kwargs() | {"election": self.election}
def get_success_url(self, **kwargs):
return reverse("election:detail", kwargs={"election_id": self.election.id})
# Delete Views # Delete Views
@@ -410,9 +389,7 @@ class ElectionListDeleteView(CanEditMixin, DeleteView):
return reverse("election:detail", kwargs={"election_id": self.election.id}) return reverse("election:detail", kwargs={"election_id": self.election.id})
class ApplyResultFragment( class ApplyResultFragment(LoginRequiredMixin, UserPassesTestMixin, FormView):
LoginRequiredMixin, UserPassesTestMixin, FragmentMixin, FormView
):
template_name = "election/fragments/apply_result.jinja" template_name = "election/fragments/apply_result.jinja"
form_class = ApplyElectionResultForm form_class = ApplyElectionResultForm
@@ -429,6 +406,11 @@ class ApplyResultFragment(
id__in=self.request.user.all_groups id__in=self.request.user.all_groups
).exists() ).exists()
def post(self, request, *args, **kwargs):
if self.election.results_applied:
raise PermissionDenied
return super().post(request, *args, **kwargs)
def get_form_kwargs(self): def get_form_kwargs(self):
return super().get_form_kwargs() | {"election": self.election} return super().get_form_kwargs() | {"election": self.election}
@@ -437,14 +419,7 @@ class ApplyResultFragment(
return super().form_valid(form) return super().form_valid(form)
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
return super().get_context_data(**kwargs) | { return super().get_context_data(**kwargs) | {"clubs": self.election.clubs.all()}
"already_applied": Membership.objects.filter(
role__election_roles__election=self.election,
end_date=None,
start_date__gte=self.election.end_date,
).exists(),
"clubs": self.election.clubs.all(),
}
def get_success_url(self, **kwargs): def get_success_url(self, **kwargs):
return reverse( return reverse(