Louis Mazin 4db822aebe big maj
2026-10-05 17:52:28 +02:00

112 lines
5.8 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""Règles des sondages. L'anonymat est appliqué avant toute sérialisation API."""
from rest_framework import serializers
from .models import Poll, PollOption
class PollSettingsSerializer(serializers.Serializer):
vote_mode = serializers.ChoiceField(choices=Poll.VoteMode.choices, default='single')
exact_choices = serializers.IntegerField(min_value=1, max_value=30, default=1)
is_anonymous = serializers.BooleanField(default=True)
# Compatibilité API : une ancienne app peut encore envoyer closes_at, mais il est ignoré.
closes_at = serializers.DateTimeField(allow_null=True, required=False, write_only=True)
options = serializers.ListField(child=serializers.CharField(max_length=240, trim_whitespace=True), min_length=2, max_length=30)
def validate(self, attrs):
attrs.pop('closes_at', None)
# DRF n'applique pas les valeurs par défaut imbriquées lors d'un PATCH.
existing = getattr(getattr(self.root, 'instance', None), 'poll', None)
defaults = dict(vote_mode='single', exact_choices=1, is_anonymous=True)
if existing:
defaults = {key: getattr(existing, key) for key in defaults}
defaults['options'] = [option.label for option in existing.options.all()]
attrs = {**defaults, **attrs}
if 'options' not in attrs:
raise serializers.ValidationError({'options': 'Ajoutez au moins deux réponses.'})
labels = [label.casefold() for label in attrs['options']]
if len(set(labels)) != len(labels):
raise serializers.ValidationError({'options': 'Chaque réponse proposée doit être différente.'})
if attrs['vote_mode'] == 'exact' and attrs['exact_choices'] > len(labels):
raise serializers.ValidationError({'exact_choices': 'Le nombre de choix ne peut pas dépasser le nombre de réponses proposées.'})
if attrs['vote_mode'] != 'exact':
attrs['exact_choices'] = 1
return attrs
def is_closed(poll):
return bool(poll.closed_at)
def can_vote(user, note):
recipients = list(note.recipients.all())
return bool(user.is_authenticated and user.is_active and
(any(person.pk == user.pk for person in recipients) if recipients else note.is_public))
def person(user):
if user is None:
return {'id': None, 'username': 'Compte supprimé', 'first_name': '', 'last_name': ''}
return {key: getattr(user, key) for key in ['id', 'username', 'first_name', 'last_name']}
def poll_representation(note, user):
if note.kind != 'poll':
return None
try:
poll = note.poll
except Poll.DoesNotExist:
return None
votes = list(poll.votes.all())
total = len(votes)
selections = {vote.pk: {option.pk for option in vote.options.all()} for vote in votes}
own = next((vote for vote in votes if vote.user_id == user.pk), None)
options = []
for option in poll.options.all():
selected = [vote for vote in votes if option.pk in selections[vote.pk]]
item = dict(id=option.pk, label=option.label, count=len(selected), percent=round(len(selected) * 100 / total, 1) if total else 0)
if not poll.is_anonymous:
item['voters'] = [person(vote.user) for vote in selected]
options.append(item)
return dict(vote_mode=poll.vote_mode, exact_choices=poll.exact_choices, is_anonymous=poll.is_anonymous,
closes_at=None, is_closed=is_closed(poll), is_locked=bool(poll.locked_at),
can_vote=can_vote(user, note) and not is_closed(poll), total_voters=total,
my_option_ids=sorted(selections[own.pk]) if own else [], options=options)
def validate_poll_note(instance, attrs):
kind = attrs.get('kind', instance.kind if instance else 'memo')
data = attrs.get('poll_settings')
poll = getattr(instance, 'poll', None) if instance else None
if kind == 'poll' and not poll and data is None:
raise serializers.ValidationError({'poll_settings': 'Renseignez les paramètres et les réponses du sondage.'})
if kind != 'poll' and data is not None:
raise serializers.ValidationError({'poll_settings': 'Ces paramètres sont réservés aux sondages.'})
if poll and poll.locked_at:
if kind != 'poll' or attrs.get('title', instance.title) != instance.title:
raise serializers.ValidationError('La question et le type sont figés après le premier vote.')
if attrs.get('is_public', instance.is_public) != instance.is_public:
raise serializers.ValidationError('La visibilité est figée après le premier vote.')
if 'recipients' in attrs and {u.pk for u in attrs['recipients']} != {u.pk for u in instance.recipients.all()}:
raise serializers.ValidationError('Les participants sont figés après le premier vote.')
if data is not None:
for key in ['vote_mode', 'exact_choices', 'is_anonymous']:
if data[key] != getattr(poll, key):
raise serializers.ValidationError('Les règles et l’anonymat sont figés après le premier vote.')
if data['options'] != [o.label for o in poll.options.all()]:
raise serializers.ValidationError('Les réponses proposées sont figées après le premier vote.')
def configure_poll(note, data):
if note.kind != 'poll':
Poll.objects.filter(note=note).delete()
return
if data is None:
return
values = {key: data[key] for key in ['vote_mode', 'exact_choices', 'is_anonymous']}
values['closes_at'] = None
poll, _ = Poll.objects.update_or_create(note=note, defaults=values)
current = [o.label for o in poll.options.all()]
if current != data['options']:
poll.options.all().delete()
PollOption.objects.bulk_create([PollOption(poll=poll, label=label, position=i) for i, label in enumerate(data['options'])])