Django REST Framework: nested relationship, how to submit json? - django

I'm using Django 2.1, DRF 3.7.7.
I've some models and their relative (model) serializers: these models are nested, and so are the serializers.
Let me give an example:
# models.py
class City(models.Model):
name = models.CharField(max_length=50)
class Person(models.Model):
surname = models.CharField(max_length=30)
birth_place = models.ForeignKey(City)
# serializers.py
class CitySerializer(serializers.ModelSerializer):
class Meta:
model = models.CitySerializer
fields = "__all__"
class PersonSerializer(serializers.ModelSerializer):
birth_place = CitySerializer()
class Meta:
model = models.Person
fields = "__all__"
If I submit an AJAX request with a json like:
{'surname': 'smith', 'birth_place': 42}
I get back a Bad Request response, containing: Invalid data. Expected a dictionary, but got int.
If I submit a nested json like:
{'surname': 'smith', 'birth_place': {'id': 42, 'name': 'Toronto'}}
the relation is not converted, the id field is ignored and the rest is parsed to:
OrderedDict([('birth_place', OrderedDict([('name', 'Toronto')]))])
The following is the post method I'm using on a class-based view:
def post(self, request):
print("Original data:", request.data)
serializer = self.serializer_class(data=request.data)
if serializer.is_valid():
self.data = serializer.validated_data
print("Parsed data:", self.data)
...
I only need to get data from the endpoints connected to the serializers, I don't need to write/save anything through the REST interface, since the POST processing of the form is done by Django.
TL;DR: How should I correctly submit a JSON request to a nested serializer, without having to write handmade conversions? Did I commit errors in setting up the serializers?
Edit: I've discovered that by adding id = serializers.IntegerField() to the serializer parent class (e.g. City), the serializer parser now processes the id. At least now I'm able to perform actions in the backend with django.

Generic writing for nested serializers is not available by default. And there is a reason for that:
Consider, you are creating a person with a birthplace, using a POST request. It is not clear if the submitted city is a new one or an existing one. Should it return an error if there isn't such a city? Or should it be created?
This is why, if you want to handle this kind of relationship in your serializer, you need to write your own create() and update() methods of your serializer.
Here is the relevant part of the DRF docs: http://www.django-rest-framework.org/api-guide/relations/#writable-nested-serializers

It's definitely not clearly put into the docs of django-rest. If you follow the process of serializers processing the data for creation then it becomes clear that django manages m2m by saving the parent instance first and then adding the m2m values, but somehow the m2m fields don't go through the validation if you mark them as read_only.
The solution to this is to overr run_validation method of the serializer. The serializer should look like this:
class ExampleSerializer(serializers.ModelSerializer):
queryset = SomeModel.objects.all()
tags = TagSerializer(many=True, read_only=True)
class Meta:
model = SomeModel
fields = ['pk', 'name', 'tags']
def run_validation(self, data):
validated_data = super(StudyResourceSerializer, self).run_validation(data)
validated_data['tags'] = data['tags']
return validated_data
The request body should look like this:
{
"tags": [51, 54],
"name": "inheritance is a mess"
}

Related

filter django serializer data

Many time we access data via serializer directory according to relationship defined in models in Django(1.11.10). How can i set a filter like fetch-only is_active=1.
class DaasJobsSerializer(serializers.ModelSerializer):
class Meta:
model = DaasJobs
fields = '__all__'
class DaasScheduleSerializer(serializers.ModelSerializer):
jobs = DaasJobsSerializer(read_only=True,many=True)
class Meta:
model = DaasSchedule
fields = '__all__'
Here i just want to set a filter to fetch only those Jobs which db field is_active=1 in this line like that DaasJobsSerializer(read_only=True,many=True, filter={"is_active":1}) how to do something like this ??
Currently it is giving me all the data without checking is_active,
and i dont want to create serializerMethodField for that.. because all methods written earlier.. i am just setting a is_active field later in the tables in db.
If you want to do it via serializers you can try overriding the ListSerializer and passing it as a custom list_serializer_class.
class IsActiveListSerializer(serializers.ListSerializer):
def to_representation(self, data):
data = data.filter(is_active=1)
return super().to_representation(data)
In your serializer:
class DaasJobsSerializer(serializers.ModelSerializer):
class Meta:
model = DaasJobs
fields = '__all__'
list_serializer_class = IsActiveListSerializer # import it here
Of course this is a specific use-case, you could make a more generalized version of the ListSerializer to:
class FilteredListSerializer(serializers.ListSerializer):
filter_kwargs = {}
def to_representation(self, data):
if not self.filter_kwargs or not isinstance(self.filter_kwargs, dict):
raise TypeError(_('Invalid Attribute Type: `filter_kwargs` must be a of type `dict`.'))
data = data.filter(**self.filter_kwargs)
return super().to_representation(data)
And then you could sub-class that to make other specific ListSerializers such as:
class IsActiveListSerializer(FilteredListSerializer):
filter_kwargs = {'is_active': 1}
and many others...

