Search code examples
djangodjango-modelsdjango-rest-frameworkdjango-viewsdjango-rest-viewsets

Use serializer of model having foreign key to do CRUD on parent table in Django Rest Framework


In my API, I have two models Question and Option as shown below

class Question(models.Model):
    body = models.TextField()


class Options(models.Model):
    question = models.ForeignKey(Question, on_delete=models.CASCADE)
    option = models.CharField(max_length=100)
    is_correct = models.SmallIntegerField()

While creating a question it would be nicer the options can be created at the same time. And already existed question should not be created but the options can be changed if the options are different from previous.
I am using ModelSerializer and ModelViewSet. I use different urls and views for Question and Option.

serializers.py

class QuestionSerializer(serializers.ModelSerializer):
    class Meta:
        model = Question
        fields = '__all__'


class OptionReadSerializer(serializers.ModelSerializer):
    question = QuestionSerializer(read_only=True)

    class Meta:
        model = Option
        fields = ('question', 'option', 'is_correct')


class OptionWriteSerializer(serializer.ModelSerializer):
    class Meta:
        model = Option
        fields = ('question', 'option', 'is_correct')

views.py

class QuestionViewSet(ModelViewSet):
    seriaizer_class = QuestionSerializer
    queryset = Question.objects.all()


class OptionViewSet(ModelViewSet):
    queryset = Option.objects.all()

    def get_serializer_class(self):
        if self.request.method == 'POST':
            return OptionWriteSerializer
        return OptionReadSerializer

urls.py

from django.urls import include
from rest_framework.routers import DefaultRouter

router = DefaultRouter()
router.register('api/question', QuestionViewset, base_name='question')
router.register('api/option', OptionViewSet, base_name='option')

urlpatterns = [
    path('', include(router.urls))
]

In this way, I always have to create questions first and then I can individually add the option for that question. I think this may not be a practical approach.
It would be nicer that question and option can be added at the same time and similar to all CRUD operations.

The expected result and posting data in JSON format are as shown below:

{
    "body": "Which country won the FIFA world cup 2018",
    "options": [
        {
            "option": "England",
            "is_correct": 0
        },
        {
            "option": "Germany",
            "is_correct": 0
        },
        {
            "option": "France",
            "is_correct": 1
        }
    ]
}

Solution

  • In models I added related_name='options' in foreign key field of Option model

    models.py

    class Question(models.Model):
        body = models.TextField()
    
    
    class Options(models.Model):
        question = models.ForeignKey(Question, on_delete=models.CASCADE, related_name='options')
        option = models.CharField(max_length=100)
        is_correct = models.SmallIntegerField()
    

    In QuestionWriteSerializer I override the update() and create() method. For creating and updating the logic was handled from QuestionWriteSerialzer.

    serializers.py

    class OptionSerializer(serializers.ModelSerializer):
        id = serializers.IntegerField(required=False)
    
        class Meta:
            model = Option
            fields = ('id', 'question', 'option', 'is_correct')
    
    
    class QuestionReadSerializer(serializers.ModelSerializer):
        options = OptionSerializer(many=True, read_only=True)
    
        class Meta:
            model = Question
            fields = ('id', 'body', 'options')
    
    
    class QuestionWriteSerializers(serializers.ModelSerializer):
        options = OptionSerializer(many=True)
    
        class Meta:
            model = Question
            fields = ('id', 'body', 'options')
    
        def create(self, validated_data):
            options_data = validated_data.pop('options')
            question_created = Questions.objects.update_or_create(**validated_data)
    
            option_query = Options.objects.filter(question=question_created[0])
            if len(option_query) > 1:
                for existeding_option in option_query:
                    option_query.delete()
    
            for option_data in options_data:
                Options.objects.create(question=question_created[0], **option_data)
    
            return question_created[0]
    
        def update(self, instance, validated_data):
            options = validated_data.pop('options')
            instance.body = validated_data.get('body', instance.body)
            instance.save()
    
            keep_options = []
            for option_data in options:
                if 'id' in option_data.keys():
                    if Options.objects.filter(id=option_data['id'], question_id=instance.id).exists():
                        o = Options.objects.get(id=option_data['id'])
                        o.option = option_data.get('option', o.option)
                        o.is_correct = option_data.get('is_correct', o.is_correct)
                        o.save()
                        keep_options.append(o.id)
                    else:
                        continue
                else:
                    o = Options.objects.create(**option_data, question=instance)
                    keep_options.append(o.id)
    
            for option_data in instance.options.all():
                if option_data.id not in keep_options:
                    Options.objects.filter(id=option_data.id).delete()
    
            return instance
    

    The QuestionViewSet is almost the same and I removed the OptionViewSet and controlled all things from QuestionViewSet

    views.py

    class QuestionViewSet(ModelViewSet):
        queryset = Question.objects.all()
    
        def get_serializer_class(self) or self.request.method == 'PUT' or self.request.method == 'PATCH':
            if self.request.method == 'POST':
                return QuestionWriteSerializer
            return QuestionReadSerializer
    
        def create(self, request, *args, **kwargs):
            """
            Overriding create() method to change response format
            """
            serializer = self.get_serializer(data=request.data)
            if serializer.is_valid():
                self.perform_create(serializer)
                headers = self.get_success_headers(serializer.data)
                return Response({
                    'message': 'Successfully created question',
                    'data': serializer.data,
                    'status': 'HTTP_201_CREATED',
                }, status=status.HTTP_201_CREATED, headers=headers)
            else:
                return Response({
                    'message': 'Can not create',
                    'data': serializer.errors,
                    'status': 'HT',
                }, status=status.HTTP_400_BAD_REQUEST)