Compare commits

...

2 commits

6 changed files with 117 additions and 7 deletions

View file

@ -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),
),
]

View 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)]

View file

@ -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)

View file

@ -10,3 +10,11 @@ def send_election_mail(election_pk: int, subject: str, body: str, reply_to: str)
send_mail(election, subject, body, reply_to)
election.sent_mail = True
election.save(update_fields=["sent_mail"])
@background
def pseudonimize_election(election_pk: int):
election = Election.objects.get(pk=election_pk)
for q in election.questions.all():
q.pseudonymize()

View file

@ -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:

View file

@ -41,13 +41,14 @@ from .mixins import (
)
from .models import Election, Option, Question, Vote
from .staticdefs import MAIL_VOTE_DELETED, MAIL_VOTERS, QUESTION_TYPES, VOTE_RULES
from .tasks import send_election_mail
from .tasks import pseudonimize_election, send_election_mail
from .utils import create_users
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*
@ -299,6 +300,9 @@ class ElectionTallyView(ClosedElectionMixin, BackgroundUpdateView):
election.tallied = True
election.time_tallied = timezone.now()
election.save()
pseudonimize_election(election.pk)
return super().get(request, *args, **kwargs)