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.json import Serializer 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(Serializer, 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, ) serializable_fields = ["text_en", "text_fr", "type"] 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(Serializer, 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) serializable_fields = ["text_fr", "text_en", "abbreviation"] 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 is_admin(self, election): return election.created_by == self or self.is_staff 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"]