On rajoute le vote de condorcet, et les votes par classement en général

This commit is contained in:
Tom Hubrecht 2021-03-29 12:42:34 +02:00
parent 1af7bbe26f
commit 62e7066ce6
7 changed files with 213 additions and 7 deletions

View file

@ -61,7 +61,7 @@ class OptionForm(forms.ModelForm):
widgets = {"text": forms.TextInput}
class VoteForm(forms.ModelForm):
class SelectVoteForm(forms.ModelForm):
def __init__(self, **kwargs):
super().__init__(**kwargs)
# We set the option's text as the label for the checkbox
@ -77,6 +77,27 @@ class VoteForm(forms.ModelForm):
exclude = ["voters"]
OptionFormSet = inlineformset_factory(
Question, Option, extra=0, form=VoteForm, can_delete=False
)
class RankVoteForm(forms.ModelForm):
def __init__(self, **kwargs):
super().__init__(**kwargs)
# We set the option's text as the label for the rank
instance = kwargs.get("instance", None)
if instance is not None:
self.fields["rank"].label = instance.text
rank = forms.IntegerField(required=False)
class Meta:
model = Option
fields = []
exclude = ["voters"]
class BallotFormset:
select_formset = inlineformset_factory(
Question, Option, extra=0, form=SelectVoteForm, can_delete=False
)
rank_formset = inlineformset_factory(
Question, Option, extra=0, form=RankVoteForm, can_delete=False
)

View file

