"""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'])])