Creating related objects in Django REST Framework

Polls apis to create related objects

Agenda

This post assumes that you have followed our first post of this series.

We will be creating the following apis in this post.

  • An api to create an associated choice along with a Question
  • An api to create multiple associated choices along with a Question
  • Api to create multiple questions, each question with multiple choices

Apis

Let’s start writing our apis.

Create a choice along with creating a question

The api for creating question is POST /api/polls/questions/.

Till now we used QuestionListPageSerializer for creating question, serializer looked like:

class QuestionListPageSerializer(serializers.Serializer):
    id = serializers.IntegerField(read_only=True)
    question_text = serializers.CharField(max_length=200)
    pub_date = serializers.DateTimeField()
    was_published_recently = serializers.BooleanField(read_only=True) # Serializer is smart enough to understand that was_published_recently is a method on Question

    def create(self, validated_data):
        return Question.objects.create(**validated_data)

POST handler for view looked like.

elif request.method == 'POST':
    serializer = QuestionListPageSerializer(data=request.data)
    if serializer.is_valid():
        question = serializer.save()
        return Response(QuestionDetailPageSerializer(question).data, status=status.HTTP_201_CREATED)
    return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)

You can see full code https://www.agiliq.com/blog/2019/04/drf-polls/#final-code

We now want to create a choice along with question. Add following attribute to QuestionListPageSerializer:

choice = ChoiceSerializer(write_only=True)

A DRF Serializer is a subclass of DRF Field. Similar to how a serializer can have a Field as an attribute, it can also have another Serializer as an attribute.

Modify create() of QuestionListPageSerializer to following:

def create(self, validated_data):
    choice_dict = validated_data['choice']
    question = Question.objects.create(**validated_data)
    choice_dict['question'] = question
    Choice.objects.create(**choice_dict)
    return question

Try to POST a question without a choice and the code would raise a 400 Bad Request.

POST a question with valid choice data and the api call would succeed.

Since QuestionListPageSerializer has a field ChoiceSerializer so a ChoiceSerializer representation needs to be sent as choice key in the POSTed data. Remember how we POSTed choice in the last post. We used a similar datastructure, i.e a dictionary containing choice_text.

Verify that the correct question and choice were created. Also verify that the choice was associated with the question.

In [2]: question = Question.objects.latest('pk')

In [3]: question.choice_set.all()
Out[3]: <QuerySet [<Choice: Fossil>]>

In [5]: choice = Choice.objects.latest('pk')

In [6]: choice
Out[6]: <Choice: Fossil>

In [7]: choice.question
Out[7]: <Question: Which company makes the most durable wallets?>

In [8]: choice.question == question
Out[8]: True

Try to post with invalid choice data, say without choice_text and api will respond with descriptive message.

The message is:

{
    "choice": {
        "choice_text": [
            "This field is required."
        ]
    }
}

In case you want to make choice as an optional thing during question creation, add a required=False keyword argument.

choice = ChoiceSerializer(write_only=True, required=False)

We will also need to adjust create() to deal with situation when choice isn’t present in POSTed data.

def create(self, validated_data):
    question = Question.objects.create(**validated_data)
    if 'choice' in validated_data:
        choice_dict = validated_data['choice']
        choice_dict['question'] = question
        Choice.objects.create(**choice_dict)
    return question

Create multiple choices with question

We want the POST questions api to allow creation of multiple choices along with the question.

We will remove field choice from QuestionListPageSerializer and instead add a choices field which looks like:

choices = ChoiceSerializer(many=True, write_only=True)

We will need to adjust create() code of QuestionListPageSerializer too.

def create(self, validated_data):
    choices = validated_data.pop('choices', [])
    question = Question.objects.create(**validated_data)
    for choice_dict in choices:
        choice_dict['question'] = question
        Choice.objects.create(**choice_dict)
    return question

Entire serializer would look like:

class QuestionListPageSerializer(serializers.Serializer):
    id = serializers.IntegerField(read_only=True)
    question_text = serializers.CharField(max_length=200)
    pub_date = serializers.DateTimeField()
    was_published_recently = serializers.BooleanField(read_only=True) # Serializer is smart enough to understand that was_published_recently is a method on Question
    choices = ChoiceSerializer(many=True, write_only=True)

    def create(self, validated_data):
        choices = validated_data.pop('choices', [])
        question = Question.objects.create(**validated_data)
        for choice_dict in choices:
            choice_dict['question'] = question
            Choice.objects.create(**choice_dict)
        return question

Let’s try POSTing a Question with two choices.

You should have noticed Status: 201 Created which suggests that the Question and Choices should have been created.

The response of this call should be looking like the following:

A question with id 21 was created. You can verify that the POSTed choices, i.e Fossil and Burberry, were associated with this question.

In [2]: q = Question.objects.get(id=21)

In [3]: q.choice_set.all()
Out[3]: <QuerySet [<Choice: Burberry>, <Choice: Fossil>]>

Let’s try posting with one invalid choice and one valid choice:

You should have noticed that the api gave a 400 Bad Request. The response content would look like:

If you notice the response content, choices is a list and the first dictionary of choices is empty. This means that first choice data is valid. The second dictionary has a message telling that choice_text is required, which suggests that second choice data is invalid.

Create multiple questions

We want to create multiple questions along with associated choices in a single api call.

We will need to create a urlpattern and an apiview to achieve this.

@api_view(['POST'])
def multiple_questions_view(request):
    serializer = QuestionListPageSerializer(many=True, data=request.data)
    if serializer.is_valid():
        questions = serializer.save()
        return Response(QuestionDetailPageSerializer(questions, many=True).data, status=status.HTTP_201_CREATED)
    return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)

Let’s keep the urlpattern as:

path('multiple-questions/', apiviews.multiple_questions_view, name='multiple_questions_view')

Let’s make a POST request.

The status code 201 suggests that our questions and choices were correctly created. You response should look like:

You can see the full code on GitHub

Stay tuned for next post of the series.

Thank you for reading the Agiliq blog. This article was written by Akshar on Apr 23, 2019 in APIdjangodrf .

You can subscribe ⚛ to our blog.

We love building amazing apps for web and mobile for our clients. If you are looking for development help, contact us today ✉.

Would you like to download 10+ free Django and Python books? Get them here