Can't create() nested relationship with DRF - django

This is the first time I'm working with DRF.
My models:
class ServiceCategory(models.Model):
category = models.CharField(max_length=24)
class Service(models.Model):
service = models.CharField(max_length=24)
category = models.ForeignKey('ServiceCategory')
Their serializers:
class ServiceCategorySerializer(serializers.ModelSerializer):
class Meta:
model = ServiceCategory
fields = ('id', 'category')
class ServiceSerializer(serializers.ModelSerializer):
category = ServiceCategorySerializer()
class Meta:
model = Service
fields = ('service', 'category')
def create(self, data):
return Service.objects.create(**data)
And the view:
elif request.method == 'POST':
serializer = ServiceSerializer(data=request.data)
print(serializer.initial_data) # To debug the contents of the request
if serializer.is_valid():
serializer.save()
return Response(serializer.data, status=status.HTTP_201_CREATED)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
Initially, before I added the nested category to the ServiceSerializer, I had no problem creating new Services. The print(serializer.initial_data) outputs <QueryDict: {'category': ['1'], 'service': ['EC2']}> so obviously I'm supplying the category to the request but I'm getting "category" : ["This field is required"] errors.
So I'm thinking the problem might be with my create(self, data) method in the ServiceSerializer but I'm unable to put a finger on what exactly is wrong with it.
What have I missed?
UPDATE
Without the ServiceCategorySerializer in the ServiceSerializer, and the view being:
elif request.method == 'POST':
serializer = ServiceSerializer(data=request.data)
print(serializer.initial_data) # for debugging
if serializer.is_valid():
print(serializer.data) # for debugging
serializer.initial_data returns <QueryDict: {'category': ['1'], 'service': ['EC2']}>
and
serializer.data returns {'service': 'EC2', 'category': 1} so I assume the contents of serializer.data are what will get passed to the create() method of the ServiceSerializer. By itself, it works, but when I include the ServiceCategorySerializer inside it, the POST doesn't go through and I get the same annoying "category" : ["This field is required"]
I've been stuck with this for over 6 hours now. What is going on???

I have a full working example - of what you wanna achieve - using just information that I found in this thread:
Models:
from __future__ import unicode_literals
from django.db import models
class ServiceCategory(models.Model):
category = models.CharField(max_length=24)
class Service(models.Model):
service = models.CharField(max_length=24)
category = models.ForeignKey('ServiceCategory')
Serializers:
from rest_framework import serializers
from nestedd.models import ServiceCategory, Service
class ServiceCategorySerializer(serializers.ModelSerializer):
class Meta:
model = ServiceCategory
fields = ('id', 'category')
class ServiceSerializer(serializers.ModelSerializer):
category = ServiceCategorySerializer()
class Meta:
model = Service
fields = ('service', 'category')
def create(self, validated_data):
category_data = validated_data.pop('category')
# 'created' will be True if no existing category matches
category, created = ServiceCategory.objects.get_or_create(**category_data)
return Service.objects.create(category=category, **validated_data)
Views:
# Create your views here.
from rest_framework import viewsets
from nestedd.models import Service
from nestedd.serializers import ServiceSerializer
class ServiceViewSet(viewsets.ModelViewSet):
queryset = Service.objects.all()
serializer_class = ServiceSerializer
urls:
from rest_framework.routers import DefaultRouter
from nestedd.views import ServiceViewSet
router = DefaultRouter()
router.register(r'nested', ServiceViewSet, base_name='service')
urlpatterns = router.urls
app urls:
url(r'^api/v2/', include('nestedd.urls')),
And this how my postman look likes:
THE PROBLEM - POST DATA FORMAT
Probably you made a wrong POST query - if you want to use nested serializer, like this:
category = ServiceCategorySerializer()
In some other serializer, you must know that first field name is attached to the parent serializer, eg.:
{
"service_name": "test",
"category": ...
}
And what should be placed in category field? Well - an object, because you tell that this field is another serializer, if object then:
{
"service_name": "test",
"category": {
"category": "some_category"
}
}
And in this object you specify the fields for the model which is described by inner serializer, so basically, when you pass only the "id" -> it obvious that ServiceCategory cannot be created, beacause the category field on model ServiceCategory - is required.
ANOTHER NOTE: EXISITING VS. NON EXISITNG CATEGORY
You will have problems with handling existing/non-existing category;
Basically you should make category field unique on ServiceCategory, and in post on ServiceViewSet - check if category exists (if so take it and assign to the Service object - if no - create a category) - in this scenario you will not need to pass category id each time. And handle it when id - does not exists.

