From fea1ab495d642256c2675487ff9d2c0105b56246 Mon Sep 17 00:00:00 2001 From: Tom Hubrecht Date: Mon, 29 Mar 2021 18:04:54 +0200 Subject: [PATCH] =?UTF-8?q?D=C3=A9pouillement=20des=20questions=20de=20typ?= =?UTF-8?q?e=20condorcet?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../migrations/0016_auto_20210329_1759.py | 69 +++++++++++++++++++ .../migrations/0017_auto_20210329_1802.py | 32 +++++++++ elections/models.py | 15 +++- elections/utils.py | 49 +++++++++++++ 4 files changed, 162 insertions(+), 3 deletions(-) create mode 100644 elections/migrations/0016_auto_20210329_1759.py create mode 100644 elections/migrations/0017_auto_20210329_1802.py diff --git a/elections/migrations/0016_auto_20210329_1759.py b/elections/migrations/0016_auto_20210329_1759.py new file mode 100644 index 0000000..5b9ce64 --- /dev/null +++ b/elections/migrations/0016_auto_20210329_1759.py @@ -0,0 +1,69 @@ +# Generated by Django 2.2.19 on 2021-03-29 15:59 + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("elections", "0015_condorcet"), + ] + + operations = [ + migrations.CreateModel( + name="Duel", + 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", + ), + ), + ( + "question", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="duels", + to="elections.Question", + ), + ), + ( + "winner", + models.OneToOneField( + on_delete=django.db.models.deletion.CASCADE, + related_name="contests_won", + to="elections.Option", + ), + ), + ], + ), + migrations.AlterModelOptions( + name="rank", + options={"ordering": ["vote"]}, + ), + migrations.AlterModelOptions( + name="vote", + options={"ordering": ["option"]}, + ), + migrations.DeleteModel( + name="Matrix", + ), + ] diff --git a/elections/migrations/0017_auto_20210329_1802.py b/elections/migrations/0017_auto_20210329_1802.py new file mode 100644 index 0000000..e28d94a --- /dev/null +++ b/elections/migrations/0017_auto_20210329_1802.py @@ -0,0 +1,32 @@ +# Generated by Django 2.2.19 on 2021-03-29 16:02 + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("elections", "0016_auto_20210329_1759"), + ] + + operations = [ + migrations.AlterField( + model_name="duel", + name="loser", + field=models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="contests_lost", + to="elections.Option", + ), + ), + migrations.AlterField( + model_name="duel", + name="winner", + field=models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="contests_won", + to="elections.Option", + ), + ), + ] diff --git a/elections/models.py b/elections/models.py index 8e0f45f..4419eaa 100644 --- a/elections/models.py +++ b/elections/models.py @@ -123,17 +123,26 @@ class Vote(models.Model): option = models.ForeignKey(Option, on_delete=models.CASCADE) user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE) + class Meta: + ordering = ["option"] + class Rank(models.Model): vote = models.OneToOneField(Vote, on_delete=models.CASCADE) rank = models.PositiveSmallIntegerField(_("rang de l'option")) + class Meta: + ordering = ["vote"] -class Matrix(models.Model): - winner = models.OneToOneField( + +class Duel(models.Model): + question = models.ForeignKey( + Question, related_name="duels", on_delete=models.CASCADE + ) + winner = models.ForeignKey( Option, related_name="contests_won", on_delete=models.CASCADE ) - loser = models.OneToOneField( + loser = models.ForeignKey( Option, related_name="contests_lost", on_delete=models.CASCADE ) amount = models.PositiveSmallIntegerField(_("votes supplémentaires")) diff --git a/elections/utils.py b/elections/utils.py index 6e3e9f4..1c8ad60 100644 --- a/elections/utils.py +++ b/elections/utils.py @@ -2,6 +2,8 @@ import csv import io import random +import numpy as np + from django.contrib.auth.hashers import make_password from django.core.exceptions import ValidationError from django.core.mail import EmailMessage, get_connection @@ -87,6 +89,53 @@ class TallyFunctions: Option.objects.bulk_update(options, ["nb_votes"]) + def tally_rank(question): + """On dépouille un vote par classement et on crée la matrice des duels""" + from .models import Duel, Rank + + def duel(a, b): + # Renvoie 1 si a est classé avant b, -1 si c'est l'inverse et + # 0 si a et b ont le même rang, ce qui permet d'avoir directement + # le graphe des duels en faisant le max avec la matrice nulle + return (a.rank < b.rank) - (a.rank > b.rank) + + options = list(question.options.all()) + ranks = Rank.objects.select_related("vote").filter(vote__option__in=options) + ranks_by_user = {} + + for r in ranks: + user = r.vote.user + if user in ranks_by_user: + ranks_by_user[user].append(r) + else: + ranks_by_user[user] = [r] + + ballots = [] + + # Pour chaque votant·e, on regarde son classement + for user in ranks_by_user: + votes = ranks_by_user[user] + ballots.append(np.array([[duel(x, y) for x in votes] for y in votes])) + + # On additionne les classements de tout le monde + duels = np.maximum(sum(ballots), 0) + + cells = [] + + # On enregistre la matrice + for i, line in enumerate(duels): + for j, cell in enumerate(line): + if cell > 0: + cells.append( + Duel( + question=question, + winner=options[j], + loser=options[i], + amount=cell, + ) + ) + Duel.objects.bulk_create(cells) + class ValidateFunctions: """Classe pour valider les formsets selon le type de question"""