Django REST Framework: creating hierarchical objects using URL arguments - python

I have a django-rest-framework REST API with hierarchical resources. I want to be able to create subobjects by POSTing to /v1/objects/<pk>/subobjects/ and have it automatically set the foreign key on the new subobject to the pk kwarg from the URL without having to put it in the payload. Currently, the serializer is causing a 400 error, because it expects the object foreign key to be in the payload, but it shouldn't be considered optional either. The URL of the subobjects is /v1/subobjects/<pk>/ (since the key of the parent isn't necessary to identify it), so it is still required if I want to PUT an existing resource.
Should I just make it so that you POST to /v1/subobjects/ with the parent in the payload to add subobjects, or is there a clean way to pass the pk kwarg from the URL to the serializer? I'm using HyperlinkedModelSerializer and ModelViewSet as my respective base classes. Is there some recommended way of doing this? So far the only idea I had was to completely re-implement the ViewSets and make a custom Serializer class whose get_default_fields() comes from a dictionary that is passed in from the ViewSet, populated by its kwargs. This seems quite involved for something that I would have thought is completely run-of-the-mill, so I can't help but think I'm missing something. Every REST API I've ever seen that has writable endpoints has this kind of URL-based argument inference, so the fact that django-rest-framework doesn't seem to be able to do it at all seems strange.

Make the parent object serializer field read_only. It's not optional but it's not coming from the request data either. Instead you pull the pk/slug from the URL in pre_save()...
# Assuming list and detail URLs like:
# /v1/objects/<parent_pk>/subobjects/
# /v1/objects/<parent_pk>/subobjects/<pk>/
def pre_save(self, obj):
parent = models.MainObject.objects.get(pk=self.kwargs['parent_pk'])
obj.parent = parent

Here's what I've done to solve it, although it would be nice if there was a more general way to do it, since it's such a common URL pattern. First I created a mixin for my ViewSets that redefined the create method:
class CreatePartialModelMixin(object):
def initial_instance(self, request):
return None
def create(self, request, *args, **kwargs):
instance = self.initial_instance(request)
serializer = self.get_serializer(
instance=instance, data=request.DATA, files=request.FILES,
partial=True)
if serializer.is_valid():
self.pre_save(serializer.object)
self.object = serializer.save(force_insert=True)
self.post_save(self.object, created=True)
headers = self.get_success_headers(serializer.data)
return Response(
serializer.data, status=status.HTTP_201_CREATED,
headers=headers)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
Mostly it is copied and pasted from CreateModelMixin, but it defines an initial_instance method that we can override in subclasses to provide a starting point for the serializer, which is set up to do a partial deserialization. Then I can do, for example,
class SubObjectViewSet(CreatePartialModelMixin, viewsets.ModelViewSet):
# ....
def initial_instance(self, request):
instance = models.SubObject(owner=request.user)
if 'pk' in self.kwargs:
parent = models.MainObject.objects.get(pk=self.kwargs['pk'])
instance.parent = parent
return instance
(I realize I don't actually need to do a .get on the pk to associate it on the model, but in my case I'm exposing the slug rather than the primary key in the public API)

If you're using ModelSerializer (which is implemented by HyperlinkedModelSerializer) it's as easy as implementing the restore_object() method:
class MySerializer(serializers.ModelSerializer):
def restore_object(self, attrs, instance=None):
if instance is None:
# If `instance` is `None`, it means we're creating
# a new object, so we set the `parent_id` field.
attrs['parent_id'] = self.context['view'].kwargs['parent_pk']
return super(MySerializer, self).restore_object(attrs, instance)
# ...
restore_object() is used to deserialize a dictionary of attributes into an object instance. ModelSerializer implements this method and creates/updates the instance for the model you specified in the Meta class. If the given instance is None it means the object still has to be created, so you just add the parent_id attribute on the attrs argument and call super().
So this way you don't have to specify a read-only field, or have a custom view/serializer.
More information:
http://www.django-rest-framework.org/api-guide/serializers#declaring-serializers

Maybe a bit late, but i guess this drf nested routers library could be helpful for that operation.

Related

do we need query set in CreateAPIView?

My question is quite straight forward. I'm actually not sure that queryset is required need for CreateAPIView or not.. ?
class CreateNotificationAPIView(generics.CreateAPIView):
"""This endpoint allows for creation of a notification"""
queryset = Notification.objects.all() #can we remove it, if we do so, will we face any issue in future ?
serializer_class = serializers.NotificationSerializer
No. The only HTTP method the CreateAPIView [drf-doc] offers is the POST method, and it implements this by making a call to the create method. The .create(…) method is implemented as [GitHub]:
def create(self, request, *args, **kwargs):
serializer = self.get_serializer(data=request.data)
serializer.is_valid(raise_exception=True)
self.perform_create(serializer)
headers = self.get_success_headers(serializer.data)
return Response(serializer.data, status=status.HTTP_201_CREATED, headers=headers)
These methods only work with the serializer, or with self.perform_create and self.get_success_headers that by default only work with the data of the serializer.
If you thus not override the methods of the CreateAPIView to use the queryset somehow, you can define a CreateAPIView without defining a queryset or override get_queryset.
according to REST_docs
queryset - The queryset that should be used for returning objects from
this view. Typically, you must either set this attribute, or override
the get_queryset() method. If you are overriding a view method, it is
important that you call get_queryset() instead of accessing this
property directly, as queryset will get evaluated once, and those
results will be cached for all subsequent requests.

Add object to ManyToMany field using CreateModelMixin.create()

I use mixins.CreateModelMixin.create to create object, but also I need to add request.user to m2m fields in it. So my idea is to catch the object from self.create() and than filally make obj.users.add(user). But CreateModelMixin return only responce. How can I get an object from .create? Is it a better way to add user? Can I user super (not good in it)? Thanks!
ADDED:
I can use perform_create() and catch object here, but it makes code bigger and repeat .create() mostly, so I don't think that is a right way.
ADDED:
Code I user now:
#action(detail=False, methods=['POST'], serializer_class=CompanyAdminSerializer)
def create_company(self, request):
user = self.request.user
if user.user_of_company.exists():
raise NotAcceptable(detail='Only one company allowed')
serializer = self.get_serializer(data=request.data)
serializer.is_valid(raise_exception=True)
company = serializer.save()
company.users.add(user)
company.admin_users.add(user)
return Response(serializer.data)
To catch the instance from create you will have to override the create method.
The simplest way would be to override the perform_create method.
.save() returns the instance of the created object. source
Your code will look like the following:
#Assuming you're using CreateAPIView
class New_Create(CreateAPIView):
def perform_create(self, serializer):
obj = serializer.save()
#Adding to M2M
obj.users.add(self.request.user)
DRF Serializers do not support M2M create/update out of the box.
EDIT:
I do not recommend overriding the create method. The perform_create method has been created to serve exactly this purpose. You can access the instance only after .save() has been called. So, after calling .save() on the serializer you can update the instance however you want. Two ways to access the instance are:
1) Use the object being returned by the .save method (as shown above)
2) You can use serializer.instance. (Again you can only access the instance after .save has been called. )

Django Rest Framework partial update

I'm trying to implement partial_update with Django Rest Framework but I need some clarification because I'm stuck.
Why do we need to specify partial=True?
In my understanding, we could easily update Demo object inside of partial_update method. What is the purpose of this?
What is inside of serialized variable?
What is inside of serialized variable in partial_update method? Is that a Demo object? What function is called behind the scenes?
How would one finish the implementation here?
Viewset
class DemoViewSet(viewsets.ModelViewSet):
serializer_class = DemoSerializer
def partial_update(self, request, pk=None):
serialized = DemoSerializer(request.user, data=request.data, partial=True)
return Response(status=status.HTTP_202_ACCEPTED)
Serializer
class DemoSerializer(serializers.ModelSerializer):
class Meta:
model = Demo
fields = '__all__'
def update(self, instance, validated_data):
print 'this - here'
demo = Demo.objects.get(pk=instance.id)
Demo.objects.filter(pk=instance.id)\
.update(**validated_data)
return demo
I when digging into the source code of rest_framework and got the following findings:
For question 1. Why do we need to specify partial=True?
This question is related to HTTP verbs.
PUT: The PUT method replaces all current representations of the target resource with the request payload.
PATCH: The PATCH method is used to apply partial modifications to a resource.
Generally speaking, partial is used to check whether the fields in the model is needed to do field validation when client submitting data to the view.
For example, we have a Book model like this, pls note both of the name and author_name fields are mandatory (not null & not blank).
class Book(models.Model):
name = models.CharField('name of the book', max_length=100)
author_name = models.CharField('the name of the author', max_length=50)
# Create a new instance for testing
Book.objects.create(name='Python in a nut shell', author_name='Alex Martelli')
For some scenarios, we may only need to update part of the fields in the model, e.g., we only need to update name field in the Book. So for this case, client will only submit the name field with new value to the view. The data submit from the client may look like this:
{"pk": 1, name: "PYTHON IN A NUT SHELL"}
But you may have notice that our model definition does not allow author_name to be blank. So we have to use partial_update instead of update. So the rest framework will not perform field validation check for the fields which is missing in the request data.
For testing purpose, you can create two views for both update and partial_update, and you will get more understanding what I just said.
Example:
views.py
from rest_framework.generics import GenericAPIView
from rest_framework.mixins import UpdateModelMixin
from rest_framework.viewsets import ModelViewSet
from rest_framework import serializers
class BookSerializer(serializers.ModelSerializer):
class Meta:
model = Book
class BookUpdateView(GenericAPIView, UpdateModelMixin):
'''
Book update API, need to submit both `name` and `author_name` fields
At the same time, or django will prevent to do update for field missing
'''
queryset = Book.objects.all()
serializer_class = BookSerializer
def put(self, request, *args, **kwargs):
return self.update(request, *args, **kwargs)
class BookPartialUpdateView(GenericAPIView, UpdateModelMixin):
'''
You just need to provide the field which is to be modified.
'''
queryset = Book.objects.all()
serializer_class = BookSerializer
def put(self, request, *args, **kwargs):
return self.partial_update(request, *args, **kwargs)
urls.py
urlpatterns = patterns('',
url(r'^book/update/(?P<pk>\d+)/$', BookUpdateView.as_view(), name='book_update'),
url(r'^book/update-partial/(?P<pk>\d+)/$', BookPartialUpdateView.as_view(), name='book_partial_update'),
)
Data to submit
{"pk": 1, name: "PYTHON IN A NUT SHELL"}
When you submit the above json to the /book/update/1/, you will got the following error with HTTP_STATUS_CODE=400:
{
"author_name": [
"This field is required."
]
}
But when you submit the above json to /book/update-partial/1/, you will got HTTP_STATUS_CODE=200 with following response,
{
"id": 1,
"name": "PYTHON IN A NUT SHELL",
"author_name": "Alex Martelli"
}
For question 2. What is inside of serialized variable?
serialized is a object wrapping the model instance as a serialisable object. and you can use this serialized to generate a plain JSON string with serialized.data .
For question 3. How would one finish the implementation here?
I think you can answer yourself when you have read the answer above, and you should have known when to use update and when to used partial_update.
If you still have any question, feel free to ask. I just read part of the source code of the rest framework, and may have not understand very deeply for some terms, and please point it out when it is wrong...
For partial update - PATCH http method
For full update - PUT http method
When doing an update with DRF, you are supposed to send request data that includes values for all (required) fields. This is at least the case when the request is via the PUT http method. From what I understand, you want to update one or at least not all model instance fields. In this case make a request with the PATCH http method. Django rest framework (DRF) will take care of it out of the box.
Example (with token auth):
curl -i -X PATCH -d '{"name":"my favorite banana"}' -H "Content-Type: application/json" -H 'Authorization: Token <some token>' http://localhost:8000/bananas/
So simple, just override init method of your serializer like that:
def __init__(self, *args, **kwargs):
kwargs['partial'] = True
super(DemoSerializer, self).__init__(*args, **kwargs)
Just a quick note as it seems that nobody has already pointed this out:
serialized = DemoSerializer(request.user, data=request.data, partial=True)
The first argument of DemoSerializer should be a Demo instance, not a user (at least if you use DRF 3.6.2 like me).
I don't know what you are trying to do, but this is a working example:
def partial_update(self, request, *args, **kwargs):
response_with_updated_instance = super(DemoViewSet, self).partial_update(request, *args, **kwargs)
Demo.objects.my_func(request.user, self.get_object())
return response_with_updated_instance
I do the partial update and then I do other things calling my_func and passing the current user and the demo instance already updated.
Hope this helps.
I had an issue where my multi-attribute/field validation in a rest_framework serializer was working with a POST /resources/ request but failing with a PATCH /resources/ request. It failed in the PATCH case because it was only looking for values in the supplied attrs dict and not falling back to values in self.instance. Adding a method get_attr_or_default to do that fallback seems to have worked:
class EmailSerializer(serializers.ModelSerializer):
def get_attr_or_default(self, attr, attrs, default=''):
"""Return the value of key ``attr`` in the dict ``attrs``; if that is
not present, return the value of the attribute ``attr`` in
``self.instance``; otherwise return ``default``.
"""
return attrs.get(attr, getattr(self.instance, attr, ''))
def validate(self, attrs):
"""Ensure that either a) there is a body or b) there is a valid template
reference and template context.
"""
existing_body = self.get_attr_or_default('body', attrs).strip()
if existing_body:
return attrs
template = self.get_attr_or_default('template', attrs)
templatecontext = self.get_attr_or_default('templatecontext', attrs)
if template and templatecontext:
try:
render_template(template.data, templatecontext)
return attrs
except TemplateRendererException as err:
raise serializers.ValidationError(str(err))
raise serializers.ValidationError(NO_BODY_OR_TEMPLATE_ERROR_MSG)
I don't know why, but for me, the only way to solve it was to override the validate method in the Serializer class.
Maybe it's related to the fact that I'm using MongoDB with Djongo
class DemoSerializer(serializers.ModelSerializer):
def validate(self, attrs):
self._kwargs["partial"] = True
return super().validate(attrs)
You forgot serializer.save()
You can finish it the following way . . .
class DemoViewSet(viewsets.ModelViewSet):
serializer_class = DemoSerializer
def partial_update(self, request, pk=None):
serializer = DemoSerializer(request.user, data=request.data, partial=True)
serializer.save()
serializer.is_valid(raise_exception=True)
return Response(serializer.data)
Also, you shouldn't need to override the update method in the serializer.

