from django.urls import reverse from django.utils.crypto import get_random_string from django.utils.translation import gettext as _ from django.utils.translation import ngettext from rest_framework import serializers from ..models import Poll __all__ = [ "PollSerializer", "EditPollSerializer", "NewPollSerializer", "PollChoiceSerializer", ] MAX_POLL_OPTIONS = 16 class PollSerializer(serializers.ModelSerializer): acl = serializers.SerializerMethodField() choices = serializers.SerializerMethodField() api = serializers.SerializerMethodField() url = serializers.SerializerMethodField() class Meta: model = Poll fields = [ "id", "poster_name", "posted_on", "length", "question", "allowed_choices", "allow_revotes", "votes", "is_public", "acl", "choices", "api", "url", ] def get_api(self, obj): return {"index": obj.get_api_url(), "votes": obj.get_votes_api_url()} def get_url(self, obj): return {"poster": self.get_poster_url(obj)} def get_poster_url(self, obj): if obj.poster_id: return reverse( "misago:user", kwargs={"slug": obj.poster_slug, "pk": obj.poster_id} ) def get_acl(self, obj): try: return obj.acl except AttributeError: return None def get_choices(self, obj): return obj.choices class EditPollSerializer(serializers.ModelSerializer): length = serializers.IntegerField(required=True, min_value=0, max_value=180) question = serializers.CharField(required=True, max_length=255) allowed_choices = serializers.IntegerField(required=True, min_value=1) choices = serializers.ListField(allow_empty=False, child=serializers.DictField()) class Meta: model = Poll fields = ["length", "question", "allowed_choices", "allow_revotes", "choices"] def validate_choices(self, choices): clean_choices = list(map(self.clean_choice, choices)) # generate hashes for added choices choices_map = {} for choice in self.instance.choices: choices_map[choice["hash"]] = choice final_choices = [] for choice in clean_choices: if choice["hash"] in choices_map: choices_map[choice["hash"]].update({"label": choice["label"]}) final_choices.append(choices_map[choice["hash"]]) else: choice.update({"hash": get_random_string(12), "votes": 0}) final_choices.append(choice) self.validate_choices_num(final_choices) return final_choices def clean_choice(self, choice): clean_choice = { "hash": choice.get("hash", get_random_string(12)), "label": choice.get("label", ""), } serializer = PollChoiceSerializer(data=clean_choice) if not serializer.is_valid(): raise serializers.ValidationError( _("One or more poll choices are invalid.") ) return serializer.data def validate_choices_num(self, choices): total_choices = len(choices) if total_choices < 2: raise serializers.ValidationError( _("You need to add at least two choices to a poll.") ) if total_choices > MAX_POLL_OPTIONS: # pylint: disable=line-too-long message = ngettext( "You can't add more than %(limit_value)s option to a single poll (added %(show_value)s).", "You can't add more than %(limit_value)s options to a single poll (added %(show_value)s).", MAX_POLL_OPTIONS, ) raise serializers.ValidationError( message % {"limit_value": MAX_POLL_OPTIONS, "show_value": total_choices} ) def validate(self, data): if data["allowed_choices"] > len(data["choices"]): raise serializers.ValidationError( _( "Number of allowed choices can't be " "greater than number of all choices." ) ) return data def update(self, instance, validated_data): if instance.choices: self.update_choices(instance, validated_data["choices"]) return super().update(instance, validated_data) def update_choices(self, instance, cleaned_choices): removed_hashes = [] final_hashes = [c["hash"] for c in cleaned_choices] for choice in instance.choices: if choice["hash"] not in final_hashes: instance.votes -= choice["votes"] removed_hashes.append(choice["hash"]) if removed_hashes: instance.pollvote_set.filter(choice_hash__in=removed_hashes).delete() class NewPollSerializer(EditPollSerializer): class Meta: model = Poll fields = [ "length", "question", "allowed_choices", "allow_revotes", "is_public", "choices", ] def validate_choices(self, choices): clean_choices = list(map(self.clean_choice, choices)) self.validate_choices_num(clean_choices) for choice in clean_choices: choice.update({"hash": get_random_string(12), "votes": 0}) return clean_choices class PollChoiceSerializer(serializers.Serializer): hash = serializers.CharField(required=True, min_length=12, max_length=12) label = serializers.CharField(required=True, max_length=255)