112 lines
5.8 KiB
Python
112 lines
5.8 KiB
Python
"""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'])])
|