feat(kadenios): Add typing
This commit is contained in:
parent
c6aa72e843
commit
8f88eef5c7
19 changed files with 245 additions and 98 deletions
|
@ -31,6 +31,7 @@ in
|
||||||
(python3.withPackages (ps: [
|
(python3.withPackages (ps: [
|
||||||
ps.django
|
ps.django
|
||||||
ps.ipython
|
ps.ipython
|
||||||
|
ps.django-stubs
|
||||||
|
|
||||||
ps.markdown
|
ps.markdown
|
||||||
ps.numpy
|
ps.numpy
|
||||||
|
|
|
@ -14,6 +14,9 @@ class ElectionForm(forms.ModelForm):
|
||||||
|
|
||||||
def clean(self):
|
def clean(self):
|
||||||
cleaned_data = super().clean()
|
cleaned_data = super().clean()
|
||||||
|
|
||||||
|
assert cleaned_data is not None
|
||||||
|
|
||||||
if cleaned_data["start_date"] < timezone.now():
|
if cleaned_data["start_date"] < timezone.now():
|
||||||
self.add_error(
|
self.add_error(
|
||||||
"start_date", _("Impossible de faire débuter l'élection dans le passé")
|
"start_date", _("Impossible de faire débuter l'élection dans le passé")
|
||||||
|
|
|
@ -1,23 +1,32 @@
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
from django.contrib.auth.mixins import PermissionRequiredMixin
|
from django.contrib.auth.mixins import PermissionRequiredMixin
|
||||||
from django.db.models import Q
|
from django.db.models import Q, QuerySet
|
||||||
|
from django.http.request import HttpRequest
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
from django.views.generic.detail import SingleObjectMixin
|
from django.views.generic.detail import SingleObjectMixin
|
||||||
|
|
||||||
|
from elections.typing import AuthenticatedRequest
|
||||||
|
|
||||||
from .models import Election, Option, Question
|
from .models import Election, Option, Question
|
||||||
|
|
||||||
|
|
||||||
class AdminOnlyMixin(PermissionRequiredMixin):
|
class AdminOnlyMixin(PermissionRequiredMixin):
|
||||||
"""Restreint l'accès aux admins"""
|
"""Restreint l'accès aux admins"""
|
||||||
|
|
||||||
|
request: AuthenticatedRequest
|
||||||
|
|
||||||
permission_required = "elections.election_admin"
|
permission_required = "elections.election_admin"
|
||||||
|
|
||||||
|
|
||||||
class SelectElectionMixin:
|
class SelectElectionMixin:
|
||||||
"""Sélectionne automatiquement les foreignkeys voulues"""
|
"""Sélectionne automatiquement les foreignkeys voulues"""
|
||||||
|
|
||||||
def get_queryset(self):
|
model: type
|
||||||
qs = super().get_queryset()
|
|
||||||
|
def get_queryset(self) -> QuerySet:
|
||||||
|
qs = super().get_queryset() # pyright: ignore
|
||||||
if self.model is Question:
|
if self.model is Question:
|
||||||
return qs.select_related("election")
|
return qs.select_related("election")
|
||||||
elif self.model is Option:
|
elif self.model is Option:
|
||||||
|
@ -28,15 +37,19 @@ class SelectElectionMixin:
|
||||||
class RestrictAccessMixin(SelectElectionMixin):
|
class RestrictAccessMixin(SelectElectionMixin):
|
||||||
"""Permet de restreindre l'accès à des élections/questions/options"""
|
"""Permet de restreindre l'accès à des élections/questions/options"""
|
||||||
|
|
||||||
f_prefixes = {Election: "", Question: "election__", Option: "question__election__"}
|
f_prefixes = {
|
||||||
|
Election: "",
|
||||||
|
Question: "election__",
|
||||||
|
Option: "question__election__",
|
||||||
|
}
|
||||||
|
|
||||||
def get_f_prefix(self):
|
def get_f_prefix(self) -> str:
|
||||||
return self.f_prefixes.get(self.model, None)
|
return self.f_prefixes.get(self.model, "")
|
||||||
|
|
||||||
def get_filters(self):
|
def get_filters(self) -> dict[str, Any]:
|
||||||
return {}
|
return {}
|
||||||
|
|
||||||
def get_queryset(self):
|
def get_queryset(self) -> QuerySet:
|
||||||
qs = super().get_queryset()
|
qs = super().get_queryset()
|
||||||
if self.model in self.f_prefixes:
|
if self.model in self.f_prefixes:
|
||||||
return qs.filter(**self.get_filters())
|
return qs.filter(**self.get_filters())
|
||||||
|
@ -47,7 +60,7 @@ class RestrictAccessMixin(SelectElectionMixin):
|
||||||
class OpenElectionOnlyMixin(RestrictAccessMixin):
|
class OpenElectionOnlyMixin(RestrictAccessMixin):
|
||||||
"""N'autorise la vue que lorsque l'élection est ouverte"""
|
"""N'autorise la vue que lorsque l'élection est ouverte"""
|
||||||
|
|
||||||
def get_filters(self):
|
def get_filters(self) -> dict[str, Any]:
|
||||||
f_prefix = self.get_f_prefix()
|
f_prefix = self.get_f_prefix()
|
||||||
# On ne peut modifier que les élections qui n'ont pas commencé, et
|
# On ne peut modifier que les élections qui n'ont pas commencé, et
|
||||||
# accessoirement qui ne sont pas dépouillées ou archivées
|
# accessoirement qui ne sont pas dépouillées ou archivées
|
||||||
|
@ -67,7 +80,7 @@ class CreatorOnlyMixin(AdminOnlyMixin, RestrictAccessMixin, SingleObjectMixin):
|
||||||
def get_next_url(self):
|
def get_next_url(self):
|
||||||
return reverse("kadenios")
|
return reverse("kadenios")
|
||||||
|
|
||||||
def get_filters(self):
|
def get_filters(self) -> dict[str, Any]:
|
||||||
filters = super().get_filters()
|
filters = super().get_filters()
|
||||||
# TODO: change the way we collect the user according to the model used
|
# TODO: change the way we collect the user according to the model used
|
||||||
filters[self.get_f_prefix() + "created_by"] = self.request.user
|
filters[self.get_f_prefix() + "created_by"] = self.request.user
|
||||||
|
@ -77,7 +90,7 @@ class CreatorOnlyMixin(AdminOnlyMixin, RestrictAccessMixin, SingleObjectMixin):
|
||||||
class CreatorOnlyEditMixin(CreatorOnlyMixin):
|
class CreatorOnlyEditMixin(CreatorOnlyMixin):
|
||||||
"""Permet au créateurice de modifier l'élection implicitement"""
|
"""Permet au créateurice de modifier l'élection implicitement"""
|
||||||
|
|
||||||
def get_filters(self):
|
def get_filters(self) -> dict[str, Any]:
|
||||||
# On ne peut modifier que les élections qui n'ont pas commencé
|
# On ne peut modifier que les élections qui n'ont pas commencé
|
||||||
filters = super().get_filters()
|
filters = super().get_filters()
|
||||||
filters[self.get_f_prefix() + "start_date__gt"] = timezone.now()
|
filters[self.get_f_prefix() + "start_date__gt"] = timezone.now()
|
||||||
|
@ -87,7 +100,7 @@ class CreatorOnlyEditMixin(CreatorOnlyMixin):
|
||||||
class ClosedElectionMixin(CreatorOnlyMixin):
|
class ClosedElectionMixin(CreatorOnlyMixin):
|
||||||
"""Permet d'agir sur une élection terminée"""
|
"""Permet d'agir sur une élection terminée"""
|
||||||
|
|
||||||
def get_filters(self):
|
def get_filters(self) -> dict[str, Any]:
|
||||||
f_prefix = self.get_f_prefix()
|
f_prefix = self.get_f_prefix()
|
||||||
# L'élection doit être terminée et non archivée
|
# L'élection doit être terminée et non archivée
|
||||||
filters = super().get_filters()
|
filters = super().get_filters()
|
||||||
|
@ -102,9 +115,11 @@ class NotArchivedMixin:
|
||||||
ou dont on est l'admin
|
ou dont on est l'admin
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def get_queryset(self):
|
request: HttpRequest
|
||||||
|
|
||||||
|
def get_queryset(self) -> QuerySet:
|
||||||
user = self.request.user
|
user = self.request.user
|
||||||
qs = super().get_queryset()
|
qs = super().get_queryset() # pyright: ignore
|
||||||
if user.is_authenticated:
|
if user.is_authenticated:
|
||||||
return qs.filter(Q(archived=False, visible=True) | Q(created_by=user))
|
return qs.filter(Q(archived=False, visible=True) | Q(created_by=user))
|
||||||
|
|
||||||
|
|
|
@ -1,8 +1,11 @@
|
||||||
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
from translated_fields import TranslatedFieldWithFallback
|
from translated_fields import TranslatedFieldWithFallback
|
||||||
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.contrib.auth.models import AbstractUser
|
from django.contrib.auth.models import AbstractUser
|
||||||
from django.db import models, transaction
|
from django.db import models, transaction
|
||||||
|
from django.http.request import HttpRequest
|
||||||
from django.template.loader import render_to_string
|
from django.template.loader import render_to_string
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
|
||||||
|
@ -25,12 +28,20 @@ from .utils import (
|
||||||
ValidateFunctions,
|
ValidateFunctions,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from django.db.models.fields.related_descriptors import ManyRelatedManager
|
||||||
|
from django.utils.functional import _StrPromise
|
||||||
|
|
||||||
|
|
||||||
# #############################################################################
|
# #############################################################################
|
||||||
# Models regarding an election
|
# Models regarding an election
|
||||||
# #############################################################################
|
# #############################################################################
|
||||||
|
|
||||||
|
|
||||||
class Election(models.Model):
|
class Election(models.Model):
|
||||||
|
registered_voters: models.Manager["User"]
|
||||||
|
questions: models.Manager["Question"]
|
||||||
|
|
||||||
name = TranslatedFieldWithFallback(models.CharField(_("nom"), max_length=255))
|
name = TranslatedFieldWithFallback(models.CharField(_("nom"), max_length=255))
|
||||||
short_name = models.SlugField(_("nom bref"), unique=True)
|
short_name = models.SlugField(_("nom bref"), unique=True)
|
||||||
description = TranslatedFieldWithFallback(
|
description = TranslatedFieldWithFallback(
|
||||||
|
@ -88,6 +99,9 @@ class Election(models.Model):
|
||||||
|
|
||||||
|
|
||||||
class Question(Serializer, models.Model):
|
class Question(Serializer, models.Model):
|
||||||
|
options: models.Manager["Option"]
|
||||||
|
duels: models.Manager["Duel"]
|
||||||
|
|
||||||
election = models.ForeignKey(
|
election = models.ForeignKey(
|
||||||
Election, related_name="questions", on_delete=models.CASCADE
|
Election, related_name="questions", on_delete=models.CASCADE
|
||||||
)
|
)
|
||||||
|
@ -113,22 +127,22 @@ class Question(Serializer, models.Model):
|
||||||
|
|
||||||
serializable_fields = ["text_en", "text_fr", "type"]
|
serializable_fields = ["text_en", "text_fr", "type"]
|
||||||
|
|
||||||
def is_form_valid(self, vote_form):
|
def is_form_valid(self, vote_form) -> bool:
|
||||||
validate_function = getattr(ValidateFunctions, VALIDATE_FUNCTIONS[self.type])
|
validate_function = getattr(ValidateFunctions, VALIDATE_FUNCTIONS[self.type])
|
||||||
return vote_form.is_valid() and validate_function(vote_form)
|
return vote_form.is_valid() and validate_function(vote_form)
|
||||||
|
|
||||||
@transaction.atomic
|
@transaction.atomic
|
||||||
def cast_ballot(self, user, vote_form):
|
def cast_ballot(self, user: "User", vote_form) -> None:
|
||||||
cast_function = getattr(CastFunctions, CAST_FUNCTIONS[self.type])
|
cast_function = getattr(CastFunctions, CAST_FUNCTIONS[self.type])
|
||||||
cast_function(user, vote_form)
|
cast_function(user, vote_form)
|
||||||
|
|
||||||
@transaction.atomic
|
@transaction.atomic
|
||||||
def tally(self):
|
def tally(self) -> None:
|
||||||
tally_function = getattr(TallyFunctions, TALLY_FUNCTIONS[self.type])
|
tally_function = getattr(TallyFunctions, TALLY_FUNCTIONS[self.type])
|
||||||
tally_function(self)
|
tally_function(self)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def results(self):
|
def results(self) -> str:
|
||||||
return render_to_string(
|
return render_to_string(
|
||||||
f"elections/results/{self.vote_type}_export.txt", {"question": self}
|
f"elections/results/{self.vote_type}_export.txt", {"question": self}
|
||||||
)
|
)
|
||||||
|
@ -150,8 +164,8 @@ class Question(Serializer, models.Model):
|
||||||
def vote_type(self):
|
def vote_type(self):
|
||||||
return BALLOT_TYPE[self.type]
|
return BALLOT_TYPE[self.type]
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self) -> str:
|
||||||
return self.text
|
return str(self.text)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
ordering = ["id"]
|
ordering = ["id"]
|
||||||
|
@ -168,7 +182,7 @@ class Option(Serializer, models.Model):
|
||||||
voters = models.ManyToManyField(
|
voters = models.ManyToManyField(
|
||||||
settings.AUTH_USER_MODEL,
|
settings.AUTH_USER_MODEL,
|
||||||
related_name="votes",
|
related_name="votes",
|
||||||
through="Vote",
|
through="elections.Vote",
|
||||||
blank=True,
|
blank=True,
|
||||||
)
|
)
|
||||||
# For now, we store the amount of votes received after the election is tallied
|
# For now, we store the amount of votes received after the election is tallied
|
||||||
|
@ -182,13 +196,13 @@ class Option(Serializer, models.Model):
|
||||||
|
|
||||||
super().save(*args, **kwargs)
|
super().save(*args, **kwargs)
|
||||||
|
|
||||||
def get_abbr(self, default):
|
def get_abbr(self, default: str) -> str:
|
||||||
return self.abbreviation or default
|
return self.abbreviation or default
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self) -> str:
|
||||||
if self.abbreviation:
|
if self.abbreviation:
|
||||||
return self.abbreviation + " - " + self.text
|
return f"{self.abbreviation} - {self.text}"
|
||||||
return self.text
|
return str(self.text)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
ordering = ["id"]
|
ordering = ["id"]
|
||||||
|
@ -202,6 +216,10 @@ class Vote(models.Model):
|
||||||
ordering = ["option"]
|
ordering = ["option"]
|
||||||
|
|
||||||
|
|
||||||
|
class RankedVote(Vote):
|
||||||
|
rank: "Rank"
|
||||||
|
|
||||||
|
|
||||||
class Rank(models.Model):
|
class Rank(models.Model):
|
||||||
vote = models.OneToOneField(Vote, on_delete=models.CASCADE)
|
vote = models.OneToOneField(Vote, on_delete=models.CASCADE)
|
||||||
rank = models.PositiveSmallIntegerField(_("rang de l'option"))
|
rank = models.PositiveSmallIntegerField(_("rang de l'option"))
|
||||||
|
@ -229,6 +247,10 @@ class Duel(models.Model):
|
||||||
|
|
||||||
|
|
||||||
class User(AbstractUser):
|
class User(AbstractUser):
|
||||||
|
cast_elections: "ManyRelatedManager[Election]"
|
||||||
|
cast_questions: "ManyRelatedManager[Question]"
|
||||||
|
votes: "ManyRelatedManager[Vote]"
|
||||||
|
|
||||||
election = models.ForeignKey(
|
election = models.ForeignKey(
|
||||||
Election,
|
Election,
|
||||||
related_name="registered_voters",
|
related_name="registered_voters",
|
||||||
|
@ -240,28 +262,30 @@ class User(AbstractUser):
|
||||||
has_valid_email = models.BooleanField(_("email valide"), null=True, default=None)
|
has_valid_email = models.BooleanField(_("email valide"), null=True, default=None)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def base_username(self):
|
def base_username(self) -> str:
|
||||||
return "__".join(self.username.split("__")[1:])
|
return "__".join(self.username.split("__")[1:])
|
||||||
|
|
||||||
def can_vote(self, request, election):
|
def can_vote(self, request: HttpRequest, election: Election) -> bool:
|
||||||
# Si c'est un·e utilisateur·ice CAS, iel peut voter dans les élections
|
# Si c'est un·e utilisateur·ice CAS, iel peut voter dans les élections
|
||||||
# ouvertes à tou·te·s
|
# ouvertes à tou·te·s
|
||||||
if self.election is None:
|
if self.election is None:
|
||||||
# If the user is connected via CAS, request.session["CASCONNECTED"] is set
|
# If the user is connected via CAS, request.session["CASCONNECTED"] is set
|
||||||
# to True by authens
|
# to True by authens
|
||||||
return not election.restricted and request.session.get("CASCONNECTED")
|
return not election.restricted and request.session.get(
|
||||||
|
"CASCONNECTED", False
|
||||||
|
)
|
||||||
|
|
||||||
# Pour les élections restreintes, il faut y être associé
|
# Pour les élections restreintes, il faut y être associé
|
||||||
return election.restricted and (self.election == election)
|
return election.restricted and (self.election == election)
|
||||||
|
|
||||||
def is_admin(self, election):
|
def is_admin(self, election: Election) -> bool:
|
||||||
return election.created_by == self or self.is_staff
|
return election.created_by == self or self.is_staff
|
||||||
|
|
||||||
def get_prefix(self):
|
def get_prefix(self) -> str:
|
||||||
return self.username.split("__")[0]
|
return self.username.split("__")[0]
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def connection_method(self):
|
def connection_method(self) -> "_StrPromise":
|
||||||
method = self.username.split("__")[0]
|
method = self.username.split("__")[0]
|
||||||
return CONNECTION_METHODS.get(method, _("identifiants spécifiques"))
|
return CONNECTION_METHODS.get(method, _("identifiants spécifiques"))
|
||||||
|
|
||||||
|
|
|
@ -5,7 +5,7 @@ from .utils import send_mail
|
||||||
|
|
||||||
|
|
||||||
@background
|
@background
|
||||||
def send_election_mail(election_pk, subject, body, reply_to):
|
def send_election_mail(election_pk: int, subject: str, body: str, reply_to: str):
|
||||||
election = Election.objects.get(pk=election_pk)
|
election = Election.objects.get(pk=election_pk)
|
||||||
send_mail(election, subject, body, reply_to)
|
send_mail(election, subject, body, reply_to)
|
||||||
election.sent_mail = True
|
election.sent_mail = True
|
||||||
|
|
|
@ -1,3 +1,5 @@
|
||||||
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.contrib.auth import get_user_model
|
from django.contrib.auth import get_user_model
|
||||||
from django.test import TestCase
|
from django.test import TestCase
|
||||||
|
@ -5,7 +7,10 @@ from django.utils.translation import gettext_lazy as _
|
||||||
|
|
||||||
from .test_utils import create_election
|
from .test_utils import create_election
|
||||||
|
|
||||||
User = get_user_model()
|
if TYPE_CHECKING:
|
||||||
|
from elections.typing import User
|
||||||
|
else:
|
||||||
|
User = get_user_model()
|
||||||
|
|
||||||
|
|
||||||
class UserTests(TestCase):
|
class UserTests(TestCase):
|
||||||
|
@ -40,8 +45,11 @@ class UserTests(TestCase):
|
||||||
session["CASCONNECTED"] = True
|
session["CASCONNECTED"] = True
|
||||||
session.save()
|
session.save()
|
||||||
|
|
||||||
|
assert session.session_key is not None
|
||||||
|
|
||||||
# On sauvegarde le cookie de session
|
# On sauvegarde le cookie de session
|
||||||
session_cookie_name = settings.SESSION_COOKIE_NAME
|
session_cookie_name = settings.SESSION_COOKIE_NAME
|
||||||
|
|
||||||
self.client.cookies[session_cookie_name] = session.session_key
|
self.client.cookies[session_cookie_name] = session.session_key
|
||||||
|
|
||||||
self.assertFalse(self.cas_user.can_vote(self.client, self.election_1))
|
self.assertFalse(self.cas_user.can_vote(self.client, self.election_1))
|
||||||
|
|
|
@ -1,11 +1,16 @@
|
||||||
from django.contrib.auth import get_user_model
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
from django.contrib.auth.models import Permission
|
from django.contrib.auth.models import Permission
|
||||||
from django.test import TestCase
|
from django.test import TestCase
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
|
|
||||||
from .test_utils import create_election
|
from .test_utils import create_election
|
||||||
|
|
||||||
User = get_user_model()
|
if TYPE_CHECKING:
|
||||||
|
from elections.typing import User
|
||||||
|
else:
|
||||||
|
from django.contrib.auth import get_user_model
|
||||||
|
User = get_user_model()
|
||||||
|
|
||||||
|
|
||||||
class AdminViewsTest(TestCase):
|
class AdminViewsTest(TestCase):
|
||||||
|
|
7
elections/typing.py
Normal file
7
elections/typing.py
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
from django.http.request import HttpRequest
|
||||||
|
|
||||||
|
from elections.models import User
|
||||||
|
|
||||||
|
|
||||||
|
class AuthenticatedRequest(HttpRequest):
|
||||||
|
user: User
|
|
@ -1,31 +1,46 @@
|
||||||
import csv
|
import csv
|
||||||
import io
|
import io
|
||||||
import smtplib
|
import smtplib
|
||||||
|
from typing import TYPE_CHECKING, TypeGuard
|
||||||
|
|
||||||
import networkx as nx
|
import networkx as nx
|
||||||
import numpy as np
|
import numpy as np
|
||||||
from networkx.algorithms.dag import ancestors, descendants
|
from networkx.algorithms.dag import ancestors, descendants
|
||||||
|
from numpy._typing import NDArray
|
||||||
|
|
||||||
from django.contrib.auth import get_user_model
|
from django.contrib.auth import get_user_model
|
||||||
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
|
||||||
|
from django.core.files.base import File
|
||||||
from django.core.mail import EmailMessage
|
from django.core.mail import EmailMessage
|
||||||
from django.core.validators import validate_email
|
from django.core.validators import validate_email
|
||||||
|
from django.forms import BaseFormSet
|
||||||
from django.template.loader import render_to_string
|
from django.template.loader import render_to_string
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
|
||||||
from shared.auth.utils import generate_password
|
from shared.auth.utils import generate_password
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from elections.forms import RankVoteForm, SelectVoteForm
|
||||||
|
from elections.models import Election, Question, RankedVote, Vote
|
||||||
|
from elections.typing import User
|
||||||
|
|
||||||
|
|
||||||
# #############################################################################
|
# #############################################################################
|
||||||
# Classes pour différencier les différents types de questions
|
# Classes pour différencier les différents types de questions
|
||||||
# #############################################################################
|
# #############################################################################
|
||||||
|
|
||||||
|
|
||||||
|
def has_rank(v: "Vote") -> TypeGuard["RankedVote"]:
|
||||||
|
return hasattr(v, "rank")
|
||||||
|
|
||||||
|
|
||||||
class CastFunctions:
|
class CastFunctions:
|
||||||
"""Classe pour enregistrer les votes"""
|
"""Classe pour enregistrer les votes"""
|
||||||
|
|
||||||
def cast_select(user, vote_form):
|
@staticmethod
|
||||||
|
def cast_select(user: "User", vote_form: "BaseFormSet[SelectVoteForm]"):
|
||||||
"""On enregistre un vote classique"""
|
"""On enregistre un vote classique"""
|
||||||
selected, n_selected = [], []
|
selected, n_selected = [], []
|
||||||
for v in vote_form:
|
for v in vote_form:
|
||||||
|
@ -37,7 +52,8 @@ class CastFunctions:
|
||||||
user.votes.add(*selected)
|
user.votes.add(*selected)
|
||||||
user.votes.remove(*n_selected)
|
user.votes.remove(*n_selected)
|
||||||
|
|
||||||
def cast_rank(user, vote_form):
|
@staticmethod
|
||||||
|
def cast_rank(user: "User", vote_form: "BaseFormSet[RankVoteForm]"):
|
||||||
"""On enregistre un vote par classement"""
|
"""On enregistre un vote par classement"""
|
||||||
from .models import Rank, Vote
|
from .models import Rank, Vote
|
||||||
|
|
||||||
|
@ -53,7 +69,8 @@ class CastFunctions:
|
||||||
|
|
||||||
for v in vote_form:
|
for v in vote_form:
|
||||||
vote = votes[v.instance]
|
vote = votes[v.instance]
|
||||||
if hasattr(vote, "rank"):
|
|
||||||
|
if has_rank(vote):
|
||||||
vote.rank.rank = v.cleaned_data["rank"]
|
vote.rank.rank = v.cleaned_data["rank"]
|
||||||
ranks_update.append(vote.rank)
|
ranks_update.append(vote.rank)
|
||||||
else:
|
else:
|
||||||
|
@ -66,7 +83,8 @@ class CastFunctions:
|
||||||
class TallyFunctions:
|
class TallyFunctions:
|
||||||
"""Classe pour gérer les dépouillements"""
|
"""Classe pour gérer les dépouillements"""
|
||||||
|
|
||||||
def tally_select(question):
|
@staticmethod
|
||||||
|
def tally_select(question: "Question") -> None:
|
||||||
"""On dépouille un vote classique"""
|
"""On dépouille un vote classique"""
|
||||||
from .models import Option
|
from .models import Option
|
||||||
|
|
||||||
|
@ -86,7 +104,8 @@ class TallyFunctions:
|
||||||
|
|
||||||
Option.objects.bulk_update(options, ["nb_votes", "winner"])
|
Option.objects.bulk_update(options, ["nb_votes", "winner"])
|
||||||
|
|
||||||
def tally_schultze(question):
|
@staticmethod
|
||||||
|
def tally_schultze(question: "Question") -> None:
|
||||||
"""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, Option, Rank
|
from .models import Duel, Option, Rank
|
||||||
|
|
||||||
|
@ -102,12 +121,12 @@ class TallyFunctions:
|
||||||
else:
|
else:
|
||||||
ranks_by_user[user] = [r]
|
ranks_by_user[user] = [r]
|
||||||
|
|
||||||
ballots = []
|
ballots: list[NDArray[np.int_]] = []
|
||||||
|
|
||||||
# Pour chaque votant·e, on regarde son classement
|
# Pour chaque votant·e, on regarde son classement
|
||||||
for user in ranks_by_user:
|
for user in ranks_by_user:
|
||||||
votes = ranks_by_user[user]
|
votes = ranks_by_user[user]
|
||||||
ballot = np.zeros((nb_options, nb_options))
|
ballot = np.zeros((nb_options, nb_options), dtype=int)
|
||||||
|
|
||||||
for i in range(nb_options):
|
for i in range(nb_options):
|
||||||
for j in range(i):
|
for j in range(i):
|
||||||
|
@ -121,6 +140,9 @@ class TallyFunctions:
|
||||||
# des duels
|
# des duels
|
||||||
duels = sum(ballots)
|
duels = sum(ballots)
|
||||||
|
|
||||||
|
# As ballots is not empty, sum cannot be 0
|
||||||
|
assert duels != 0
|
||||||
|
|
||||||
# Configuration du graphe
|
# Configuration du graphe
|
||||||
graph = nx.DiGraph()
|
graph = nx.DiGraph()
|
||||||
|
|
||||||
|
@ -163,11 +185,11 @@ class TallyFunctions:
|
||||||
# le plus faible
|
# le plus faible
|
||||||
min_weight = min(nx.get_edge_attributes(graph, "weight").values())
|
min_weight = min(nx.get_edge_attributes(graph, "weight").values())
|
||||||
min_edges = []
|
min_edges = []
|
||||||
for (u, v) in graph.edges():
|
for u, v in graph.edges():
|
||||||
if graph[u][v]["weight"] == min_weight:
|
if graph[u][v]["weight"] == min_weight:
|
||||||
min_edges.append((u, v))
|
min_edges.append((u, v))
|
||||||
|
|
||||||
for (u, v) in min_edges:
|
for u, v in min_edges:
|
||||||
graph.remove_edge(u, v)
|
graph.remove_edge(u, v)
|
||||||
|
|
||||||
# Les options gagnantes sont celles encore présentes dans le graphe
|
# Les options gagnantes sont celles encore présentes dans le graphe
|
||||||
|
@ -181,29 +203,31 @@ class TallyFunctions:
|
||||||
class ValidateFunctions:
|
class ValidateFunctions:
|
||||||
"""Classe pour valider les formsets selon le type de question"""
|
"""Classe pour valider les formsets selon le type de question"""
|
||||||
|
|
||||||
def always_true(vote_form):
|
@staticmethod
|
||||||
"""Retourne True pour les votes sans validation particulière"""
|
def always_true(_) -> bool:
|
||||||
|
"""Renvoie True pour les votes sans validation particulière"""
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def unique_selected(vote_form):
|
@staticmethod
|
||||||
|
def unique_selected(vote_form: "BaseFormSet[SelectVoteForm]") -> bool:
|
||||||
"""Vérifie qu'une seule option est choisie"""
|
"""Vérifie qu'une seule option est choisie"""
|
||||||
nb_selected = 0
|
|
||||||
for v in vote_form:
|
nb_selected = sum(v.cleaned_data["selected"] for v in vote_form)
|
||||||
nb_selected += v.cleaned_data["selected"]
|
|
||||||
|
|
||||||
if nb_selected == 0:
|
if nb_selected == 0:
|
||||||
vote_form._non_form_errors.append(
|
vote_form._non_form_errors.append( # pyright: ignore
|
||||||
ValidationError(_("Vous devez sélectionnner une option."))
|
ValidationError(_("Vous devez sélectionnner une option."))
|
||||||
)
|
)
|
||||||
return False
|
return False
|
||||||
elif nb_selected > 1:
|
elif nb_selected > 1:
|
||||||
vote_form._non_form_errors.append(
|
vote_form._non_form_errors.append( # pyright: ignore
|
||||||
ValidationError(_("Vous ne pouvez pas sélectionner plus d'une option."))
|
ValidationError(_("Vous ne pouvez pas sélectionner plus d'une option."))
|
||||||
)
|
)
|
||||||
return False
|
return False
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def limit_ranks(vote_form):
|
@staticmethod
|
||||||
|
def limit_ranks(vote_form: "BaseFormSet[RankVoteForm]"):
|
||||||
"""Limite le classement au nombre d'options"""
|
"""Limite le classement au nombre d'options"""
|
||||||
nb_options = len(vote_form)
|
nb_options = len(vote_form)
|
||||||
valid = True
|
valid = True
|
||||||
|
@ -229,11 +253,13 @@ class ValidateFunctions:
|
||||||
class ResultsData:
|
class ResultsData:
|
||||||
"""Classe pour afficher des informations supplémentaires après la fin d'une élection"""
|
"""Classe pour afficher des informations supplémentaires après la fin d'une élection"""
|
||||||
|
|
||||||
def select(question):
|
@staticmethod
|
||||||
|
def select(_: "Question") -> str:
|
||||||
"""On renvoie l'explication des couleurs"""
|
"""On renvoie l'explication des couleurs"""
|
||||||
return render_to_string("elections/results/select.html")
|
return render_to_string("elections/results/select.html")
|
||||||
|
|
||||||
def rank(question):
|
@staticmethod
|
||||||
|
def rank(question: "Question") -> str:
|
||||||
"""On récupère la matrice des résultats et on l'affiche"""
|
"""On récupère la matrice des résultats et on l'affiche"""
|
||||||
duels = question.duels.all()
|
duels = question.duels.all()
|
||||||
options = list(question.options.all())
|
options = list(question.options.all())
|
||||||
|
@ -270,7 +296,8 @@ class ResultsData:
|
||||||
class BallotsData:
|
class BallotsData:
|
||||||
"""Classe pour afficher les bulletins d'une question"""
|
"""Classe pour afficher les bulletins d'une question"""
|
||||||
|
|
||||||
def select(question):
|
@staticmethod
|
||||||
|
def select(question: "Question") -> str:
|
||||||
"""Renvoie un tableau affichant les options sélectionnées pour chaque bulletin"""
|
"""Renvoie un tableau affichant les options sélectionnées pour chaque bulletin"""
|
||||||
from .models import Vote
|
from .models import Vote
|
||||||
|
|
||||||
|
@ -290,7 +317,8 @@ class BallotsData:
|
||||||
{"options": options, "ballots": sorted(ballots.values(), reverse=True)},
|
{"options": options, "ballots": sorted(ballots.values(), reverse=True)},
|
||||||
)
|
)
|
||||||
|
|
||||||
def rank(question):
|
@staticmethod
|
||||||
|
def rank(question: "Question") -> str:
|
||||||
"""Renvoie un tableau contenant les classements des options par bulletin"""
|
"""Renvoie un tableau contenant les classements des options par bulletin"""
|
||||||
from .models import Rank
|
from .models import Rank
|
||||||
|
|
||||||
|
@ -318,7 +346,7 @@ class BallotsData:
|
||||||
# #############################################################################
|
# #############################################################################
|
||||||
|
|
||||||
|
|
||||||
def create_users(election, csv_file):
|
def create_users(election: "Election", csv_file: File):
|
||||||
"""Crée les votant·e·s pour l'élection donnée, en remplissant les champs
|
"""Crée les votant·e·s pour l'élection donnée, en remplissant les champs
|
||||||
`username`, `election` et `full_name`.
|
`username`, `election` et `full_name`.
|
||||||
"""
|
"""
|
||||||
|
@ -331,7 +359,7 @@ def create_users(election, csv_file):
|
||||||
users = [
|
users = [
|
||||||
User(
|
User(
|
||||||
election=election,
|
election=election,
|
||||||
username=f"{election.id}__{username}",
|
username=f"{election.pk}__{username}",
|
||||||
email=email,
|
email=email,
|
||||||
full_name=full_name,
|
full_name=full_name,
|
||||||
)
|
)
|
||||||
|
@ -341,7 +369,7 @@ def create_users(election, csv_file):
|
||||||
User.objects.bulk_create(users)
|
User.objects.bulk_create(users)
|
||||||
|
|
||||||
|
|
||||||
def check_csv(csv_file):
|
def check_csv(csv_file: File):
|
||||||
"""Vérifie que le fichier donnant la liste de votant·e·s est bien formé"""
|
"""Vérifie que le fichier donnant la liste de votant·e·s est bien formé"""
|
||||||
try:
|
try:
|
||||||
dialect = csv.Sniffer().sniff(csv_file.readline().decode("utf-8"))
|
dialect = csv.Sniffer().sniff(csv_file.readline().decode("utf-8"))
|
||||||
|
@ -394,15 +422,14 @@ def check_csv(csv_file):
|
||||||
return errors
|
return errors
|
||||||
|
|
||||||
|
|
||||||
def send_mail(election, subject, body, reply_to):
|
def send_mail(election: "Election", subject: str, body: str, reply_to: str) -> None:
|
||||||
"""Envoie le mail d'annonce de l'élection avec identifiants et mot de passe
|
"""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é.
|
aux votant·e·s, le mdp est généré en même temps que le mail est envoyé.
|
||||||
"""
|
"""
|
||||||
User = get_user_model()
|
|
||||||
|
|
||||||
# On n'envoie le mail qu'aux personnes qui n'en n'ont pas déjà reçu un
|
# On n'envoie le mail qu'aux personnes qui n'en n'ont pas déjà reçu un
|
||||||
voters = list(election.registered_voters.exclude(has_valid_email=True))
|
voters = list(election.registered_voters.exclude(has_valid_email=True))
|
||||||
e_url = reverse("election.view", args=[election.id])
|
e_url = reverse("election.view", args=[election.pk])
|
||||||
url = f"https://vote.eleves.ens.fr{e_url}"
|
url = f"https://vote.eleves.ens.fr{e_url}"
|
||||||
start = election.start_date.strftime("%d/%m/%Y %H:%M %Z")
|
start = election.start_date.strftime("%d/%m/%Y %H:%M %Z")
|
||||||
end = election.end_date.strftime("%d/%m/%Y %H:%M %Z")
|
end = election.end_date.strftime("%d/%m/%Y %H:%M %Z")
|
||||||
|
@ -431,7 +458,7 @@ def send_mail(election, subject, body, reply_to):
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
for (m, v) in messages:
|
for m, v in messages:
|
||||||
try:
|
try:
|
||||||
m.send()
|
m.send()
|
||||||
except smtplib.SMTPException:
|
except smtplib.SMTPException:
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import csv
|
import csv
|
||||||
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
from django.contrib import messages
|
from django.contrib import messages
|
||||||
from django.contrib.auth import get_user_model
|
|
||||||
from django.contrib.messages.views import SuccessMessageMixin
|
from django.contrib.messages.views import SuccessMessageMixin
|
||||||
from django.core.mail import EmailMessage
|
from django.core.mail import EmailMessage
|
||||||
from django.db import transaction
|
from django.db import transaction
|
||||||
|
@ -19,6 +19,7 @@ from django.views.generic import (
|
||||||
View,
|
View,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
from elections.typing import AuthenticatedRequest
|
||||||
from shared.json import JsonCreateView, JsonDeleteView, JsonUpdateView
|
from shared.json import JsonCreateView, JsonDeleteView, JsonUpdateView
|
||||||
from shared.views import BackgroundUpdateView, TimeMixin
|
from shared.views import BackgroundUpdateView, TimeMixin
|
||||||
|
|
||||||
|
@ -43,7 +44,11 @@ from .staticdefs import MAIL_VOTE_DELETED, MAIL_VOTERS, QUESTION_TYPES, VOTE_RUL
|
||||||
from .tasks import send_election_mail
|
from .tasks import send_election_mail
|
||||||
from .utils import create_users
|
from .utils import create_users
|
||||||
|
|
||||||
User = get_user_model()
|
if TYPE_CHECKING:
|
||||||
|
from elections.typing import User
|
||||||
|
else:
|
||||||
|
from django.contrib.auth import get_user_model
|
||||||
|
User = get_user_model()
|
||||||
|
|
||||||
# TODO: access control *everywhere*
|
# TODO: access control *everywhere*
|
||||||
|
|
||||||
|
@ -53,6 +58,8 @@ User = get_user_model()
|
||||||
|
|
||||||
|
|
||||||
class ElectionCreateView(AdminOnlyMixin, SuccessMessageMixin, CreateView):
|
class ElectionCreateView(AdminOnlyMixin, SuccessMessageMixin, CreateView):
|
||||||
|
object: Election
|
||||||
|
|
||||||
model = Election
|
model = Election
|
||||||
form_class = ElectionForm
|
form_class = ElectionForm
|
||||||
success_message = _("Élection créée avec succès !")
|
success_message = _("Élection créée avec succès !")
|
||||||
|
@ -61,7 +68,7 @@ class ElectionCreateView(AdminOnlyMixin, SuccessMessageMixin, CreateView):
|
||||||
def get_success_url(self):
|
def get_success_url(self):
|
||||||
return reverse("election.admin", args=[self.object.pk])
|
return reverse("election.admin", args=[self.object.pk])
|
||||||
|
|
||||||
def form_valid(self, form):
|
def form_valid(self, form: ElectionForm):
|
||||||
# We need to add the short name and the creator od the election
|
# We need to add the short name and the creator od the election
|
||||||
form.instance.short_name = slugify(
|
form.instance.short_name = slugify(
|
||||||
form.instance.start_date.strftime("%Y-%m-%d") + "_" + form.instance.name
|
form.instance.start_date.strftime("%Y-%m-%d") + "_" + form.instance.name
|
||||||
|
@ -75,11 +82,11 @@ class ElectionDeleteView(CreatorOnlyMixin, BackgroundUpdateView):
|
||||||
model = Election
|
model = Election
|
||||||
pattern_name = "election.list"
|
pattern_name = "election.list"
|
||||||
|
|
||||||
def get_object(self, queryset=None):
|
def get_object(self):
|
||||||
obj = self.get_object()
|
obj: Election = super().get_object()
|
||||||
# On ne peut supprimer que les élections n'ayant pas eu de vote et dont
|
# On ne peut supprimer que les élections n'ayant pas eu de vote et dont
|
||||||
# le mail d'annonce n'a pas été fait
|
# le mail d'annonce n'a pas été fait
|
||||||
if obj.voters.exists() or obj.send_election_mail:
|
if obj.voters.exists() or obj.sent_mail:
|
||||||
raise Http404
|
raise Http404
|
||||||
return obj
|
return obj
|
||||||
|
|
||||||
|
@ -89,6 +96,8 @@ class ElectionDeleteView(CreatorOnlyMixin, BackgroundUpdateView):
|
||||||
|
|
||||||
|
|
||||||
class ElectionAdminView(CreatorOnlyMixin, TimeMixin, DetailView):
|
class ElectionAdminView(CreatorOnlyMixin, TimeMixin, DetailView):
|
||||||
|
object: Election
|
||||||
|
|
||||||
model = Election
|
model = Election
|
||||||
template_name = "elections/election_admin.html"
|
template_name = "elections/election_admin.html"
|
||||||
|
|
||||||
|
@ -115,7 +124,7 @@ class ElectionSetVisibleView(CreatorOnlyMixin, BackgroundUpdateView):
|
||||||
success_message = _("Élection visible !")
|
success_message = _("Élection visible !")
|
||||||
|
|
||||||
def get(self, request, *args, **kwargs):
|
def get(self, request, *args, **kwargs):
|
||||||
self.election = self.get_object()
|
self.election: Election = self.get_object()
|
||||||
self.election.visible = True
|
self.election.visible = True
|
||||||
self.election.save()
|
self.election.save()
|
||||||
return super().get(request, *args, **kwargs)
|
return super().get(request, *args, **kwargs)
|
||||||
|
@ -232,6 +241,8 @@ class ElectionUpdateView(CreatorOnlyEditMixin, SuccessMessageMixin, UpdateView):
|
||||||
|
|
||||||
|
|
||||||
class DeleteVoteView(ClosedElectionMixin, JsonDeleteView):
|
class DeleteVoteView(ClosedElectionMixin, JsonDeleteView):
|
||||||
|
voter: User
|
||||||
|
|
||||||
model = Election
|
model = Election
|
||||||
|
|
||||||
def get_message(self):
|
def get_message(self):
|
||||||
|
@ -416,7 +427,7 @@ class ElectionView(NotArchivedMixin, DetailView):
|
||||||
context = super().get_context_data(**kwargs)
|
context = super().get_context_data(**kwargs)
|
||||||
context["current_time"] = timezone.now()
|
context["current_time"] = timezone.now()
|
||||||
|
|
||||||
if user.is_authenticated:
|
if user.is_authenticated and isinstance(user, User):
|
||||||
context["can_vote"] = user.can_vote(self.request, context["election"])
|
context["can_vote"] = user.can_vote(self.request, context["election"])
|
||||||
context["cast_questions"] = user.cast_questions.all()
|
context["cast_questions"] = user.cast_questions.all()
|
||||||
context["has_voted"] = user.cast_elections.filter(
|
context["has_voted"] = user.cast_elections.filter(
|
||||||
|
@ -444,7 +455,7 @@ class ElectionVotersView(NotArchivedMixin, DetailView):
|
||||||
election = context["election"]
|
election = context["election"]
|
||||||
voters = list(election.voters.all())
|
voters = list(election.voters.all())
|
||||||
|
|
||||||
if user.is_authenticated:
|
if user.is_authenticated and isinstance(user, User):
|
||||||
context["can_vote"] = user.can_vote(self.request, context["election"])
|
context["can_vote"] = user.can_vote(self.request, context["election"])
|
||||||
context["is_admin"] = user.is_admin(election)
|
context["is_admin"] = user.is_admin(election)
|
||||||
can_delete = (
|
can_delete = (
|
||||||
|
@ -476,6 +487,8 @@ class ElectionBallotsView(NotArchivedMixin, DetailView):
|
||||||
|
|
||||||
|
|
||||||
class VoteView(OpenElectionOnlyMixin, DetailView):
|
class VoteView(OpenElectionOnlyMixin, DetailView):
|
||||||
|
request: AuthenticatedRequest
|
||||||
|
|
||||||
model = Question
|
model = Question
|
||||||
|
|
||||||
def dispatch(self, request, *args, **kwargs):
|
def dispatch(self, request, *args, **kwargs):
|
||||||
|
|
3
pyproject.toml
Normal file
3
pyproject.toml
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
[tool.pyright]
|
||||||
|
reportIncompatibleMethodOverride = false
|
||||||
|
reportIncompatibleVariableOverride = false
|
|
@ -1,3 +1,5 @@
|
||||||
from .staticdefs import CONNECTION_METHODS
|
from .staticdefs import CONNECTION_METHODS
|
||||||
|
|
||||||
__all__ = [CONNECTION_METHODS]
|
__all__ = [
|
||||||
|
"CONNECTION_METHODS",
|
||||||
|
]
|
||||||
|
|
|
@ -1,10 +1,15 @@
|
||||||
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
from authens.backends import ENSCASBackend
|
from authens.backends import ENSCASBackend
|
||||||
|
|
||||||
from django.contrib.auth import get_user_model
|
|
||||||
from django.contrib.auth.backends import ModelBackend
|
from django.contrib.auth.backends import ModelBackend
|
||||||
from django.core.exceptions import PermissionDenied
|
from django.core.exceptions import PermissionDenied
|
||||||
|
|
||||||
User = get_user_model()
|
if TYPE_CHECKING:
|
||||||
|
from elections.typing import User
|
||||||
|
else:
|
||||||
|
from django.contrib.auth import get_user_model
|
||||||
|
User = get_user_model()
|
||||||
|
|
||||||
|
|
||||||
class CASBackend(ENSCASBackend):
|
class CASBackend(ENSCASBackend):
|
||||||
|
|
|
@ -1,11 +1,16 @@
|
||||||
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
from django import forms
|
from django import forms
|
||||||
from django.contrib.auth import authenticate
|
from django.contrib.auth import authenticate
|
||||||
from django.contrib.auth import forms as auth_forms
|
from django.contrib.auth import forms as auth_forms
|
||||||
from django.contrib.auth import get_user_model
|
|
||||||
from django.core.validators import validate_email
|
from django.core.validators import validate_email
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
|
||||||
User = get_user_model()
|
if TYPE_CHECKING:
|
||||||
|
from elections.typing import User
|
||||||
|
else:
|
||||||
|
from django.contrib.auth import get_user_model
|
||||||
|
User = get_user_model()
|
||||||
|
|
||||||
|
|
||||||
class ElectionAuthForm(forms.Form):
|
class ElectionAuthForm(forms.Form):
|
||||||
|
|
|
@ -4,12 +4,10 @@ import random
|
||||||
# Fonctions universelles
|
# Fonctions universelles
|
||||||
# #############################################################################
|
# #############################################################################
|
||||||
|
|
||||||
|
alphabet = "abcdefghjkmnopqrstuvwxyzABCDEFGHJKLMNPQRSTUVWXYZ23456789"
|
||||||
|
|
||||||
|
|
||||||
def generate_password(size=15):
|
def generate_password(size=15):
|
||||||
random.seed()
|
random.seed()
|
||||||
alphabet = "abcdefghjkmnopqrstuvwxyzABCDEFGHJKLMNPQRSTUVWXYZ23456789"
|
|
||||||
password = ""
|
|
||||||
for i in range(size):
|
|
||||||
password += random.choice(alphabet)
|
|
||||||
|
|
||||||
return password
|
return "".join(random.choice(alphabet) for _ in range(size))
|
||||||
|
|
|
@ -1,17 +1,25 @@
|
||||||
from django.contrib.auth import get_user_model
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
from django.contrib.auth import views as auth_views
|
from django.contrib.auth import views as auth_views
|
||||||
from django.contrib.auth.hashers import make_password
|
from django.contrib.auth.hashers import make_password
|
||||||
from django.contrib.auth.mixins import LoginRequiredMixin, UserPassesTestMixin
|
from django.contrib.auth.mixins import LoginRequiredMixin, UserPassesTestMixin
|
||||||
from django.contrib.auth.models import Permission
|
from django.contrib.auth.models import Permission
|
||||||
from django.contrib.messages.views import SuccessMessageMixin
|
from django.contrib.messages.views import SuccessMessageMixin
|
||||||
|
from django.db.models import QuerySet
|
||||||
from django.urls import reverse, reverse_lazy
|
from django.urls import reverse, reverse_lazy
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
from django.views.generic import CreateView, FormView, ListView, TemplateView
|
from django.views.generic import CreateView, FormView, ListView, TemplateView
|
||||||
|
|
||||||
|
from elections.typing import AuthenticatedRequest
|
||||||
|
|
||||||
from .forms import ElectionAuthForm, PwdUserForm, UserAdminForm
|
from .forms import ElectionAuthForm, PwdUserForm, UserAdminForm
|
||||||
from .utils import generate_password
|
from .utils import generate_password
|
||||||
|
|
||||||
User = get_user_model()
|
if TYPE_CHECKING:
|
||||||
|
from elections.typing import User
|
||||||
|
else:
|
||||||
|
from django.contrib.auth import get_user_model
|
||||||
|
User = get_user_model()
|
||||||
|
|
||||||
|
|
||||||
# #############################################################################
|
# #############################################################################
|
||||||
|
@ -25,6 +33,8 @@ class StaffMemberMixin(UserPassesTestMixin):
|
||||||
n'est pas connectée, renvoie sur la page d'authentification
|
n'est pas connectée, renvoie sur la page d'authentification
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
request: AuthenticatedRequest
|
||||||
|
|
||||||
def test_func(self):
|
def test_func(self):
|
||||||
return self.request.user.is_active and self.request.user.is_staff
|
return self.request.user.is_active and self.request.user.is_staff
|
||||||
|
|
||||||
|
@ -85,7 +95,7 @@ class AccountListView(StaffMemberMixin, ListView):
|
||||||
|
|
||||||
def get_context_data(self, **kwargs):
|
def get_context_data(self, **kwargs):
|
||||||
ctx = super().get_context_data(**kwargs)
|
ctx = super().get_context_data(**kwargs)
|
||||||
qs = self.get_queryset()
|
qs: QuerySet = self.get_queryset() # pyright: ignore
|
||||||
|
|
||||||
ctx["cas_users"] = qs.filter(username__startswith="cas__")
|
ctx["cas_users"] = qs.filter(username__startswith="cas__")
|
||||||
ctx["pwd_users"] = qs.filter(username__startswith="pwd__")
|
ctx["pwd_users"] = qs.filter(username__startswith="pwd__")
|
||||||
|
@ -143,16 +153,16 @@ class PermissionManagementView(StaffMemberMixin, SuccessMessageMixin, FormView):
|
||||||
# Election admin
|
# Election admin
|
||||||
election_perm = Permission.objects.get(codename="election_admin")
|
election_perm = Permission.objects.get(codename="election_admin")
|
||||||
if form.cleaned_data["election_admin"]:
|
if form.cleaned_data["election_admin"]:
|
||||||
election_perm.user_set.add(user)
|
election_perm.user_set.add(user) # pyright: ignore
|
||||||
else:
|
else:
|
||||||
election_perm.user_set.remove(user)
|
election_perm.user_set.remove(user) # pyright: ignore
|
||||||
|
|
||||||
# FAQ admin
|
# FAQ admin
|
||||||
faq_perm = Permission.objects.get(codename="faq_admin")
|
faq_perm = Permission.objects.get(codename="faq_admin")
|
||||||
if form.cleaned_data["faq_admin"]:
|
if form.cleaned_data["faq_admin"]:
|
||||||
faq_perm.user_set.add(user)
|
faq_perm.user_set.add(user) # pyright: ignore
|
||||||
else:
|
else:
|
||||||
faq_perm.user_set.remove(user)
|
faq_perm.user_set.remove(user) # pyright: ignore
|
||||||
|
|
||||||
user.save()
|
user.save()
|
||||||
return super().form_valid(form)
|
return super().form_valid(form)
|
||||||
|
|
|
@ -1,4 +1,7 @@
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
from django.http import JsonResponse
|
from django.http import JsonResponse
|
||||||
|
from django.template.response import TemplateResponse
|
||||||
from django.utils.decorators import method_decorator
|
from django.utils.decorators import method_decorator
|
||||||
from django.views.decorators.http import require_POST
|
from django.views.decorators.http import require_POST
|
||||||
from django.views.generic.base import TemplateResponseMixin, View
|
from django.views.generic.base import TemplateResponseMixin, View
|
||||||
|
@ -48,10 +51,17 @@ class JsonMessageMixin:
|
||||||
|
|
||||||
def get_data(self, **kwargs):
|
def get_data(self, **kwargs):
|
||||||
kwargs.update(message=self.get_message())
|
kwargs.update(message=self.get_message())
|
||||||
return super().get_data(**kwargs)
|
return super().get_data(**kwargs) # pyright: ignore
|
||||||
|
|
||||||
|
|
||||||
class JsonDetailView(JsonMixin, SingleObjectMixin, TemplateResponseMixin, View):
|
class TypedResponseMixin(TemplateResponseMixin):
|
||||||
|
def render_to_response(
|
||||||
|
self, context: dict[str, Any], **response_kwargs: Any
|
||||||
|
) -> TemplateResponse:
|
||||||
|
return super().render_to_response(context, **response_kwargs) # pyright: ignore
|
||||||
|
|
||||||
|
|
||||||
|
class JsonDetailView(JsonMixin, SingleObjectMixin, TypedResponseMixin, View):
|
||||||
def get(self, request, *args, **kwargs):
|
def get(self, request, *args, **kwargs):
|
||||||
self.object = self.get_object()
|
self.object = self.get_object()
|
||||||
context = self.get_context_data(object=self.object)
|
context = self.get_context_data(object=self.object)
|
||||||
|
@ -69,7 +79,7 @@ class JsonDeleteView(JsonMessageMixin, JsonDetailView):
|
||||||
|
|
||||||
@method_decorator(require_POST, name="dispatch")
|
@method_decorator(require_POST, name="dispatch")
|
||||||
class JsonCreateView(
|
class JsonCreateView(
|
||||||
JsonMessageMixin, JsonModelFormMixin, TemplateResponseMixin, ProcessFormView
|
JsonMessageMixin, JsonModelFormMixin, TypedResponseMixin, ProcessFormView
|
||||||
):
|
):
|
||||||
def render_to_json(self, **kwargs):
|
def render_to_json(self, **kwargs):
|
||||||
context = self.get_context_data(object=self.object)
|
context = self.get_context_data(object=self.object)
|
||||||
|
@ -81,7 +91,7 @@ class JsonCreateView(
|
||||||
|
|
||||||
@method_decorator(require_POST, name="dispatch")
|
@method_decorator(require_POST, name="dispatch")
|
||||||
class JsonUpdateView(
|
class JsonUpdateView(
|
||||||
JsonMessageMixin, JsonModelFormMixin, TemplateResponseMixin, ProcessFormView
|
JsonMessageMixin, JsonModelFormMixin, TypedResponseMixin, ProcessFormView
|
||||||
):
|
):
|
||||||
def post(self, request, *args, **kwargs):
|
def post(self, request, *args, **kwargs):
|
||||||
self.object = self.get_object()
|
self.object = self.get_object()
|
||||||
|
|
|
@ -1,9 +1,14 @@
|
||||||
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
from django.contrib.auth import get_user_model
|
from django.contrib.auth import get_user_model
|
||||||
from django.contrib.auth.hashers import make_password
|
from django.contrib.auth.hashers import make_password
|
||||||
from django.contrib.auth.models import Permission
|
from django.contrib.auth.models import Permission
|
||||||
from django.core.management.base import BaseCommand, CommandError
|
from django.core.management.base import BaseCommand, CommandError
|
||||||
|
|
||||||
User = get_user_model()
|
if TYPE_CHECKING:
|
||||||
|
from elections.typing import User
|
||||||
|
else:
|
||||||
|
User = get_user_model()
|
||||||
|
|
||||||
|
|
||||||
class Command(BaseCommand):
|
class Command(BaseCommand):
|
||||||
|
@ -38,5 +43,11 @@ class Command(BaseCommand):
|
||||||
|
|
||||||
user.save()
|
user.save()
|
||||||
|
|
||||||
Permission.objects.get(codename="election_admin").user_set.add(user)
|
Permission.objects.get(
|
||||||
Permission.objects.get(codename="faq_admin").user_set.add(user)
|
codename="election_admin"
|
||||||
|
).user_set.add( # pyright: ignore
|
||||||
|
user
|
||||||
|
)
|
||||||
|
Permission.objects.get(codename="faq_admin").user_set.add( # pyright: ignore
|
||||||
|
user
|
||||||
|
)
|
||||||
|
|
|
@ -23,4 +23,4 @@ class BackgroundUpdateView(RedirectView):
|
||||||
class TimeMixin:
|
class TimeMixin:
|
||||||
def get_context_data(self, **kwargs):
|
def get_context_data(self, **kwargs):
|
||||||
kwargs.update(current_time=timezone.now())
|
kwargs.update(current_time=timezone.now())
|
||||||
return super().get_context_data(**kwargs)
|
return super().get_context_data(**kwargs) # pyright: ignore
|
||||||
|
|
Loading…
Reference in a new issue