mirror of
https://github.com/ae-utbm/sith.git
synced 2026-06-05 15:49:21 +00:00
268 lines
9.4 KiB
Python
268 lines
9.4 KiB
Python
from datetime import timedelta
|
|
from itertools import groupby, islice
|
|
from operator import attrgetter
|
|
|
|
from django import forms
|
|
from django.conf import settings
|
|
from django.db import transaction
|
|
from django.db.models import Count
|
|
from django.forms.models import ModelChoiceIterator, ModelChoiceIteratorValue
|
|
from django.utils.timezone import localdate, localtime
|
|
from django.utils.translation import gettext_lazy as _
|
|
|
|
from club.forms import ClubRoleChoiceField
|
|
from club.models import ClubRole, Membership
|
|
from club.widgets.ajax_select import AutoCompleteSelectMultipleClub
|
|
from core.models import User
|
|
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
|
|
|
|
|
|
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 many candidates."), code="invalid"
|
|
)
|
|
|
|
|
|
class CandidateForm(forms.ModelForm):
|
|
"""Form to candidate."""
|
|
|
|
required_css_class = "required"
|
|
|
|
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, election: Election, can_edit: bool = False, **kwargs):
|
|
super().__init__(*args, **kwargs)
|
|
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()
|
|
|
|
|
|
class VoteForm(forms.Form):
|
|
def __init__(self, election: Election, user: User, *args, **kwargs):
|
|
super().__init__(*args, **kwargs)
|
|
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"),
|
|
blank=True,
|
|
)
|
|
|
|
|
|
class RoleForm(forms.ModelForm):
|
|
"""Form for creating a role."""
|
|
|
|
required_css_class = "required"
|
|
error_css_class = "error"
|
|
|
|
class Meta:
|
|
model = Role
|
|
fields = ["club_role", "title", "description", "max_choice"]
|
|
field_classes = {"club_role": ClubRoleChoiceField}
|
|
|
|
def __init__(self, *args, election: Election, **kwargs):
|
|
super().__init__(*args, **kwargs)
|
|
self.instance.election = election
|
|
self.fields["club_role"].queryset = ClubRole.objects.filter(
|
|
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:
|
|
model = ElectionList
|
|
fields = ("title", "election")
|
|
widgets = {"election": AutoCompleteSelect}
|
|
|
|
def __init__(self, *args, election: Election, **kwargs):
|
|
super().__init__(*args, **kwargs)
|
|
self.instance.election = election
|
|
|
|
|
|
class ElectionForm(forms.ModelForm):
|
|
required_css_class = "required"
|
|
error_css_class = "error"
|
|
|
|
class Meta:
|
|
model = Election
|
|
fields = [
|
|
"title",
|
|
"description",
|
|
"clubs",
|
|
"archived",
|
|
"start_candidature",
|
|
"end_candidature",
|
|
"start_date",
|
|
"end_date",
|
|
"edit_groups",
|
|
"view_groups",
|
|
"vote_groups",
|
|
"candidature_groups",
|
|
]
|
|
widgets = {
|
|
"clubs": AutoCompleteSelectMultipleClub,
|
|
"edit_groups": AutoCompleteSelectMultipleGroup,
|
|
"view_groups": AutoCompleteSelectMultipleGroup,
|
|
"vote_groups": AutoCompleteSelectMultipleGroup,
|
|
"candidature_groups": AutoCompleteSelectMultipleGroup,
|
|
"start_date": SelectDateTime,
|
|
"end_date": SelectDateTime,
|
|
"start_candidature": SelectDateTime,
|
|
"end_candidature": SelectDateTime,
|
|
}
|
|
|
|
|
|
class ElectionCreateForm(ElectionForm):
|
|
"""ElectionForm, but specifically for creation."""
|
|
|
|
def __init__(self, *args, initial: dict | None = None, **kwargs):
|
|
# propose sound default timestamps :
|
|
# start of candidatures at tomorrow 00h01, start of votes a week later.
|
|
start = localtime().replace(hour=0, minute=1, second=0) + timedelta(days=1)
|
|
default_initial = {
|
|
"start_candidature": start,
|
|
"end_candidature": start + timedelta(days=7, minutes=-2), # 23h59
|
|
"start_date": start + timedelta(days=7), # 00h01
|
|
"end_date": start + timedelta(days=14, minutes=-2), # 23h59
|
|
"view_groups": [settings.SITH_GROUP_PUBLIC_ID],
|
|
"vote_groups": [settings.SITH_GROUP_SUBSCRIBERS_ID],
|
|
"candidature_groups": [settings.SITH_GROUP_SUBSCRIBERS_ID],
|
|
}
|
|
if initial:
|
|
default_initial.update(initial)
|
|
super().__init__(*args, initial=default_initial, **kwargs)
|
|
|
|
def save(self, commit=True): # noqa: FBT002
|
|
instance = super().save(commit=commit)
|
|
if commit:
|
|
ElectionList.objects.create(title="Candidat⸱e libre", election=instance)
|
|
return instance
|
|
|
|
|
|
class ElectionWinnerChoiceIterator(ModelChoiceIterator):
|
|
"""Iterate over the candidates that gathered enough votes"""
|
|
|
|
def __iter__(self):
|
|
# for each role, yield only the N first candidates,
|
|
# where N is the election role max_choice
|
|
qs = (
|
|
self.queryset.annotate(nb_votes=Count("votes"))
|
|
.order_by("role__order", "-nb_votes")
|
|
.select_related("role", "user", "role__club_role", "role__club_role__club")
|
|
)
|
|
yield from (
|
|
(
|
|
f"{role.title} \u2013 {role.club_role.club.name}",
|
|
[self.choice(cand) for cand in islice(candidates, role.max_choice)],
|
|
)
|
|
for role, candidates in groupby(qs, key=attrgetter("role"))
|
|
)
|
|
|
|
def choice(self, obj: Candidature):
|
|
return (
|
|
ModelChoiceIteratorValue(self.field.prepare_value(obj), obj),
|
|
obj.user.get_full_name(),
|
|
)
|
|
|
|
|
|
class ElectionWinnerChoiceField(forms.ModelMultipleChoiceField):
|
|
"""Custom `ModelChoiceField` for `[ClubRole][club.models.ClubRole]`.
|
|
|
|
If only one club is involved, behave like the base `ModelChoiceField`.
|
|
If dealing with the roles of multiple clubs, group the roles
|
|
into a different `optgroup` for each club.
|
|
"""
|
|
|
|
iterator = ElectionWinnerChoiceIterator
|
|
widget = forms.CheckboxSelectMultiple
|
|
|
|
|
|
class ApplyElectionResultForm(forms.Form):
|
|
"""Form to select winners of an election, and automatically apply the results."""
|
|
|
|
candidates = ElectionWinnerChoiceField(Candidature.objects.none())
|
|
|
|
def __init__(self, *args, election: Election, **kwargs):
|
|
self.election = election
|
|
super().__init__(*args, **kwargs)
|
|
qs = Candidature.objects.filter(
|
|
role__election=election, role__club_role__isnull=False
|
|
)
|
|
# pass all candidates to the ModelChoiceField ;
|
|
# its inner choice iterator will take care of filtering only the winners.
|
|
self.fields["candidates"].queryset = qs
|
|
# By default, mark every candidate as selected.
|
|
# Election results are usually completely validated during the AG,
|
|
# so it makes more sense UX-wise to eventually unselect a candidate
|
|
# than to select everyone.
|
|
self.fields["candidates"].initial = qs.values_list("id", flat=True)
|
|
|
|
def save(self):
|
|
if self.errors:
|
|
return
|
|
candidates: list[Candidature] = list(self.cleaned_data["candidates"])
|
|
with transaction.atomic():
|
|
Membership.objects.filter(
|
|
role__in=[c.role.club_role for c in candidates],
|
|
end_date=None,
|
|
start_date__lt=self.election.end_date,
|
|
).update(end_date=localdate())
|
|
memberships = [
|
|
Membership(
|
|
user_id=c.user_id,
|
|
club_id=c.role.club_role.club_id,
|
|
role=c.role.club_role,
|
|
)
|
|
for c in candidates
|
|
]
|
|
Membership.objects.bulk_create(memberships)
|
|
Membership._add_club_groups(memberships)
|