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()
)
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:
@@ -50,4 +50,13 @@ class Migration(migrations.Migration):
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 ordered_model.models import OrderedModel
from club.models import Club, ClubRole
from club.models import Club, ClubRole, Membership
from core.models import Group, User
@@ -101,6 +101,15 @@ class Election(models.Model):
results[role.title] = role.results(total_vote)
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):
"""This class allows to create a new role available for a candidature."""
@@ -130,6 +139,16 @@ class Role(OrderedModel):
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):
return f"{self.title} - {self.election.title}"
@@ -7,7 +7,7 @@
was linked to a club role.
{% endtrans %}
</p>
{% elif already_applied %}
{% elif form.election.results_applied %}
<em>
{%- trans trimmed -%}
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.edit import CreateView, DeleteView, FormView, UpdateView
from club.models import Membership
from core.auth.mixins import CanEditMixin, CanViewMixin
from core.views import FragmentMixin
from election.forms import (
ApplyElectionResultForm,
CandidateForm,
@@ -232,11 +230,8 @@ class RoleCreateView(LoginRequiredMixin, UserPassesTestMixin, CreateView):
def test_func(self):
if not self.election.is_vote_editable:
return False
if self.request.user.has_perm("election.add_role"):
return True
return self.election.edit_groups.filter(
id__in=self.request.user.all_groups
).exists()
user = self.request.user
return user.has_perm("election.add_role") or user.can_edit(self.election)
def get_form_kwargs(self):
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
form_class = RoleForm
template_name = "election/role_form.jinja"
pk_url_kwarg = "role_id"
def dispatch(self, request, *arg, **kwargs):
self.object = self.get_object()
if not self.object.election.is_vote_editable:
raise PermissionDenied
return super().dispatch(request, *arg, **kwargs)
@cached_property
def election(self):
return self.get_object().election
def get(self, request, *args, **kwargs):
self.object = self.get_object()
self.form = self.get_form()
return self.render_to_response(self.get_context_data(form=self.form))
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 test_func(self):
if not self.election.is_vote_editable:
return False
user = self.request.user
return user.has_perm("election.change_role") or user.can_edit(self.election)
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
@@ -410,9 +389,7 @@ class ElectionListDeleteView(CanEditMixin, DeleteView):
return reverse("election:detail", kwargs={"election_id": self.election.id})
class ApplyResultFragment(
LoginRequiredMixin, UserPassesTestMixin, FragmentMixin, FormView
):
class ApplyResultFragment(LoginRequiredMixin, UserPassesTestMixin, FormView):
template_name = "election/fragments/apply_result.jinja"
form_class = ApplyElectionResultForm
@@ -429,6 +406,11 @@ class ApplyResultFragment(
id__in=self.request.user.all_groups
).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):
return super().get_form_kwargs() | {"election": self.election}
@@ -437,14 +419,7 @@ class ApplyResultFragment(
return super().form_valid(form)
def get_context_data(self, **kwargs):
return super().get_context_data(**kwargs) | {
"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(),
}
return super().get_context_data(**kwargs) | {"clubs": self.election.clubs.all()}
def get_success_url(self, **kwargs):
return reverse(