2

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
        }
    ]
}
dipesh
  • 763
  • 2
  • 9
  • 27

2 Answers2

2

We can use PrimaryKeyRelatedField.

tldr;

I believe a Question can have multiple Options attached to it. Rather than having an Option hooked to a Question.

Something like this:

class Question(models.Model):
    body = models.TextField()
    options = models.ManyToManyField(Option)

class Options(models.Model):
    text = models.CharField(max_length=100)
    is_correct = models.BooleanField()

Then we can use PrimaryKeyRelatedField something like this:

class QuestionSerializer(serializers.ModelSerializer):
    options = serializers.PrimaryKeyRelatedField(queryset=Options.objects.all(), many=True, read_only=False)
    class Meta:
        model = Question
        fields = '__all__'

Reference : https://www.django-rest-framework.org/api-guide/relations/#primarykeyrelatedfield

Umair Mohammad
  • 4,489
  • 2
  • 20
  • 34
  • 2
    You give me some logic to continue. I don't want to change the model. The idea I used to achieve the solution, I will answer in the below section. – dipesh Aug 06 '19 at 08:41
1

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)
dipesh
  • 763
  • 2
  • 9
  • 27