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

走远了吗. 提交于 2019-12-22 13:02:52

问题


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
        }
    ]
}

回答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)



回答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



来源:https://stackoverflow.com/questions/57353803/use-serializer-of-model-having-foreign-key-to-do-crud-on-parent-table-in-django

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!