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 from django.core.validators import validate_email from django.utils.translation import gettext_lazy as _ # ############################################################################# # Fonctions universelles # ############################################################################# def choices_length(choices): """Renvoie la longueur maximale des choix de choices""" m = 0 for c in choices: m = max(m, len(c[0])) return m # ############################################################################# # Classes pour différencier les différents types de questions # ############################################################################# class CastFunctions: """Classe pour enregistrer les votes""" def cast_select(user, vote_form): """On enregistre un vote classique""" selected, n_selected = [], [] for v in vote_form: if v.cleaned_data["selected"]: selected.append(v.instance) else: n_selected.append(v.instance) 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""" def tally_select(question): """On dépouille un vote classique""" from .models import Option max_votes = 0 options = [] for o in question.options.prefetch_related("voters").all(): o.nb_votes = o.voters.count() max_votes = max(max_votes, o.nb_votes) options.append(o) question.max_votes = max_votes question.save() 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""" def always_true(vote_form): """Retourne True pour les votes sans validation particulière""" return True def unique_selected(vote_form): """Vérifie qu'une seule option est choisie""" nb_selected = 0 for v in vote_form: nb_selected += v.cleaned_data["selected"] if nb_selected == 0: vote_form._non_form_errors.append( ValidationError(_("Vous devez sélectionnner une option.")) ) return False elif nb_selected > 1: vote_form._non_form_errors.append( ValidationError(_("Vous ne pouvez pas sélectionner plus d'une option.")) ) 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 # ############################################################################# def create_users(election, csv_file): """Crée les votant·e·s pour l'élection donnée, en remplissant les champs `username`, `election` et `full_name`. """ dialect = csv.Sniffer().sniff(csv_file.readline().decode("utf-8")) csv_file.seek(0) reader = csv.reader(io.StringIO(csv_file.read().decode("utf-8")), dialect) for (username, full_name, email) in reader: election.registered_voters.create( username=f"{election.id}__{username}", email=email, full_name=full_name ) def check_csv(csv_file): """Vérifie que le fichier donnant la liste de votant·e·s est bien formé""" try: dialect = csv.Sniffer().sniff(csv_file.readline().decode("utf-8")) except csv.Error: return [ _( "Format invalide. Vérifiez que le fichier est bien formé (i.e. " "chaque ligne de la forme 'login,nom,email')." ) ] csv_file.seek(0) reader = csv.reader(io.StringIO(csv_file.read().decode("utf-8")), dialect) errors = [] users = {} line_nb = 0 for line in reader: line_nb += 1 if len(line) != 3: errors.append( _("La ligne {} n'a pas le bon nombre d'éléments.").format(line_nb) ) else: if line[0] == "": errors.append( _("Valeur manquante dans la ligne {} : 'login'.").format(line_nb) ) else: if line[0] in users: errors.append( _("Doublon dans les logins : lignes {} et {}.").format( line_nb, users[line[0]] ) ) else: users[line[0]] = line_nb if line[1] == "": errors.append( _("Valeur manquante dans la ligne {} : 'nom'.").format(line_nb) ) try: validate_email(line[2]) except ValidationError: errors.append( _("Adresse mail invalide à la ligne {} : '{}'.").format( line_nb, line[2] ) ) return errors def generate_password(): random.seed() alphabet = "abcdefghjkmnopqrstuvwxyzABCDEFGHJKLMNPQRSTUVWXYZ23456789" password = "" for i in range(15): password += random.choice(alphabet) return password def send_mail(election, mail_form): """Envoie le mail d'annonce de l'élection avec identifiants et mot de passe aux votant·e·s, le mdp est généré en même temps que le mail est envoyé. """ from .models import User voters = list(election.registered_voters.all()) url = f"https://kadenios.eleves.ens.fr/elections/view/{election.id}" messages = [] for v in voters: password = generate_password() v.password = make_password(password) messages.append( EmailMessage( subject=mail_form.cleaned_data["objet"], body=mail_form.cleaned_data["message"].format( full_name=v.full_name, election_url=url, username=v.get_username(), password=password, ), to=[v.email], ) ) get_connection(fail_silently=False).send_messages(messages) User.objects.bulk_update(voters, ["password"])