diff --git a/elections/migrations/0019_option_winner.py b/elections/migrations/0019_option_winner.py new file mode 100644 index 0000000..b0141e9 --- /dev/null +++ b/elections/migrations/0019_option_winner.py @@ -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"), + ), + ] diff --git a/elections/models.py b/elections/models.py index 47cfe7d..f7a6671 100644 --- a/elections/models.py +++ b/elections/models.py @@ -123,6 +123,7 @@ class Option(models.Model): ) text = models.TextField(_("texte"), blank=False) + winner = models.BooleanField(_("option gagnante"), default=False) voters = models.ManyToManyField( settings.AUTH_USER_MODEL, related_name="votes", diff --git a/elections/templates/elections/election.html b/elections/templates/elections/election.html index 59c3e26..3c1ef36 100644 --- a/elections/templates/elections/election.html +++ b/elections/templates/elections/election.html @@ -135,23 +135,22 @@ {% for o in q.options.all %}
{% if election.tallied and election.results_public %} - {% if q.vote_type == "select" %} - + + {% if q.vote_type == "select" %} {{ o.nb_votes }} - - {% elif q.vote_type == "rank" %} - + {% elif q.vote_type == "rank" %} + {{ forloop.counter }} + {% endif %} {% endif %} - {% endif %} {{ o.text }}
diff --git a/elections/templates/elections/election_admin.html b/elections/templates/elections/election_admin.html index e4458cb..bc49fe5 100644 --- a/elections/templates/elections/election_admin.html +++ b/elections/templates/elections/election_admin.html @@ -139,32 +139,34 @@ {# Liste des options possibles #} {% for o in q.options.all %} -
- {% if election.start_date > current_time %} - - - - - - - - - - - - - {% elif election.tallied %} - +
+ {% if election.tallied %} + + {% if q.vote_type == "select" %} {{ o.nb_votes }} + + {% elif q.vote_type == "rank" %} + + + + + {{ forloop.counter }} + {% endif %} {% endif %} + {{ o.text }}
{% endfor %} + {# Affiche plus d'informations sur le résultat #} + {% if election.tallied %} + {{ q.get_results_data }} + {% endif %} + {# Rajout d'une option #} {% if election.start_date > current_time %}
diff --git a/elections/utils.py b/elections/utils.py index 56b05c1..52208c8 100644 --- a/elections/utils.py +++ b/elections/utils.py @@ -2,7 +2,9 @@ import csv import io import random +import networkx as nx import numpy as np +from networkx.algorithms.dag import ancestors, descendants from django.contrib.auth.hashers import make_password from django.core.exceptions import ValidationError @@ -85,14 +87,17 @@ class TallyFunctions: max_votes = max(max_votes, o.nb_votes) options.append(o) + for o in options: + o.winner = o.nb_votes == max_votes + question.max_votes = max_votes question.save() - Option.objects.bulk_update(options, ["nb_votes"]) + Option.objects.bulk_update(options, ["nb_votes", "winner"]) def tally_rank(question): """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): # Renvoie 1 si a est classé avant b, -1 si c'est l'inverse et @@ -118,24 +123,62 @@ class TallyFunctions: 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) + if ballots != []: + # On additionne les classements de tout le monde pour avoir la matrice + # des duels + duels = np.maximum(sum(ballots), 0) - cells = [] + # Configuration du graphe + graph = nx.DiGraph() - # 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, + graph.add_nodes_from(options) + + cells = [] + + # On enregistre la matrice + for i, line in enumerate(duels): + for j, cell in enumerate(line): + if cell > 0: + graph.add_edge(options[j], options[i], weight=cell) + cells.append( + Duel( + question=question, + winner=options[j], + loser=options[i], + amount=cell, + ) ) - ) - 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: