Code-Apprentice
Code-Apprentice

Reputation: 83527

Create related field if it does not already exist

I have the following two Django model classes (names have been changed to protect the not-so-innocent):

class Foo(models.Model)
    foo = models.CharField(max_length=25)


class Bar(models.Model):
    foo = models.ForeignKey(Foo)

And Django Rest Framework serializers for each model:

class FooSerializer(serializers.ModelSerializer):
    class Meta:
        model = Foo
        fields = ('foo',)


class BarSerializer(serializers.ModelSerializer):
    class Meta:
        model = Bar
        fields = ('foo',)

Routes:

urlpatterns = [
    url(r'^foo/', ModelListView.as_view(model_class=Foo, serializer_class=FooSerializer), name='foo'),
    url(r'^bar/', ModelListView.as_view(model_class=Bar, serializer_class=BarSerializer), name='Bar'),
]

Views:

class ModelListView(ListCreateAPIView):
    model_class = None
    serializer_class = None

    def create(self, request, *args, **kwargs):
        data = JSONParser().parse(request)
        serializer = self.serializer_class(data=data, many=True)
        if serializer.is_valid():
            serializer.save()
            return Response(serializer.data, status=HTTP_201_CREATED)
        return Response(serializer.errors, status=HTTP_400_BAD_REQUEST)

To illustrate usage of the /bar route, the Bar model and its serializer, I wrote a test. I want to be able to specify the string value of the field foo. The POST request should use an existing instance of Foo if one already exists. Otherwise it should create it.

class BarTest(UnitTest):
    def setUp(self):
        self.model_class = Bar
        self.fields = [{'foo': 'foo1'},
                       {'foo': 'foo2'}]
        self.url = reverse('bar')

    def test_post(self):
        data = JSONRenderer().render(self.fields)
        response = self.client.post(self.url, data=data, content_type='application/json')
        self.assertEqual(HTTP_201_CREATED, response.status_code)
        result = response.json()
        self.assertEqual(self.fields, result)

I read Serializer Relations in the Django REST Framework documentation. It seems that either StringRelatedField or SlugRelatedField might fit my use case. I don't understand the difference between these two nor how to use them to get the behavior I want. Will either of these work for these purposes? If so, how do I use them? If not, what are alternative solutions?

Upvotes: 2

Views: 1603

Answers (3)

Code-Apprentice
Code-Apprentice

Reputation: 83527

From @dukebody's answer, I learned about Model.objects.get_or_create(). Some further research into serializers revealed that I can override to_internal_value().

class BarSerializer(serializers.ModelSerializer):
    class Meta:
        model = Bar
        fields = ('foo',)

    def to_internal_value(self, data):
        Foo.objects.get_or_create(foo=data['foo'])
        return super().to_internal_value(data)

Clearly my code in my original question is contrived. In the real project, the Bar counterpart has several more fields, so this solution leverages ModelSerializer to manage the primitive fields.

Upvotes: 3

AviKKi
AviKKi

Reputation: 1204

You can also modify the SlugRelatedField by inheriting it as shown below

class SlugRelatedGetOrCreateField(serializers.SlugRelatedField):
    def to_internal_value(self, data):
        queryset = self.get_queryset()
        try:
            return queryset.get_or_create(**{self.slug_field: data})[0]
        except (TypeError, ValueError):
            self.fail("invalid")

I disagree with @dukebody 's views that automatic serializers can be a bit too rigid

If you can browse around the code of DRF there are plenty of methods that can be overridden to achieve any desirable result.

Upvotes: 5

dukebody
dukebody

Reputation: 7185

From my personal experience I find that many times using the generic DRF views and automatic serializers can be a bit too rigid. If you use SlugRelatedField it will try to find a Foo item with the given parameters, and fail if it doesn't exist yet.

By the way, the difference between StringRelatedField and SlugRelatedField is that the former is read only and always returns the string representation of the object, while the later is more flexible and can be used for writing as well.

To achieve what you want you can try something like the following:

from rest_framework.generics import ListCreateAPIView

# don't need to subclass from ModelSerializer because we won't use
# its `create`/`update` method or automatic field generation
class CreateBarSerializer(serializers.Serializer):
    foo = serializers.CharField()


class BarListView(ListCreateAPIView):
    model_class = Bar
    serializer_class = BarSerializer

    def create(self, request, *args, **kwargs):
        data = JSONParser().parse(request)
        serializer = CreateBarSerializer(data=data, many=True)
        if serializer.is_valid():
            validated_data = serializer.validated_data
            # create a Foo with given name if it doesn't exist yet
            foo = Foo.objects.get_or_create(
                foo=validated_data['foo']
            )
            Bar.objects.create(foo=foo)
            return Response(serializer.data, status=HTTP_201_CREATED)
        return Response(serializer.errors, status=HTTP_400_BAD_REQUEST)

Upvotes: 2

Related Questions