On rajoute le vote de condorcet, et les votes par classement en général
This commit is contained in:
parent
1af7bbe26f
commit
62e7066ce6
7 changed files with 213 additions and 7 deletions
|
@ -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
|
||||
)
|
||||
|
|
74
elections/migrations/0014_matrix_rank.py
Normal file
74
elections/migrations/0014_matrix_rank.py
Normal 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",
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
]
|
27
elections/migrations/0015_condorcet.py
Normal file
27
elections/migrations/0015_condorcet.py
Normal 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",
|
||||
),
|
||||
),
|
||||
]
|
|
@ -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
|
||||
# #############################################################################
|
||||
|
|
|
@ -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",
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in a new issue