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 shared.auth import CONNECTION_METHODS
|
||||
from shared.auth.utils import generate_password
|
||||
from shared.json import Serializer
|
||||
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(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
|
||||
def results(self) -> str:
|
||||
return render_to_string(
|
||||
|
@ -172,6 +193,8 @@ class Question(Serializer, models.Model):
|
|||
|
||||
|
||||
class Option(Serializer, models.Model):
|
||||
vote_set: models.Manager["Vote"]
|
||||
|
||||
question = models.ForeignKey(
|
||||
Question, related_name="options", on_delete=models.CASCADE
|
||||
)
|
||||
|
@ -210,7 +233,10 @@ class Option(Serializer, models.Model):
|
|||
|
||||
class Vote(models.Model):
|
||||
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:
|
||||
ordering = ["option"]
|
||||
|
@ -219,6 +245,9 @@ class Vote(models.Model):
|
|||
class RankedVote(Vote):
|
||||
rank: "Rank"
|
||||
|
||||
class Meta:
|
||||
abstract = True
|
||||
|
||||
|
||||
class Rank(models.Model):
|
||||
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"""
|
||||
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())
|
||||
|
||||
ballots = {}
|
||||
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
|
||||
|
||||
ballots[v.user] = ballot
|
||||
ballots[v.pseudonymous_user] = ballot
|
||||
|
||||
return render_to_string(
|
||||
"elections/ballots/select.html",
|
||||
|
@ -323,13 +323,13 @@ class BallotsData:
|
|||
from .models import Rank
|
||||
|
||||
options = list(question.options.all())
|
||||
ranks = Rank.objects.select_related("vote__user").filter(
|
||||
ranks = Rank.objects.select_related("vote").filter(
|
||||
vote__option__in=options
|
||||
)
|
||||
ranks_by_user = {}
|
||||
|
||||
for r in ranks:
|
||||
user = r.vote.user
|
||||
user = r.vote.pseudonymous_user
|
||||
if user in ranks_by_user:
|
||||
ranks_by_user[user].append(r.rank)
|
||||
else:
|
||||
|
|
Loading…
Reference in a new issue