Reputation: 83527
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
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
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
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