feat(elections): Pseudonimize tallied elections
This commit is contained in:
parent
3a0d499ba1
commit
fb0e5a8a37
4 changed files with 104 additions and 6 deletions
|
@ -0,0 +1,25 @@
|
||||||
|
# Generated by Django 4.2.12 on 2024-07-11 12:01
|
||||||
|
|
||||||
|
from django.conf import settings
|
||||||
|
from django.db import migrations, models
|
||||||
|
import django.db.models.deletion
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('elections', '0033_inactive_users'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='vote',
|
||||||
|
name='pseudonymous_user',
|
||||||
|
field=models.CharField(blank=True, max_length=16),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='vote',
|
||||||
|
name='user',
|
||||||
|
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL),
|
||||||
|
),
|
||||||
|
]
|
44
elections/migrations/0035_auto_20240711_1424.py
Normal file
44
elections/migrations/0035_auto_20240711_1424.py
Normal file
|
@ -0,0 +1,44 @@
|
||||||
|
# Generated by Django 4.2.12 on 2024-07-11 12:24
|
||||||
|
|
||||||
|
import random
|
||||||
|
|
||||||
|
from django.db import migrations
|
||||||
|
|
||||||
|
alphabet = "abcdefghjkmnopqrstuvwxyzABCDEFGHJKLMNPQRSTUVWXYZ23456789"
|
||||||
|
|
||||||
|
|
||||||
|
def generate_password(size):
|
||||||
|
random.seed()
|
||||||
|
|
||||||
|
return "".join(random.choice(alphabet) for _ in range(size))
|
||||||
|
|
||||||
|
|
||||||
|
def pseudonymize_users(apps, _):
|
||||||
|
Question = apps.get_model("elections", "Question")
|
||||||
|
Vote = apps.get_model("elections", "Vote")
|
||||||
|
|
||||||
|
votes = set()
|
||||||
|
|
||||||
|
for q in Question.objects.filter(election__tallied=True).prefetch_related(
|
||||||
|
"options__vote_set"
|
||||||
|
):
|
||||||
|
|
||||||
|
for v in q.voters.all():
|
||||||
|
pseudonym = generate_password(16)
|
||||||
|
|
||||||
|
for opt in q.options.all():
|
||||||
|
for vote in opt.vote_set.filter(user=v):
|
||||||
|
vote.pseudonymous_user = pseudonym
|
||||||
|
vote.user = None
|
||||||
|
votes.add(vote)
|
||||||
|
|
||||||
|
Vote.objects.bulk_update(votes, ["pseudonymous_user", "user"])
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
("elections", "0034_vote_pseudonymous_user_alter_vote_user"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [migrations.RunPython(pseudonymize_users)]
|
|
@ -10,6 +10,7 @@ from django.template.loader import render_to_string
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
|
||||||
from shared.auth import CONNECTION_METHODS
|
from shared.auth import CONNECTION_METHODS
|
||||||
|
from shared.auth.utils import generate_password
|
||||||
from shared.json import Serializer
|
from shared.json import Serializer
|
||||||
from shared.utils import choices_length
|
from shared.utils import choices_length
|
||||||
|
|
||||||
|
@ -141,6 +142,26 @@ class Question(Serializer, models.Model):
|
||||||
tally_function = getattr(TallyFunctions, TALLY_FUNCTIONS[self.type])
|
tally_function = getattr(TallyFunctions, TALLY_FUNCTIONS[self.type])
|
||||||
tally_function(self)
|
tally_function(self)
|
||||||
|
|
||||||
|
@transaction.atomic
|
||||||
|
def pseudonymize(self):
|
||||||
|
"""
|
||||||
|
Generates a random id for each voter
|
||||||
|
"""
|
||||||
|
|
||||||
|
options = list(self.options.prefetch_related("vote_set"))
|
||||||
|
votes: set[Vote] = set()
|
||||||
|
|
||||||
|
for v in self.voters.all():
|
||||||
|
pseudonym = generate_password(16)
|
||||||
|
|
||||||
|
for opt in options:
|
||||||
|
for vote in opt.vote_set.filter(user=v):
|
||||||
|
vote.pseudonymous_user = pseudonym
|
||||||
|
vote.user = None
|
||||||
|
votes.add(vote)
|
||||||
|
|
||||||
|
Vote.objects.bulk_update(votes, ["pseudonymous_user", "user"])
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def results(self) -> str:
|
def results(self) -> str:
|
||||||
return render_to_string(
|
return render_to_string(
|
||||||
|
@ -172,6 +193,8 @@ class Question(Serializer, models.Model):
|
||||||
|
|
||||||
|
|
||||||
class Option(Serializer, models.Model):
|
class Option(Serializer, models.Model):
|
||||||
|
vote_set: models.Manager["Vote"]
|
||||||
|
|
||||||
question = models.ForeignKey(
|
question = models.ForeignKey(
|
||||||
Question, related_name="options", on_delete=models.CASCADE
|
Question, related_name="options", on_delete=models.CASCADE
|
||||||
)
|
)
|
||||||
|
@ -210,7 +233,10 @@ class Option(Serializer, models.Model):
|
||||||
|
|
||||||
class Vote(models.Model):
|
class Vote(models.Model):
|
||||||
option = models.ForeignKey(Option, on_delete=models.CASCADE)
|
option = models.ForeignKey(Option, on_delete=models.CASCADE)
|
||||||
user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE)
|
user = models.ForeignKey(
|
||||||
|
settings.AUTH_USER_MODEL, on_delete=models.CASCADE, null=True
|
||||||
|
)
|
||||||
|
pseudonymous_user = models.CharField(max_length=16, blank=True)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
ordering = ["option"]
|
ordering = ["option"]
|
||||||
|
@ -219,6 +245,9 @@ class Vote(models.Model):
|
||||||
class RankedVote(Vote):
|
class RankedVote(Vote):
|
||||||
rank: "Rank"
|
rank: "Rank"
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
abstract = True
|
||||||
|
|
||||||
|
|
||||||
class Rank(models.Model):
|
class Rank(models.Model):
|
||||||
vote = models.OneToOneField(Vote, on_delete=models.CASCADE)
|
vote = models.OneToOneField(Vote, on_delete=models.CASCADE)
|
||||||
|
|
|
@ -301,16 +301,16 @@ class BallotsData:
|
||||||
"""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
|
||||||
|
|
||||||
votes = Vote.objects.filter(option__question=question).select_related("user")
|
votes = Vote.objects.filter(option__question=question)
|
||||||
|
|
||||||
options = list(question.options.all())
|
options = list(question.options.all())
|
||||||
|
|
||||||
ballots = {}
|
ballots = {}
|
||||||
for v in votes:
|
for v in votes:
|
||||||
ballot = ballots.get(v.user, [False] * len(options))
|
ballot = ballots.get(v.pseudonymous_user, [False] * len(options))
|
||||||
ballot[options.index(v.option)] = True
|
ballot[options.index(v.option)] = True
|
||||||
|
|
||||||
ballots[v.user] = ballot
|
ballots[v.pseudonymous_user] = ballot
|
||||||
|
|
||||||
return render_to_string(
|
return render_to_string(
|
||||||
"elections/ballots/select.html",
|
"elections/ballots/select.html",
|
||||||
|
@ -323,13 +323,13 @@ class BallotsData:
|
||||||
from .models import Rank
|
from .models import Rank
|
||||||
|
|
||||||
options = list(question.options.all())
|
options = list(question.options.all())
|
||||||
ranks = Rank.objects.select_related("vote__user").filter(
|
ranks = Rank.objects.select_related("vote").filter(
|
||||||
vote__option__in=options
|
vote__option__in=options
|
||||||
)
|
)
|
||||||
ranks_by_user = {}
|
ranks_by_user = {}
|
||||||
|
|
||||||
for r in ranks:
|
for r in ranks:
|
||||||
user = r.vote.user
|
user = r.vote.pseudonymous_user
|
||||||
if user in ranks_by_user:
|
if user in ranks_by_user:
|
||||||
ranks_by_user[user].append(r.rank)
|
ranks_by_user[user].append(r.rank)
|
||||||
else:
|
else:
|
||||||
|
|
Loading…
Reference in a new issue