When POSTing a service, the category field must contain a PK, i.e. an integer. Your category field in serializer.initial_data contains a list with a string.
BTW1: your service field also has a list when your model expects a string (CharField). This might also be a problem.
BTW2: No need to override your serializer's create in your case.

As stated in the docs, you should implement the create() method in a slightly different way, saving the category first if it doesn't exist yet, and then passing it to the Service.objects.create() function, like this (untested):
def create(self, validated_data):
category_data = validated_data.pop('category')
# 'created' will be True if no existing category matches
category, created = ServiceCategory.objects.get_or_create(**category_data)
return Service.objects.create(category=category, **validated_data)

Related

Update an field in django rest framework

I have a class named Customer:
class Customer(models.Model):
name = models.CharField(max_length=250)
status = models.IntegerField()
And the serializer is:
class CustomerSerializer(serializers.ModelSerilizer):
class Meta:
model = Customer
fields = '__all__'
Now how can I change/update only the status field using POST method. I am using function base view here.
I want to receive value such as:
{
"status": 1
}
This would have been way easier if you were using class based views. You can easily create an UpdateStatusView that updates RetrieveUpdateAPIView send a patch request.
However, since you're using function based views, I'll still recommend you use a PATCH request rather than a POST request, this give better self documentation.
def update_status_request(request, id):
if request.method == 'PATCH':
customer = Customer.objects.get(pk=id)
customer.status = request.data.get('new_status')
customer.save()
return JsonResponse({'message': 'Status has been updated'}, status=200)
You might also wanna do some extra validation and try...except.
why do you want user post method to update the data, since you are update you can user patch
class CustomerUpdateSerializer(serializers.ModelSerializer):
class Meta:
model = Customer
fields = ('status',)
from rest_framework.decorators import api_view
from rest_framework import response
#api_view(methods=['post', "patch"])
def api_function(request, *args, **kwargs):
change instance logic according to u
instance = Customer.objects.get()
if request.method == "POST":
# post is used for create data so i did for that for update use path
serializer = CustomerSerializer(data=request.data)
else:
serializer = CustomerUpdateSerializer(instance=instance, data=request.data)
if serializer.is_valid(raise_exceptions=True):
serializer.save()
return response.Response(serializer.data)
You can use viewset for this purpose.
View:
from rest_framework import viewset
class CustomerViewSet(viewset.ModelViewSet):
serializer_class = CustomerSerializer
Url:
path('customer/update/<int:pk>', CustomerViewSet.as_view({'post': 'update'})),

Update data using RetrieveUpdateAPIView - Getting validated data from a serializer