Strange ModelViewSet behaviour

I'm trying to serialize a MPTT tree model with DRF.
My code:
class SiteTreeCalc(serializers.Field):
def to_representation(self, value):
return value.exists() # return True if has children, False otherwise
class SiteTreeSerializer(serializers.ModelSerializer):
children = SiteTreeCalc()
class Meta:
model = SiteTree
fields = ('id', 'site', 'children')
depth = 1
class SiteTreeViewSet(viewsets.ModelViewSet):
#queryset = SiteTree.objects.all()
serializer_class = SiteTreeSerializer
def get_queryset(self):
if 'pk' not in self.kwargs:
# return first-level nodes
return SiteTree.objects.filter(level=0)
else:
# return all children of a given node
return SiteTree.objects.filter(parent__id=int(self.kwargs['pk']))
router = routers.DefaultRouter()
router.register(r'rest/sitetree', SiteTreeViewSet, "SiteTreeRoots")
router.register(r'rest/sitetree/(?P<tree_id>\d+)/$', SiteTreeViewSet, "SiteTreeChildren")
I have two issues with this code:
I have declared parameter "tree_id" in router registration. However, get_queryset says that parameter name is pk
The second filter never works (the one that should return children of given parent). DRF returns "detail": "Not found.". If I test that line in debugger, it naturally returns all children of the given parent.
I seem to be doing something wrong, but the code seems so obvious to me that I just can't see it.
Help - as always - very appreciated.
Turns out, I wanted to forget the convenient functionality of DefaultRouter the first chance I got.
The problem was that I wanted to create a ViewSet just like any other writable ViewSet, but this particular one was intended only for retrieving items. At least, that's what I intended. But DRF couldn't know that, so my problem #2 was a result of DRF actually checking that I'm returning ONE item with EXACTLY the same pk as was given in the URL.
A solution that works goes like this (as suggested in the DRF ViewSets documentation):
class SiteTreeViewSet(viewsets.ReadOnlyModelViewSet):
queryset = SiteTree.objects.filter(level=0)
serializer_class = SiteTreeSerializer
#detail_route()
def children(self, request, pk=None):
data = SiteTree.objects.filter(parent__id=int(pk))
data = self.get_serializer(data, many=True)
return Response(data.data)
This solution returns first-level items in default mode and also accepts /{pk}/children to return children of the given pk node. Naturally, default operations will still return just the pk node when provided with a /{pk}/ URL.
Router registration remains only the default one:
router.register(r'rest/sitetree', SiteTreeViewSet)
As for 1. you need to set lookup_url_kwarg (the named argument in the urls) on the viewset so it maps to the tree_id.
Note that routers do define the dynamic url part themselves.
As for 2. it's most of the time sending JOSN POST data with form content type. Ensure you are sending the right content type in your request's header.
Edit:
Daniel has the point for 1. With your current url patterns, there's no way to distinguish a detailed top node and a list for the child node.