Django Rest Framework: How to pass a list of UUIDs for a nested relationship to a serializer?

TL;DR: What could be the reason the incoming data for one of my serializers does not get processed?
I'm working on a serializer for a nested relationship. The serializer should get a list of UUIDs, so that I can make many to many relationships. Here is the model:
class Order(
UniversallyUniqueIdentifiable,
SoftDeletableModel,
TimeStampedModel,
models.Model
):
menu_item = models.ForeignKey(MenuItem, on_delete=models.CASCADE)
custom_choice_items = models.ManyToManyField(CustomChoiceItem, blank=True)
price = models.ForeignKey(MenuItemPrice, on_delete=models.CASCADE)
amount = models.PositiveSmallIntegerField(
validators=[MinValueValidator(MINIMUM_ORDER_AMOUNT)]
)
Here is the data (my post body) with which I hit the route in my tests:
data = {
"checkin_uuid": self.checkin.uuid,
"custom_choice_items": [],
"menu_item": self.menu_item.uuid,
"price": self.menu_item_price.uuid,
"amount": ORDER_AMOUNT,
}
response = self.client.post(self.order_consumer_create_url, self.data)
Note that the empty list for custom_choice_items does not change anything. Even if I fill it with values the same error occurs. And last but not least here are the serializers:
class CustomChoiceItemUUIDSerializer(serializers.ModelSerializer):
"""Serializer just for the uuids, which is used when creating orders."""
class Meta:
model = CustomChoiceItem
fields = ["uuid"]
....
# The serializer that does not work
class OrderSerializer(serializers.ModelSerializer):
menu_item = serializers.UUIDField(source="menu_item.uuid")
custom_choice_items = CustomChoiceItemUUIDSerializer()
price = serializers.UUIDField(source="price.uuid")
wish = serializers.CharField(required=False)
class Meta:
model = Order
fields = [
"uuid",
"menu_item",
"custom_choice_items",
"price",
"amount",
"wish",
]
The problem is now, that when I leave out many=True, I get the error:
{'custom_choice_items': [ErrorDetail(string='This field is required.', code='required')]}
And If I set many=True I just simply don't get any data. By that I mean e.g. the value of validated_data["custom_choice_items"] in the serializers create() method is just empty.
What goes wrong here?
I even checked that the data is in the request self.context["request"].data includes a key custom_choice_items the way I pass the data to this view!
EDIT: Here is the data I pass to custom_choice_items:
data = {
“checkin_uuid”: self.checkin.uuid,
“custom_choice_items”: [{“uuid”: custom_choice_item.uuid}],
“menu_item”: self.menu_item.uuid,
“price”: self.menu_item_price.uuid,
“amount”: ORDER_AMOUNT,
}
self.client.credentials(HTTP_AUTHORIZATION=“Token ” + self.token.key)
response = self.client.post(self.order_consumer_create_url, data)
When you post data using the test api client, if the data contains nested structure you should use format=json, like this:
response = self.client.post(self.order_consumer_create_url, data, format='json')
Did you override .create method in the serializer? Something like this should work:
from django.db import transaction
class OrderSerializer(serializers.ModelSerializer):
# your fields and Meta class here
#transaction.atomic
def create(self, validated_data):
custom_choice_items = validated_data.pop('custom_choice_items')
order = super().create(validated_data)
order.custom_choice_items.add(*custom_choice_items)
return order
By the way you don't really need to define CustomChoiceItemUUIDSerializer if is just the primary key of that.

Nest serializers skips transform step