I would like to update certain properties of a user (say first_name and last_name)
my json object through a PUT request would look like this
{
"user" : {
"first_name": "Jack",
"last_name": "shnider",
"password":"admin123"
"email" : "foo#google.com"
},
"employee_zip" : 12345
}
This is what my view looks like (I would like to update the existing fields to these new fields).
These are the serializer
class Serializer_UpdateUser(ModelSerializer):
class Meta:
model = User
fields = ('first_name','last_name','password')
class Serializer_UpdateEmployer(ModelSerializer):
user = Serializer_UpdateUser()
class Meta:
model = modelEmployer
fields = [
'user',
'employer_zip',
]
This is the view :
class UpdateProfile_RetrieveUpdateAPIView(RetrieveUpdateAPIView):
queryset = modelEmployer.objects.all()
serializer_class = Serializer_UpdateEmployer
lookup_field = 'user__email'
permission_classes = [permissions.AllowAny]
def update(self, request, *args, **kwargs):
instance = self.get_object() #------>I have the object that I would like to update
serializer = self.get_serializer(instance, data=request.data, partial=partial)
serializer.is_valid(raise_exception=True) #--->Success
Now I would like to get a validated fields (The json only contains the fields that have been updated). I know if I do something like this
serializer.save
I would get back a modelEmployer but instead I get back this error
AssertionError at /api/employer/update_profile/employerA#gmail.com/ The `.update()` method does not support writable nested fields by default. Write an explicit `.update()` method for serializer `Employer.api.serializers.Serializer_ListEmployer`, or set `read_only=True` on nested serializer fields. Request Method:
I have two questions
1-Why is save failing ?
2-How can I get the validated data from the above serializer ?
The save is failing because django-rest-framework doesn't deal with nested serializers by default.
from the django-rest-framework docs:
By default nested serializers are
read-only. If you want to support write-operations to a nested
serializer field you'll need to create create() and/or update()
methods in order to explicitly specify how the child relationships
should be saved.
You have to override the update method in the serializer to allow that behavior:
class Serializer_UpdateEmployer(ModelSerializer):
user = Serializer_UpdateUser()
class Meta:
model = modelEmployer
fields = [
'user',
'employer_zip',
]
def update(self, instance, validated_data):
user_data = validated_data.pop('user', {})
user_serializer = Serializer_UpdateUser(instance.user, data=user_data)
user_serializer.save()
return instance
Another solution is to use drf-writable-nested. It automatically makes your nested serializers updatable.
from drf_writable_nested import WritableNestedModelSerializer
class Serializer_UpdateEmployer(WritableNestedModelSerializer):
user = Serializer_UpdateUser()
class Meta:
model = modelEmployer
fields = [
'user',
'employer_zip',
]
I think drf-writable-nested can help you to update nested data.
In you case:
from django.contrib.auth import password_validation
class Serializer_UpdateUser(ModelSerializer):
def update(self, instance, validated_data):
password = validated_data.pop('password', None)
super(Serializer_UpdateUser, self).update(instance, validated_data)
if password is not None:
instance.set_password(password)
instance.save()
return instance
def validate_password(self, value):
password_validation.validate_password(value)
return value
class Meta:
model = User
fields = ('first_name','last_name','password')
class Serializer_UpdateEmployer(WritableNestedModelSerializer):
user = Serializer_UpdateUser()
class Meta:
model = modelEmployer
fields = [
'user',
'employer_zip',
]
Note you need special handling password field.

Django Rest API: How to get rid of 'UUID' in json when serializing models?