How can I PUT/POST JSON data to a ListSerializer?

I'm reading about customizing multiple update here and I haven't figured out in what case the custom ListSerializer update method is called. I would like to update multiple objects at once, I'm not worried about multiple create or delete at the moment.
From the example in the docs:
# serializers.py
class BookListSerializer(serializers.ListSerializer):
def update(self, instance, validated_data):
# custom update logic
...
class BookSerializer(serializers.Serializer):
...
class Meta:
list_serializer_class = BookListSerializer
And my ViewSet
# api.py
class BookViewSet(ModelViewSet):
queryset = Book.objects.all()
serializer_class = BookSerializer
And my url setup using DefaultRouter
# urls.py
router = routers.DefaultRouter()
router.register(r'Book', BookViewSet)
urlpatterns = patterns('',
url(r'^api/', include(router.urls)),
...
So I have this set up using the DefaultRouter so that /api/Book/ will use the BookSerializer.
Is the general idea that if I POST/PUT/PATCH an array of JSON objects to /api/Book/ then the serializer should switch over to BookListSerializer?
I've tried POST/PUT/PATCH JSON data list to this /api/Book/ that looks like:
[ {id:1,title:thing1}, {id:2, title:thing2} ]
but it seems to still treat the data using BookSerializer instead of BookListSerializer. If I submit via POST I get Invalid data. Expected a dictionary, but got list. and if I submit via PATCH or PUT then I get a Method 'PATCH' not allowed error.
Question:
Do I have to adjust the allowed_methods of the DefaultRouter or the BookViewSet to allow POST/PATCH/PUT of lists? Are the generic views not set up to work with the ListSerializer?
I know I could write my own list deserializer for this, but I'm trying to stay up to date with the new features in DRF 3 and it looks like this should work but I'm just missing some convention or some option.
Django REST framework by default assumes that you are not dealing with bulk data creation, updates, or deletion. This is because 99% of people are not dealing with bulk data creation, and DRF leaves the other 1% to third-party libraries.
In Django REST framework 2.x and 3.x, a third party package exists for this.
Now, you are trying to do bulk creation but you are getting an error back that says
Invalid data. Expected a dictionary, but got list
This is because you are sending in a list of objects to create, instead of just sending in one. You can get around this a few ways, but the easiest is to just override get_serializer on your view to add the many=True flag to the serializer when it is a list.
def get_serializer(self, *args, **kwargs):
if "data" in kwargs:
data = kwargs["data"]
if isinstance(data, list):
kwargs["many"] = True
return super(MyViewSet, self).get_serializer(*args, **kwargs)
This will allow Django REST framework to know to automatically use the ListSerializer when creating objects in bulk. Now, for other operations such as updating and deleting, you are going to need to override the default routes. I'm going to assume that you are using the routes provided by Django REST framework bulk, but you are free to use whatever method names you want.
You are going to need to add methods for bulk PUT and PATCH to the view as well.
from rest_framework.response import Response
def bulk_update(self, request, *args, **kwargs):
partial = kwargs.pop("partial", False)
queryset = self.filter_queryset(self.get_queryset))
serializer = self.get_serializer(instance=queryset, data=request.data, many=True)
serializer.is_valid(raise_exception=True)
self.perform_update(serializer)
return Response(serializer.data)
def partial_bulk_update(self, *args, **kwargs):
kargs["partial"] = True
return super(MyView, self).bulk_update(*args, **kwargs)
This won't work out of the box as Django REST framework doesn't support bulk updates by default. This means you also have to implement your own bulk updates. The current code will handle bulk updates as though you are trying to update the entire list, which is how the old bulk updating package previously worked.
While you didn't ask for bulk deletion, that wouldn't be particularly difficult to do.
def bulk_delete(self, request, *args, **kwargs):
queryset = self.filter_queryset(self.get_queryset())
self.perform_delete(queryset)
return Response(status=204)
This has the same effect of removing all objects, the same as the old bulk plugin.
None of this code was tested. If it doesn't work, consider it as a detailed example.

Categories

Resources