I have a model in which I use a JSON field and when I serializer it alone, it works perfectly. However, when I include it as a nested serializer it doesn't convert from JSON and I am left with a string.
I have tried using transform_fieldname but it doesn't get hit if the serializer is nested. It gets hit if the serializer is not nested.
I am using the django-jsonfield module in my model.
class TopLevelSerializer(serializers.ModelSerializer):
nest = NestedSerializer(many=True)
class Meta:
model = ModelTop
fields = ('id', 'nest')
class NestedSerializer(serializers.ModelSerializer):
def transform_options(self, obj, value):
print 'This doesn't get hit if nested'
return json.loads(obj)
class Meta:
model = ModelTwo
fields = ('id', 'options')
Have you tried specifing the depth of the serialization of your relation?
See http://www.django-rest-framework.org/api-guide/serializers#specifying-nested-serialization

Django REST framework and creating two table entries from a POST form

I want to create entries in two tables (Log and Post) using the DRF Browseable API POST form.
The example below is contrived, but it outlines what I am trying to do.
class Post(models.Model):
info = models.CharField()
class Log(TimeStampedModel):
ip = models.GenericIPAddressField(('IP Address'))
name = models.CharField()
data = models.ForeignKey(Post)
I want to use the browseable API to submit a form to create a Log entry. Here are the serializers:
class PostSerializer(serializers.ModelSerializer):
class Meta:
model = Post
fields = ('info',)
class LogSerializer(serializers.ModelSerializer):
data = serializers.Field()
class Meta:
model = Log
fields = ('ip', 'name', 'data')
The problem with the above is that serializer.Field is read only so does not show up on the POST form. If I change it to CharField it shows up, but then I get an error because an instance of a Post is expected not just a field of the Post object.
Here are my views:
class LogMixin(object):
queryset = Log.objects.all()
serializer_class = LogSerializer
class LogList(LogMixin, ListCreateAPIView):
pass
class LogDetail(LogMixin, RetrieveUpdateDestroyAPIView):
pass
What's the correct way of doing this?
From what I can tell you want to create a nested Log object. There are 2 ways of doing this:
Send 2 POST Requests, One to create the Post, and the other to create the Log contained the received HTTP 200 data from the API.
(Django and best way) Send the data all in one POST and parse it server side. Django Rest Framework takes care of this for you.
I have changed your code so that it should work.
Source
class LogSerializer(serializers.ModelSerializer):
class Meta:
model = Log
fields = ('ip', 'name')
class PostSerializer(serializers.ModelSerializer):
log = LogSerializer()
class Meta:
model = Post
fields = ('info', 'log')
views.py
import generics
class PostCreateAPIView(generics.CreateAPIView):
model = Post
serializer_class = PostSerializer
Then you can send a POST Request containing 'info', 'ip', and 'name'.
This is a hacky way and the best way to use the nested serializer as stated above. But just to show another way I am posting it here.
# Add New Item
#api_view(('POST',))
def add_new_item(request):
request.data['areaname_name'] = request.data['item_areaname']
request.data['buildingname_name'] = request.data['item_buildingname']
item_serializer = TblItemSerializer(data=request.data)
area_name_serializer = TblAreanameSerializer(data=request.data)
building_name_serializer = TblBuildingnameSerializer(data=request.data)
response = []
if item_serializer.is_valid() & area_name_serializer.is_valid() & building_name_serializer.is_valid():
# Save To Item Table
item_serializer.save()
# Save Unique Values Into Areaname and Buildingname Tables
area_name_serializer.save()
building_name_serializer.save()
return Response(item_serializer.data, status=status.HTTP_201_CREATED)
else:
response.append(item_serializer.errors)
response.append(area_name_serializer.errors)
response.append(building_name_serializer.errors)
return Response(response, status=status.HTTP_400_BAD_REQUEST)
In the error response you could also use (depending on how you want to handle on client side)
merge = {**item_serializer.errors, **area_name_serializer.errors, **building_name_serializer.errors}
return Response(merge, status=status.HTTP_400_BAD_REQUEST)

How can I apply a filter to a nested resource in Django REST framework?

