261 lines
8 KiB
Python
261 lines
8 KiB
Python
from translated_fields import TranslatedFieldWithFallback
|
|
|
|
from django.conf import settings
|
|
from django.contrib.auth.models import AbstractUser
|
|
from django.db import models, transaction
|
|
from django.template.loader import render_to_string
|
|
from django.utils.translation import gettext_lazy as _
|
|
|
|
from shared.auth import CONNECTION_METHODS
|
|
from shared.utils import choices_length
|
|
|
|
from .staticdefs import (
|
|
BALLOT_TYPE,
|
|
CAST_FUNCTIONS,
|
|
QUESTION_TYPES,
|
|
TALLY_FUNCTIONS,
|
|
VALIDATE_FUNCTIONS,
|
|
)
|
|
from .utils import (
|
|
BallotsData,
|
|
CastFunctions,
|
|
ResultsData,
|
|
TallyFunctions,
|
|
ValidateFunctions,
|
|
)
|
|
|
|
# #############################################################################
|
|
# Models regarding an election
|
|
# #############################################################################
|
|
|
|
|
|
class Election(models.Model):
|
|
name = TranslatedFieldWithFallback(models.CharField(_("nom"), max_length=255))
|
|
short_name = models.SlugField(_("nom bref"), unique=True)
|
|
description = TranslatedFieldWithFallback(
|
|
models.TextField(_("description"), blank=True)
|
|
)
|
|
|
|
start_date = models.DateTimeField(_("date et heure de début"))
|
|
end_date = models.DateTimeField(_("date et heure de fin"))
|
|
|
|
visible = models.BooleanField(_("visible au public"), default=False)
|
|
|
|
vote_restrictions = TranslatedFieldWithFallback(
|
|
models.TextField(_("conditions de vote"), blank=True)
|
|
)
|
|
|
|
restricted = models.BooleanField(
|
|
_("restreint le vote à une liste de personnes"), default=True
|
|
)
|
|
|
|
sent_mail = models.BooleanField(
|
|
_("mail avec les identifiants envoyé"), null=True, default=False
|
|
)
|
|
|
|
created_by = models.ForeignKey(
|
|
settings.AUTH_USER_MODEL,
|
|
related_name="elections_created",
|
|
on_delete=models.SET_NULL,
|
|
blank=True,
|
|
null=True,
|
|
)
|
|
|
|
voters = models.ManyToManyField(
|
|
settings.AUTH_USER_MODEL,
|
|
related_name="cast_elections",
|
|
blank=True,
|
|
)
|
|
|
|
results_public = models.BooleanField(_("résultats publics"), default=False)
|
|
tallied = models.BooleanField(_("dépouillée"), default=False)
|
|
|
|
archived = models.BooleanField(_("archivée"), default=False)
|
|
|
|
time_tallied = models.DateTimeField(
|
|
_("date du dépouillement"), null=True, default=None
|
|
)
|
|
time_published = models.DateTimeField(
|
|
_("date de publication"), null=True, default=None
|
|
)
|
|
|
|
class Meta:
|
|
permissions = [
|
|
("election_admin", _("Peut administrer des élections")),
|
|
]
|
|
ordering = ["-start_date", "-end_date"]
|
|
|
|
|
|
class Question(models.Model):
|
|
election = models.ForeignKey(
|
|
Election, related_name="questions", on_delete=models.CASCADE
|
|
)
|
|
text = TranslatedFieldWithFallback(
|
|
models.TextField(_("question"), blank=True, default="")
|
|
)
|
|
type = models.CharField(
|
|
_("type de question"),
|
|
choices=QUESTION_TYPES,
|
|
default="assentiment",
|
|
max_length=choices_length(QUESTION_TYPES),
|
|
)
|
|
# We cache the maximum number of votes for an option
|
|
max_votes = models.PositiveSmallIntegerField(
|
|
_("nombre maximal de votes reçus"), default=0
|
|
)
|
|
|
|
voters = models.ManyToManyField(
|
|
settings.AUTH_USER_MODEL,
|
|
related_name="cast_questions",
|
|
blank=True,
|
|
)
|
|
|
|
def is_form_valid(self, vote_form):
|
|
validate_function = getattr(ValidateFunctions, VALIDATE_FUNCTIONS[self.type])
|
|
return vote_form.is_valid() and validate_function(vote_form)
|
|
|
|
@transaction.atomic
|
|
def cast_ballot(self, user, vote_form):
|
|
cast_function = getattr(CastFunctions, CAST_FUNCTIONS[self.type])
|
|
cast_function(user, vote_form)
|
|
|
|
@transaction.atomic
|
|
def tally(self):
|
|
tally_function = getattr(TallyFunctions, TALLY_FUNCTIONS[self.type])
|
|
tally_function(self)
|
|
|
|
@property
|
|
def results(self):
|
|
return render_to_string(
|
|
f"elections/results/{self.vote_type}_export.txt", {"question": self}
|
|
)
|
|
|
|
def get_formset(self):
|
|
from .forms import BallotFormset # Avoid circular imports
|
|
|
|
return getattr(BallotFormset, BALLOT_TYPE[self.type])
|
|
|
|
def get_results_data(self):
|
|
results_function = getattr(ResultsData, BALLOT_TYPE[self.type])
|
|
return results_function(self)
|
|
|
|
def display_ballots(self):
|
|
display_function = getattr(BallotsData, BALLOT_TYPE[self.type])
|
|
return display_function(self)
|
|
|
|
@property
|
|
def vote_type(self):
|
|
return BALLOT_TYPE[self.type]
|
|
|
|
def __str__(self):
|
|
return self.text
|
|
|
|
class Meta:
|
|
ordering = ["id"]
|
|
|
|
|
|
class Option(models.Model):
|
|
question = models.ForeignKey(
|
|
Question, related_name="options", on_delete=models.CASCADE
|
|
)
|
|
text = TranslatedFieldWithFallback(models.TextField(_("texte"), blank=False))
|
|
abbreviation = models.CharField(_("abréviation"), max_length=3, blank=True)
|
|
|
|
winner = models.BooleanField(_("option gagnante"), default=False)
|
|
voters = models.ManyToManyField(
|
|
settings.AUTH_USER_MODEL,
|
|
related_name="votes",
|
|
through="Vote",
|
|
blank=True,
|
|
)
|
|
# For now, we store the amount of votes received after the election is tallied
|
|
nb_votes = models.PositiveSmallIntegerField(_("nombre de votes reçus"), default=0)
|
|
|
|
def save(self, *args, **kwargs):
|
|
# On enlève les espaces et on passe tout en majuscules
|
|
self.abbreviation = "".join(self.abbreviation.upper().split())
|
|
|
|
super().save(*args, **kwargs)
|
|
|
|
def get_abbr(self, default):
|
|
return self.abbreviation or default
|
|
|
|
def __str__(self):
|
|
if self.abbreviation:
|
|
return self.abbreviation + " - " + self.text
|
|
return self.text
|
|
|
|
class Meta:
|
|
ordering = ["id"]
|
|
|
|
|
|
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 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.ForeignKey(
|
|
Option, related_name="contests_lost", on_delete=models.CASCADE
|
|
)
|
|
amount = models.PositiveSmallIntegerField(_("votes supplémentaires"))
|
|
|
|
|
|
# #############################################################################
|
|
# Modification of the base User Model
|
|
# #############################################################################
|
|
|
|
|
|
class User(AbstractUser):
|
|
election = models.ForeignKey(
|
|
Election,
|
|
related_name="registered_voters",
|
|
null=True,
|
|
blank=True,
|
|
on_delete=models.CASCADE,
|
|
)
|
|
full_name = models.CharField(_("Nom et Prénom"), max_length=150, blank=True)
|
|
has_valid_email = models.BooleanField(_("email valide"), null=True, default=None)
|
|
|
|
@property
|
|
def base_username(self):
|
|
return "__".join(self.username.split("__")[1:])
|
|
|
|
def can_vote(self, request, election):
|
|
# Si c'est un·e utilisateur·ice CAS, iel peut voter dans les élections
|
|
# ouvertes à tou·te·s
|
|
if self.election is None:
|
|
# If the user is connected via CAS, request.session["CASCONNECTED"] is set
|
|
# to True by authens
|
|
return not election.restricted and request.session.get("CASCONNECTED")
|
|
|
|
# Pour les élections restreintes, il faut y être associé
|
|
return election.restricted and (self.election == election)
|
|
|
|
def get_prefix(self):
|
|
return self.username.split("__")[0]
|
|
|
|
@property
|
|
def connection_method(self):
|
|
method = self.username.split("__")[0]
|
|
return CONNECTION_METHODS.get(method, _("identifiants spécifiques"))
|
|
|
|
class Meta:
|
|
ordering = ["username"]
|