Repository: TabbycatDebate/tabbycat

Issue:
API: Incorrect POST of 'ms' type feedback questions.
Hi there! Another week, another API question/issue.

For the last two days, I've been unable to figure out what is happening. I'm trying to do some feedback POST and, so far so good, everything is fine and all answers are getting posted correctly, with the only exception of the 'ms' (multiple select) questions. No matter what, the strings posted are gettin splitted, word by word, with a ', ' in between each character. This only happens when the feedback is getting posted, not whenever it is manually introduced via the tabbycat interface. 

I think an image could be really telling:
[https://i.imgur.com/uQwMyN6.png](https://i.imgur.com/uQwMyN6.png)

The feedback on the right is the one manually introduced; the left one, the posted via api. The thing is that I've tried everything I've been able to think of, and the one on the left is literally a copy-paste of the right one, retrieved via a GET. 

This is the JSON for the question:
`{"question":"http://feedback-question-url.com","answer":"['OG', 'CG']"}`

As an aside, the answers are inside an array in the manually introduced one, but putting the brackets inside the answer causes to be displayed as well on the Tabbycat UI (as in the left one). Posting without them, joining them as a single string (as in `"answer":"OG, CG"`, which I think is actually the prefered syntaxis as per the API docs) prevent the brackets from appearing in the UI (but not the ', ' split).

So, just as a final clarification, originally I thought the problem was on my end and I've tried to debug my code with no success, so I decided to change course and I've also tried to debug this making the API request via Microsoft Edge's built-in network requests inside the DevTools. In both cases, the problem has appeared in exactly the same way, so that's when I decided to open the issue - I can be for sure the one making the POSTs in a wrong way, but after changing from my code to Edge I started thinking that maybe it can simply be a bug.


Discussion:
Did you try `"answer": ["OG", "CG"]`?
Yes! It throws an error and it is not accepted.

`{"answers":[{},{"answer":["Not a valid string."]},{}]}`

After putting it back to `"answer": "['OG', 'CG']"` it gets accepted again.
From the errors and so, this is my theory of what may be happening. 

As @tienne-B correctly suggested, the ms questions are an array of string options, and so are handled by Tabbycat. But when it comes to the API, the general rule is to only accept strings as answers, as all the other questions are or can be converted to strings - I assume this was to provide more uniformity and this is consistent with the schema, that states that answers must be strings. This also explains why for the `"answer": ["OG", "CG"]` post it throws a `not a valid string` error - because it is in fact not a string and only those are accepted. So how this may be handled? 

For the GET requests, as it is correctly being stored as an array but it have to be a string, it just creates a string with the saved array. Hence why the retrieved answer is `"answer":"['OG', 'CG']"` - a string of an array. 

And for the POST? The same path is taken, but on the opposite way. As it recieves a string, it splits it with split(', ') or similar, with the expectation for it to be ['string', 'string']. But as it is an entire string in and on itself, every single character is splitted with that comma and space. And that's why the app recieves the incorrect answers in that way.

That's my theory. The problem? I've been taking a dive on the api code and I've not seen any join or split around the feedback question classes, so I don't know how the implementation was and if this is indeed what it's happening. 
If you want to see where it's happening: https://github.com/TabbycatDebate/tabbycat/blob/50bc7ed9afda894aaef1c9d50b6d9fd2e3baa008/tabbycat/api/serializers.py#L968-L974

The `answer` field should be polymorphic rather than a `CharField`. Then we wouldn't need to cast to other datatypes (like `list("ABC")` returns `['A', 'B', 'C']`.

Resolving patch:
diff --git a/tabbycat/api/fields.py b/tabbycat/api/fields.py
--- a/tabbycat/api/fields.py
+++ b/tabbycat/api/fields.py
@@ -4,9 +4,11 @@
 from django.db.models import Q
 from django.urls import get_script_prefix, resolve, Resolver404
 from django.utils.encoding import uri_to_iri
+from drf_spectacular.utils import extend_schema_field
 from rest_framework.relations import Hyperlink, HyperlinkedIdentityField, HyperlinkedRelatedField, SlugRelatedField
 from rest_framework.reverse import reverse
-from rest_framework.serializers import CharField
+from rest_framework.serializers import CharField, Field
+
 
 from participants.models import Adjudicator, Speaker, Team
 from venues.models import Venue
@@ -280,3 +282,12 @@ def get_url_options(self, value, format):
             obj = getattr(value.participant_submitter, model.__name__.lower(), None)
             if obj is not None:
                 return self.get_url(obj, view_name, self.context['request'], format)
+
+
+@extend_schema_field({'anyOf': [{"type": "number"}, {"type": "boolean"}, {"type": "string"}, {"type": "array", "items": {"type": "string"}}]})
+class AnyField(Field):
+    def to_representation(self, value):
+        return value
+
+    def to_internal_value(self, data):
+        return data
diff --git a/tabbycat/api/serializers.py b/tabbycat/api/serializers.py
--- a/tabbycat/api/serializers.py
+++ b/tabbycat/api/serializers.py
@@ -966,12 +966,25 @@ class FeedbackAnswerSerializer(serializers.Serializer):
             view_name='api-feedbackquestion-detail',
             queryset=AdjudicatorFeedbackQuestion.objects.all(),
         )
-        answer = serializers.CharField()
+        answer = fields.AnyField()
 
         def validate(self, data):
             # Convert answer to correct type
             model = AdjudicatorFeedbackQuestion.ANSWER_TYPE_CLASSES[data['question'].answer_type]
+            if type(data['answer']) != model.ANSWER_TYPE:
+                raise serializers.ValidationError({'answer': 'The answer must be of type %s' % model.ANSWER_TYPE.__name__})
+
             data['answer'] = model.ANSWER_TYPE(data['answer'])
+
+            option_error = serializers.ValidationError({'answer': 'Answer must be in set of options'})
+            if len(data['question'].choices) > 0:
+                if model.ANSWER_TYPE is list and len(set(data['answer']) - set(data['question'].choices)) > 0:
+                    raise option_error
+                if data['answer'] not in data['question'].choices:
+                    raise option_error
+            if (data['question'].min_value is not None and data['answer'] < data['question'].min_value) or (data['question'].max_value is not None and data['answer'] > data['question'].max_value):
+                raise option_error
+
             return super().validate(data)
 
     url = fields.AdjudicatorFeedbackIdentityField(view_name='api-feedback-detail')
@@ -992,9 +1005,15 @@ def validate(self, data):
         source = data.pop('source')
         debate = data.pop('debate')
 
-        # Test answers for correct source
         source_type = 'from_team' if isinstance(source, Team) else 'from_adj'
-        for answer in data.get('get_answers'):
+        required_questions = self.context['tournament'].adjudicatorfeedbackquestion_set.filter(required=True, **{source_type: True})
+        answers = data.get('get_answers', [])
+
+        if len(set(required_questions) - set(a['question'] for a in answers)) > 0:
+            raise serializers.ValidationError("Answer to required question is missing")
+
+        # Test answers for correct source
+        for answer in answers:
             if not getattr(answer['question'], source_type, False):
                 raise serializers.ValidationError("Question is not permitted from source.")
 


Test patch: