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}
|
widgets = {"text": forms.TextInput}
|
||||||
|
|
||||||
|
|
||||||
class VoteForm(forms.ModelForm):
|
class SelectVoteForm(forms.ModelForm):
|
||||||
def __init__(self, **kwargs):
|
def __init__(self, **kwargs):
|
||||||
super().__init__(**kwargs)
|
super().__init__(**kwargs)
|
||||||
# We set the option's text as the label for the checkbox
|
# We set the option's text as the label for the checkbox
|
||||||
|
@ -77,6 +77,27 @@ class VoteForm(forms.ModelForm):
|
||||||
exclude = ["voters"]
|
exclude = ["voters"]
|
||||||
|
|
||||||
|
|
||||||
OptionFormSet = inlineformset_factory(
|
class RankVoteForm(forms.ModelForm):
|
||||||
Question, Option, extra=0, form=VoteForm, can_delete=False
|
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 django.utils.translation import gettext_lazy as _
|
||||||
|
|
||||||
from .staticdefs import (
|
from .staticdefs import (
|
||||||
|
BALLOT_TYPE,
|
||||||
CAST_FUNCTIONS,
|
CAST_FUNCTIONS,
|
||||||
CONNECTION_METHODS,
|
CONNECTION_METHODS,
|
||||||
QUESTION_TYPES,
|
QUESTION_TYPES,
|
||||||
|
@ -90,6 +91,11 @@ class Question(models.Model):
|
||||||
tally_function = getattr(TallyFunctions, TALLY_FUNCTIONS[self.type])
|
tally_function = getattr(TallyFunctions, TALLY_FUNCTIONS[self.type])
|
||||||
tally_function(self)
|
tally_function(self)
|
||||||
|
|
||||||
|
def get_formset(self):
|
||||||
|
from .forms import BallotFormset # Avoid circular imports
|
||||||
|
|
||||||
|
return getattr(BallotFormset, BALLOT_TYPE[self.type])
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
ordering = ["id"]
|
ordering = ["id"]
|
||||||
|
|
||||||
|
@ -118,6 +124,21 @@ class Vote(models.Model):
|
||||||
user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE)
|
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
|
# Modification of the base User Model
|
||||||
# #############################################################################
|
# #############################################################################
|
||||||
|
|
|
@ -21,8 +21,15 @@ CONNECTION_METHODS = {
|
||||||
QUESTION_TYPES = [
|
QUESTION_TYPES = [
|
||||||
("assentiment", _("Assentiment")),
|
("assentiment", _("Assentiment")),
|
||||||
("uninominal", _("Uninominal")),
|
("uninominal", _("Uninominal")),
|
||||||
|
("condorcet", _("Condorcet")),
|
||||||
]
|
]
|
||||||
|
|
||||||
|
BALLOT_TYPE = {
|
||||||
|
"assentiment": "select_formset",
|
||||||
|
"uninominal": "select_formset",
|
||||||
|
"condorcet": "rank_formset",
|
||||||
|
}
|
||||||
|
|
||||||
VOTE_RULES = {
|
VOTE_RULES = {
|
||||||
"assentiment": _(
|
"assentiment": _(
|
||||||
"Le mode de scrutin pour cette question est un vote par 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. "
|
"Le mode de scrutin pour cette question est un vote uninominal. "
|
||||||
"Vous ne pouvez donc sélectionner qu'une seule option."
|
"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 = {
|
CAST_FUNCTIONS = {
|
||||||
"assentiment": "cast_select",
|
"assentiment": "cast_select",
|
||||||
"uninominal": "cast_select",
|
"uninominal": "cast_select",
|
||||||
|
"condorcet": "cast_rank",
|
||||||
}
|
}
|
||||||
|
|
||||||
TALLY_FUNCTIONS = {
|
TALLY_FUNCTIONS = {
|
||||||
"assentiment": "tally_select",
|
"assentiment": "tally_select",
|
||||||
"uninominal": "tally_select",
|
"uninominal": "tally_select",
|
||||||
|
"condorcet": "tally_rank",
|
||||||
}
|
}
|
||||||
|
|
||||||
VALIDATE_FUNCTIONS = {
|
VALIDATE_FUNCTIONS = {
|
||||||
"assentiment": "always_true",
|
"assentiment": "always_true",
|
||||||
"uninominal": "unique_selected",
|
"uninominal": "unique_selected",
|
||||||
|
"condorcet": "limit_ranks",
|
||||||
}
|
}
|
||||||
|
|
|
@ -41,6 +41,31 @@ class CastFunctions:
|
||||||
user.votes.add(*selected)
|
user.votes.add(*selected)
|
||||||
user.votes.remove(*n_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:
|
class TallyFunctions:
|
||||||
"""Classe pour gérer les dépouillements"""
|
"""Classe pour gérer les dépouillements"""
|
||||||
|
@ -88,6 +113,28 @@ class ValidateFunctions:
|
||||||
return False
|
return False
|
||||||
return True
|
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
|
# Fonctions pour importer une liste de votant·e·s
|
||||||
|
|
|
@ -19,7 +19,6 @@ from django.views.generic import (
|
||||||
from .forms import (
|
from .forms import (
|
||||||
ElectionForm,
|
ElectionForm,
|
||||||
OptionForm,
|
OptionForm,
|
||||||
OptionFormSet,
|
|
||||||
QuestionForm,
|
QuestionForm,
|
||||||
UploadVotersForm,
|
UploadVotersForm,
|
||||||
VoterMailForm,
|
VoterMailForm,
|
||||||
|
@ -403,13 +402,13 @@ class VoteView(OpenElectionOnlyMixin, DetailView):
|
||||||
|
|
||||||
def get(self, request, *args, **kwargs):
|
def get(self, request, *args, **kwargs):
|
||||||
self.object = self.get_object()
|
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))
|
return self.render_to_response(self.get_context_data(formset=vote_form))
|
||||||
|
|
||||||
def post(self, request, *args, **kwargs):
|
def post(self, request, *args, **kwargs):
|
||||||
self.object = self.get_object()
|
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):
|
if self.object.is_form_valid(vote_form):
|
||||||
# On enregistre le vote
|
# On enregistre le vote
|
||||||
|
|
Loading…
Add table
Reference in a new issue