Why does 'UUID' appear in front of the value of 'profile' key and how do I remove it properly?
roster/serializers.py
class ShiftSerializer(serializers.ModelSerializer):
class Meta:
model = Shift
fields = ('id', 'profile', 'location', 'date', 'start_time', 'end_time')
profile/models.py
class Profile(models.Models):
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=True)
roster/models.py
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=True)
profile = models.ForeignKey('profiles.Profile', null=True, blank=True)
python manage.py shell
from roster.models import Shift
from roster.serializers import ShiftSerializer
myshift = Shift.objects.first()
serializer = ShiftSerializer(myshift)
serializer.data
Output:
{'id': '92ca258e-8624-434a-b61d-e1cd3b80e0e8', 'profile': UUID('0081b028-0a11-47fb-971e-c47177ed93be')
tl;dr
See The solution at the bottom.
The problem
Attribute .data on Serializer should return only primitive representation of object (http://www.django-rest-framework.org/api-guide/serializers/#baseserializer). This should be done by calling to_representation() method (http://www.django-rest-framework.org/api-guide/serializers/#to_representationself-obj) on a serializer and all fields.
#six.add_metaclass(SerializerMetaclass)
class Serializer(BaseSerializer):
# ...
# ...
def to_representation(self, instance):
"""
Object instance -> Dict of primitive datatypes.
"""
# ...
# ...
for field in fields:
# ...
# ...
if check_for_none is None:
ret[field.field_name] = None
else:
ret[field.field_name] = field.to_representation(attribute)
return ret
Source: https://github.com/encode/django-rest-framework/blob/master/rest_framework/serializers.py#L505-L529
There is a problem with models that have uuid.UUID as a primary key. PrimaryKeyRelatedField returns uuid.UUID instance - this is clearly not a primitive type which leads to e.g. UUID('') is not JSON serializable errors.
When pk_field attribute on PrimaryKeyRelatedField is not set, to_representation method simply returns uuid.UUID instance, see the related code:
class PrimaryKeyRelatedField(RelatedField):
# ...
def to_representation(self, value):
if self.pk_field is not None:
return self.pk_field.to_representation(value.pk)
return value.pk
Source: https://github.com/encode/django-rest-framework/blob/master/rest_framework/relations.py#L269-L272
Why is it a problem
As stated in other answers and comments, JSONRenderer will correctly handle this issue (http://www.django-rest-framework.org/api-guide/serializers/#serializing-objects)
from rest_framework.renderers import JSONRenderer
json_data = JSONRenderer().render(serializer.data)
But there are situations when you do not want to use JSONRenderer:
You're comparing .data during unit testing;
You need to store .data in database, file, ...
You want to post .data via requests to some API: requests.post(..., json=serializer.data)
...
The solution
Set pk_field attribute on PrimaryKeyRelatedField to UUIDField():
from rest_framework import serializers
from rest_framework.fields import UUIDField
class ExampleSerializer(serializers.ModelSerializer):
id = serializers.PrimaryKeyRelatedField(required=True,
allow_null=False,
# This will properly serialize uuid.UUID to str:
pk_field=UUIDField(format='hex_verbose'))
And uuid.UUID instances will be correctly serialized to str when accessing serializer.data.
http://www.django-rest-framework.org/api-guide/relations/#primarykeyrelatedfield
http://www.django-rest-framework.org/api-guide/fields/#uuidfield
You can rewrite representation ,like this
class ShiftSerializer(serializers.ModelSerializer):
class Meta:
model = Shift
fields = '__all__'
def to_representation(self, obj):
return {
"id": obj.id,
"profile": obj.profile.id,
"location": obj.location,
"date": obj.date,
"start_time": obj.start_time,
}
you can try to use, serializers.CharField
class ShiftSerializer(serializers.ModelSerializer):
profile = serializers.CharField(read_only=True)
UUID will be corrected when rendered by JSONRenderer.

Add another argument to route for DRF

There are set of API endpoints generated by default by Django Rest Framework. Example this one :
^api/ ^ ^provinces/(?P<pk>[^/.]+)/$ [name='province-detail']
produces http://127.0.0.1:8000/api/provinces/02/ which is fine.
It uses the actual code bellow:
class ProvinceSerializer(serializers.ModelSerializer):
""" Serializer to represent the Province model """
class Meta:
model = Province
fields = ("name", "code")
I want to add another route, so that I can have another endpoint for example:
^api/ ^ ^provinces/(?P<pk>[^/.]+)/(?P<product>[^/.]+)/$ [name='province-product-detail']
So that I can do like this http://127.0.0.1:8000/api/provinces/02/apple/ and access the second argument in a method of the serializer. I'm trying to do like this :
class ProvinceSerializer(serializers.ModelSerializer):
""" Serializer to represent the Province model """
class Meta:
model = Province
fields = ("name", "code")
#detail_route(methods=['post'])
def set_product(self, request, product=None):
return product
I've find out I was using the #detail_route in the wrong place. We just have to do this:
class ProvinceDistrictViewSet(viewsets.ModelViewSet):
"""
API endpoint that allows users to view or edit province.
"""
queryset = Province.objects.all()
serializer_class = ProvinceDistrictsSerializer
# For get provinces
#detail_route(methods=['get'], url_path='(?P<product>\d+)')
def update_product(self, request, pk, product=None):
""" Updates the object identified by the pk and add the product """
queryset = Province.objects.filter(pk=pk)
serializer = ProvinceDistrictsSerializer(queryset, many=True, context={'product': product})
return Response(serializer.data, status=status.HTTP_200_OK)

Django Rest Framework return nested object using PrimaryKeyRelatedField

I am using DRF to expose some API endpoints.
# models.py
class Project(models.Model):
...
assigned_to = models.ManyToManyField(
User, default=None, blank=True, null=True
)
# serializers.py
class ProjectSerializer(serializers.ModelSerializer):
assigned_to = serializers.PrimaryKeyRelatedField(
queryset=User.objects.all(), required=False, many=True)
class Meta:
model = Project
fields = ('id', 'title', 'created_by', 'assigned_to')
# view.py
class ProjectList(generics.ListCreateAPIView):
mode = Project
serializer_class = ProjectSerializer
filter_fields = ('title',)
def post(self, request, format=None):
# get a list of user.id of assigned_to users
assigned_to = [x.get('id') for x in request.DATA.get('assigned_to')]
# create a new project serilaizer
serializer = ProjectSerializer(data={
"title": request.DATA.get('title'),
"created_by": request.user.pk,
"assigned_to": assigned_to,
})
if serializer.is_valid():
serializer.save()
else:
return Response(serializer.errors,
status=status.HTTP_400_BAD_REQUEST)
return Response(serializer.data, status=status.HTTP_201_CREATED)
This all works fine, and I can POST a list of ids for the assigned to field. However, to make this function I had to use PrimaryKeyRelatedField instead of RelatedField. This means that when I do a GET then I only receive the primary keys of the user in the assigned_to field. Is there some way to maintain the current behavior for POST but return the serialized User details for the assigned_to field?
I recently solved this with a subclassed PrimaryKeyRelatedField() which uses the id for input to set the value, but returns a nested value using serializers. Now this may not be 100% what was requested here. The POST, PUT, and PATCH responses will also include the nested representation whereas the question does specify that POST behave exactly as it does with a PrimaryKeyRelatedField.
https://gist.github.com/jmichalicek/f841110a9aa6dbb6f781
class PrimaryKeyInObjectOutRelatedField(PrimaryKeyRelatedField):
"""
Django Rest Framework RelatedField which takes the primary key as input to allow setting relations,
but takes an optional `output_serializer_class` parameter, which if specified, will be used to
serialize the data in responses.
Usage:
class MyModelSerializer(serializers.ModelSerializer):
related_model = PrimaryKeyInObjectOutRelatedField(
queryset=MyOtherModel.objects.all(), output_serializer_class=MyOtherModelSerializer)
class Meta:
model = MyModel
fields = ('related_model', 'id', 'foo', 'bar')
"""
def __init__(self, **kwargs):
self._output_serializer_class = kwargs.pop('output_serializer_class', None)
super(PrimaryKeyInObjectOutRelatedField, self).__init__(**kwargs)
def use_pk_only_optimization(self):
return not bool(self._output_serializer_class)
def to_representation(self, obj):
if self._output_serializer_class:
data = self._output_serializer_class(obj).data
else:
data = super(PrimaryKeyInObjectOutRelatedField, self).to_representation(obj)
return data
You'll need to use a different serializer for POST and GET in that case.
Take a look into overriding the get_serializer_class() method on the view, and switching the serializer that's returned depending on self.request.method.