@ -0,0 +1,74 @@
# Generated by Django 2.2.19 on 2021-03-29 08:38
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("elections", "0013_admin_perm"),
]
operations = [
migrations.CreateModel(
name="Rank",
fields=[
(
"id",
models.AutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
(
"rank",
models.PositiveSmallIntegerField(verbose_name="rang de l'option"),
),
(
"vote",
models.OneToOneField(
on_delete=django.db.models.deletion.CASCADE, to="elections.Vote"
),
),
],
),
migrations.CreateModel(
name="Matrix",
fields=[
(
"id",
models.AutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
(
"amount",
models.PositiveSmallIntegerField(
verbose_name="votes supplémentaires"
),
),
(
"loser",
models.OneToOneField(
on_delete=django.db.models.deletion.CASCADE,
related_name="contests_lost",
to="elections.Option",
),
),
(
"winner",
models.OneToOneField(
on_delete=django.db.models.deletion.CASCADE,
related_name="contests_won",
to="elections.Option",
),
),
],
),
]

View file

@ -0,0 +1,27 @@
# Generated by Django 2.2.19 on 2021-03-29 09:09
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("elections", "0014_matrix_rank"),
]
operations = [
migrations.AlterField(
model_name="question",
name="type",
field=models.CharField(
choices=[
("assentiment", "Assentiment"),
("uninominal", "Uninominal"),
("condorcet", "Condorcet"),
],
default="assentiment",
max_length=11,
verbose_name="type de question",
),
),
]

View file

@ -4,6 +4,7 @@ from django.db import models
from django.utils.translation import gettext_lazy as _
from .staticdefs import (
BALLOT_TYPE,
CAST_FUNCTIONS,
CONNECTION_METHODS,
QUESTION_TYPES,
@ -90,6 +91,11 @@ class Question(models.Model):
tally_function = getattr(TallyFunctions, TALLY_FUNCTIONS[self.type])
tally_function(self)
def get_formset(self):
from .forms import BallotFormset # Avoid circular imports
return getattr(BallotFormset, BALLOT_TYPE[self.type])
class Meta:
ordering = ["id"]
@ -118,6 +124,21 @@ class Vote(models.Model):
user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE)
class Rank(models.Model):
vote = models.OneToOneField(Vote, on_delete=models.CASCADE)
rank = models.PositiveSmallIntegerField(_("rang de l'option"))
class Matrix(models.Model):
winner = models.OneToOneField(
Option, related_name="contests_won", on_delete=models.CASCADE
)
loser = models.OneToOneField(
Option, related_name="contests_lost", on_delete=models.CASCADE
)
amount = models.PositiveSmallIntegerField(_("votes supplémentaires"))
# #############################################################################
# Modification of the base User Model
# #############################################################################

View file

@ -21,8 +21,15 @@ CONNECTION_METHODS = {
QUESTION_TYPES = [
("assentiment", _("Assentiment")),
("uninominal", _("Uninominal")),
("condorcet", _("Condorcet")),
]
BALLOT_TYPE = {
"assentiment": "select_formset",
"uninominal": "select_formset",
"condorcet": "rank_formset",
}
VOTE_RULES = {
"assentiment": _(
"Le mode de scrutin pour cette question est un vote par assentiment. "
@ -33,19 +40,29 @@ VOTE_RULES = {
"Le mode de scrutin pour cette question est un vote uninominal. "
"Vous ne pouvez donc sélectionner qu'une seule option."
),
"condorcet": _(
"Le mode de scrutin pour cette question est un vote de type condorcet. "
"Vous devez classer les options entre 1 et le nombre d'options, l'option "
"classée 1 étant votre préférée. Vous pouvez donner le même classement "
"à plusieurs options, si vous laissez vide le classement d'une option, "
"elle sera classée dernière automatiquement."
),
}
CAST_FUNCTIONS = {
"assentiment": "cast_select",
"uninominal": "cast_select",
"condorcet": "cast_rank",
}
TALLY_FUNCTIONS = {
"assentiment": "tally_select",
"uninominal": "tally_select",
"condorcet": "tally_rank",
}
VALIDATE_FUNCTIONS = {
"assentiment": "always_true",
"uninominal": "unique_selected",
"condorcet": "limit_ranks",
}

View file

@ -41,6 +41,31 @@ class CastFunctions:
user.votes.add(*selected)
user.votes.remove(*n_selected)
def cast_rank(user, vote_form):
"""On enregistre un vote par classement"""
from .models import Rank, Vote
# On enregistre les votes pour pouvoir créér les classements
options = [v.instance for v in vote_form]
user.votes.add(*options)
votes = {
v.option: v for v in Vote.objects.filter(user=user, option__in=options)
}
ranks_create = []
ranks_update = []
for v in vote_form:
vote = votes[v.instance]
if hasattr(vote, "rank"):
vote.rank.rank = v.cleaned_data["rank"]
ranks_update.append(vote.rank)
else:
ranks_create.append(Rank(vote=vote, rank=v.cleaned_data["rank"]))
Rank.objects.bulk_update(ranks_update, ["rank"])
Rank.objects.bulk_create(ranks_create)
class TallyFunctions:
"""Classe pour gérer les dépouillements"""
@ -88,6 +113,28 @@ class ValidateFunctions:
return False
return True
def limit_ranks(vote_form):
"""Limite le classement au nombre d'options"""
nb_options = len(vote_form)
valid = True
for v in vote_form:
rank = v.cleaned_data["rank"]
if rank is None:
rank = nb_options
if rank > nb_options:
v.add_error(
"rank", _("Le classement maximal est {}.").format(nb_options)
)
valid = False
elif rank < 1:
v.add_error("rank", _("Le classement minimal est 1."))
valid = False
v.cleaned_data["rank"] = rank # On ajoute le défaut
return valid
# #############################################################################
# Fonctions pour importer une liste de votant·e·s

View file

@ -19,7 +19,6 @@ from django.views.generic import (
from .forms import (
ElectionForm,
OptionForm,
OptionFormSet,
QuestionForm,
UploadVotersForm,
VoterMailForm,
@ -403,13 +402,13 @@ class VoteView(OpenElectionOnlyMixin, DetailView):
def get(self, request, *args, **kwargs):
self.object = self.get_object()
vote_form = OptionFormSet(instance=self.object)
vote_form = self.object.get_formset()(instance=self.object)
return self.render_to_response(self.get_context_data(formset=vote_form))
def post(self, request, *args, **kwargs):
self.object = self.get_object()
vote_form = OptionFormSet(self.request.POST, instance=self.object)
vote_form = self.object.get_formset()(self.request.POST, instance=self.object)
if self.object.is_form_valid(vote_form):
# On enregistre le vote