On utilise un champ pour stocker l'information de l'option gagnante, et on implémente l'algorithme de schwartz pour dépouiller les votes de condorcet avec schultze

This commit is contained in:
Tom Hubrecht 2021-04-04 23:38:21 +02:00
parent 4d221047c3
commit 4a973d41b1
5 changed files with 102 additions and 39 deletions

View file

@ -0,0 +1,18 @@
# Generated by Django 2.2.19 on 2021-04-04 16:31
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("elections", "0018_auto_20210331_1317"),
]
operations = [
migrations.AddField(
model_name="option",
name="winner",
field=models.BooleanField(default=False, verbose_name="option gagnante"),
),
]

View file

@ -123,6 +123,7 @@ class Option(models.Model):
) )
text = models.TextField(_("texte"), blank=False) text = models.TextField(_("texte"), blank=False)
winner = models.BooleanField(_("option gagnante"), default=False)
voters = models.ManyToManyField( voters = models.ManyToManyField(
settings.AUTH_USER_MODEL, settings.AUTH_USER_MODEL,
related_name="votes", related_name="votes",

View file

@ -135,22 +135,21 @@
{% for o in q.options.all %} {% for o in q.options.all %}
<div class="panel-block"> <div class="panel-block">
{% if election.tallied and election.results_public %} {% if election.tallied and election.results_public %}
<span class="tag {% if o.winner %}is-success{% else %}is-primary{% endif %}">
{% if q.vote_type == "select" %} {% if q.vote_type == "select" %}
<span class="tag {% if o.nb_votes == q.max_votes %}is-success{% else %}is-primary{% endif %}">
<span class="icon"> <span class="icon">
<i class="fas fa-vote-yea"></i> <i class="fas fa-vote-yea"></i>
</span> </span>
<span>{{ o.nb_votes }}</span> <span>{{ o.nb_votes }}</span>
</span>
{% elif q.vote_type == "rank" %} {% elif q.vote_type == "rank" %}
<span class="tag is-primary">
<span class="icon"> <span class="icon">
<i class="fas fa-layer-group"></i> <i class="fas fa-layer-group"></i>
</span> </span>
<span>{{ forloop.counter }}</span> <span>{{ forloop.counter }}</span>
</span>
{% endif %} {% endif %}
</span>
{% endif %} {% endif %}
<span class="ml-2">{{ o.text }}</span> <span class="ml-2">{{ o.text }}</span>

View file

@ -139,32 +139,34 @@
{# Liste des options possibles #} {# Liste des options possibles #}
{% for o in q.options.all %} {% for o in q.options.all %}
<div class="panel-block" id="o_{{ o.pk }}"> <div class="panel-block">
{% if election.start_date > current_time %} {% if election.tallied %}
<span class="tags has-addons mb-0"> <span class="tag {% if o.winner %}is-success{% else %}is-primary{% endif %}">
<a class="tag is-danger mb-0" title="{% trans "Supprimer" %}" href="{% url 'election.del-option' o.pk %}"> {% if q.vote_type == "select" %}
<span class="icon">
<i class="fas fa-times"></i>
</span>
</a>
<a class="tag is-info mb-0" title="{% trans "Modifier" %}" href="{% url 'election.mod-option' o.pk %}">
<span class="icon">
<i class="fas fa-edit"></i>
</span>
</a>
</span>
{% elif election.tallied %}
<span class="tag {% if o.nb_votes == q.max_votes %}is-success{% else %}is-primary{% endif %}">
<span class="icon"> <span class="icon">
<i class="fas fa-vote-yea"></i> <i class="fas fa-vote-yea"></i>
</span> </span>
<span>{{ o.nb_votes }}</span> <span>{{ o.nb_votes }}</span>
{% elif q.vote_type == "rank" %}
<span class="icon">
<i class="fas fa-layer-group"></i>
</span>
<span>{{ forloop.counter }}</span>
{% endif %}
</span> </span>
{% endif %} {% endif %}
<span class="ml-2">{{ o.text }}</span> <span class="ml-2">{{ o.text }}</span>
</div> </div>
{% endfor %} {% endfor %}
{# Affiche plus d'informations sur le résultat #}
{% if election.tallied %}
{{ q.get_results_data }}
{% endif %}
{# Rajout d'une option #} {# Rajout d'une option #}
{% if election.start_date > current_time %} {% if election.start_date > current_time %}
<form action="{% url 'election.add-option' q.pk %}" method="post"> <form action="{% url 'election.add-option' q.pk %}" method="post">

View file

@ -2,7 +2,9 @@ import csv
import io import io
import random import random
import networkx as nx
import numpy as np import numpy as np
from networkx.algorithms.dag import ancestors, descendants
from django.contrib.auth.hashers import make_password from django.contrib.auth.hashers import make_password
from django.core.exceptions import ValidationError from django.core.exceptions import ValidationError
@ -85,14 +87,17 @@ class TallyFunctions:
max_votes = max(max_votes, o.nb_votes) max_votes = max(max_votes, o.nb_votes)
options.append(o) options.append(o)
for o in options:
o.winner = o.nb_votes == max_votes
question.max_votes = max_votes question.max_votes = max_votes
question.save() question.save()
Option.objects.bulk_update(options, ["nb_votes"]) Option.objects.bulk_update(options, ["nb_votes", "winner"])
def tally_rank(question): def tally_rank(question):
"""On dépouille un vote par classement et on crée la matrice des duels""" """On dépouille un vote par classement et on crée la matrice des duels"""
from .models import Duel, Rank from .models import Duel, Option, Rank
def duel(a, b): def duel(a, b):
# Renvoie 1 si a est classé avant b, -1 si c'est l'inverse et # Renvoie 1 si a est classé avant b, -1 si c'est l'inverse et
@ -118,15 +123,23 @@ class TallyFunctions:
votes = ranks_by_user[user] votes = ranks_by_user[user]
ballots.append(np.array([[duel(x, y) for x in votes] for y in votes])) ballots.append(np.array([[duel(x, y) for x in votes] for y in votes]))
# On additionne les classements de tout le monde if ballots != []:
# On additionne les classements de tout le monde pour avoir la matrice
# des duels
duels = np.maximum(sum(ballots), 0) duels = np.maximum(sum(ballots), 0)
# Configuration du graphe
graph = nx.DiGraph()
graph.add_nodes_from(options)
cells = [] cells = []
# On enregistre la matrice # On enregistre la matrice
for i, line in enumerate(duels): for i, line in enumerate(duels):
for j, cell in enumerate(line): for j, cell in enumerate(line):
if cell > 0: if cell > 0:
graph.add_edge(options[j], options[i], weight=cell)
cells.append( cells.append(
Duel( Duel(
question=question, question=question,
@ -137,6 +150,36 @@ class TallyFunctions:
) )
Duel.objects.bulk_create(cells) Duel.objects.bulk_create(cells)
# On utilise la méthode de schwartz pour trouver les options gagnantes
while graph.edges():
losers = set() # Les options qui se font battre strictement
for n in graph.nodes():
losers |= set(descendants(graph, n)) - set(ancestors(graph, n))
if losers:
# On supprime les options perdantes
for n in losers:
graph.remove_node(n)
else:
# On n'a pas d'options perdantes, on supprime les arêtes de poids
# le plus faible
min_weight = min(nx.get_edge_attributes(graph, "weight").values())
min_edges = []
for (u, v) in graph.edges():
if graph[u][v]["weight"] == min_weight:
min_edges.append((u, v))
for (u, v) in min_edges:
graph.remove_edge(u, v)
# Les options gagnantes sont celles encore présentes dans le graphe
winners = graph.nodes()
for o in options:
o.winner = o in winners
Option.objects.bulk_update(options, ["winner"])
class ValidateFunctions: class ValidateFunctions:
"""Classe pour valider les formsets selon le type de question""" """Classe pour valider les formsets selon le type de question"""