In my app I have the following models:
class Zone(models.Model):
name = models.SlugField()
class ZonePermission(models.Model):
zone = models.ForeignKey('Zone')
user = models.ForeignKey(User)
is_administrator = models.BooleanField()
is_active = models.BooleanField()
I am using Django REST framework to create a resource that returns zone details plus a nested resource showing the authenticated user's permissions for that zone. The output should be something like this:
{
"name": "test",
"current_user_zone_permission": {
"is_administrator": true,
"is_active": true
}
}
I've created serializers like so:
class ZonePermissionSerializer(serializers.ModelSerializer):
class Meta:
model = ZonePermission
fields = ('is_administrator', 'is_active')
class ZoneSerializer(serializers.HyperlinkedModelSerializer):
current_user_zone_permission = ZonePermissionSerializer(source='zonepermission_set')
class Meta:
model = Zone
fields = ('name', 'current_user_zone_permission')
The problem with this is that when I request a particular zone, the nested resource returns the ZonePermission records for all the users with permissions for that zone. Is there any way of applying a filter on request.user to the nested resource?
BTW I don't want to use a HyperlinkedIdentityField for this (to minimise http requests).
Solution
This is the solution I implemented based on the answer below. I added the following code to my serializer class:
current_user_zone_permission = serializers.SerializerMethodField('get_user_zone_permission')
def get_user_zone_permission(self, obj):
user = self.context['request'].user
zone_permission = ZonePermission.objects.get(zone=obj, user=user)
serializer = ZonePermissionSerializer(zone_permission)
return serializer.data
Thanks very much for the solution!
I'm faced with the same scenario. The best solution that I've found is to use a SerializerMethodField and have that method query and return the desired values. You can have access to request.user in that method through self.context['request'].user.
Still, this seems like a bit of a hack. I'm fairly new to DRF, so maybe someone with more experience can chime in.
You have to use filter instead of get, otherwise if multiple record return you will get Exception.
current_user_zone_permission = serializers.SerializerMethodField('get_user_zone_permission')
def get_user_zone_permission(self, obj):
user = self.context['request'].user
zone_permission = ZonePermission.objects.filter(zone=obj, user=user)
serializer = ZonePermissionSerializer(zone_permission,many=True)
return serializer.data
Now you can subclass the ListSerializer, using the method I described here: https://stackoverflow.com/a/28354281/3246023
You can subclass the ListSerializer and overwrite the to_representation method.
By default the to_representation method calls data.all() on the nested queryset. So you effectively need to make data = data.filter(**your_filters) before the method is called. Then you need to add your subclassed ListSerializer as the list_serializer_class on the meta of the nested serializer.
subclass ListSerializer, overwriting to_representation and then calling super
add subclassed ListSerializer as the meta list_serializer_class on the nested Serializer
If you're using the QuerySet / filter in multiple places, you could use a getter function on your model, and then even drop the 'source' kwarg for the Serializer / Field. DRF automatically calls functions/callables if it finds them when using it's get_attribute function.
class Zone(models.Model):
name = models.SlugField()
def current_user_zone_permission(self):
return ZonePermission.objects.get(zone=self, user=user)
I like this method because it keeps your API consistent under the hood with the api over HTTP.
class ZoneSerializer(serializers.HyperlinkedModelSerializer):
current_user_zone_permission = ZonePermissionSerializer()
class Meta:
model = Zone
fields = ('name', 'current_user_zone_permission')
Hopefully this helps some people!
Note: The names don't need to match, you can still use the source kwarg if you need/want to.
Edit: I just realised that the function on the model doesn't have access to the user or the request. So perhaps a custom model field / ListSerializer would be more suited to this task.
I would do it in one of two ways.
1) Either do it through prefetch in your view:
serializer = ZoneSerializer(Zone.objects.prefetch_related(
Prefetch('zone_permission_set',
queryset=ZonePermission.objects.filter(user=request.user),
to_attr='current_user_zone_permission'))
.get(id=pk))
2) Or do it though the .to_representation:
class ZoneSerializer(serializers.HyperlinkedModelSerializer):
class Meta:
model = Zone
fields = ('name',)
def to_representation(self, obj):
data = super(ZoneSerializer, self).to_representation(obj)
data['current_user_zone_permission'] = ZonePermissionSerializer(ZonePermission.objects.filter(zone=obj, user=self.context['request'].user)).data
return data