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)
Related
In my serializers, I have added the custom field "step_type" that grabs a value from another model.
class AccountSerializer(serializers.ModelSerializer):
step_type= serializers.SerializerMethodField()
class Meta:
model = Account
fields = '__all__'
def get_step_type(self, obj):
step = Step.objects.get(step_name=obj.step_name)
return step.step_type
I want to use query parameters to filter my REST API
class AccountViewSet(viewsets.ModelViewSet):
def get_queryset(self):
queryset = Account.objects.all().order_by('-date')
query_step_type = self.request.query_params.get("type")
if query_step_type is not None:
queryset = queryset.filter(step_type=query_step_type)
return queryset
However, this won't work because step_type isn't part of the original model fields. How can I filter the queryset using the step type serializer method field?
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)
from rest_framework import serializers
class UserProfileSerializer(serializers.ModelSerializer):
"""Serializes data for User."""
url = serializers.URLField(source='profile_url')
def profile_url(self):
"""Return user profile endpoint."""
return reverse('user:profile_view', kwargs=self.context.get('id', None))
class Meta:
model = User
fields = ('id', 'url')
How can I access User.id within profile_url? There doesn't appear to be any id parameter within the context or at least I don't think I'm accessing properly.
Actually, you may want to change the field on your serializer to be:
url = serializers.SerializerMethodField('profile_url')
This will give the method profile_url an extra parameter, the object that is being serialized. So in your case, this becomes:
def profile_url(self, obj):
"""Return user profile endpoint."""
return reverse('user:profile_view', args=[obj.id,])
I have two models: FacebookAccount and Person. There's a OneToOne relationship like so:
class FacebookAccount(models.Model):
person = models.OneToOneField(Person, related_name='facebook')
name = models.CharField(max_length=50, unique=True)
page_id = models.CharField(max_length=100, blank=True)
I created a PersonSerializer which has a facebook field set to a FacebookSerializer I created:
class FacebookSerializer(serializers.ModelSerializer):
class Meta:
model = FacebookAccount
fields = ('name', 'page_id',)
class PersonSerializer(serializers.ModelSerializer):
facebook = FacebookSerializer(required=False)
class Meta:
model = Person
fields = ('id', 'name', 'facebook',)
I then created a view to create a new Person along with a new FacebookAccount instance for a POST request:
class PersonCreate(APIView):
def post(self, request):
# Checking for something here, doesn't affect anything
if 'token' in request.DATA:
serializer = PersonSerializer(data=request.DATA)
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)
This works fine and well when my POST data has a facebook object with a unique name attribute in it. However, what if the facebook information is optional? I set "required=False" in the facebook field on the PersonSerializer to account for this, but I get a 400 error when my POST data does not contain a facebook object. The error says that the "name" field for facebook is required.
If I set "blank=True" on the name field in the FacebookAccount model and don't provide and facebook info in my POST data, a blank FacebookAccount record is created which is expected but not desired.
Does anyone know how to fix this? Thanks.
edit:
I tried overriding the save_object method on the PersonSerializer, but it looks like that method isn't called. I tried working my way back through the methods that are called, and it looks like get_field is the last one:
class PersonSerializer(serializers.ModelSerializer):
facebook = FacebookSerializer(required=False)
def get_default_fields(self):
field = super(PersonSerializer, self).get_default_fields()
print field
return field
def get_pk_field(self, model_field):
print 'MF'
mf = super(PersonSerializer, self).get_pk_field(model_field)
print mf
return mf
def get_field(self, model_field):
print 'FIELD'
mf = super(PersonSerializer, self).get_field(model_field)
print mf
return mf
# not called
def get_validation_exclusions(self, instance=None):
print '**Exclusions'
exclusions = super(PersonSerializer, self).get_validation_exclusions(instance=None)
print exclusions
return exclusions
# not called
def save_object(self, obj, **kwargs):
if isinstance(obj, FacebookAccount):
print 'HELLO'
else:
print '*NONONO'
return super(PersonSerializer, self).save_object(obj, **kwargs)
class Meta:
model = Person
fields = ('id', 'name', 'stripped_name', 'source', 'facebook',)
Simply pass allow_null=True when declaring the nested serializer in the parent serializer, like this:
class FacebookSerializer(serializers.ModelSerializer):
class Meta:
model = FacebookAccount
fields = ('name', 'page_id',)
class PersonSerializer(serializers.ModelSerializer):
facebook = FacebookSerializer(allow_null=True, required=False)
class Meta:
model = Person
fields = ('id', 'name', 'facebook',)
Well, that is the limitation of django-restframework when specifying reverse relation as a field, because it creates a list from the fields specified , required=false only directs the serializer to save the default null value for that related field, to resolve it you can override save_object method of serializer and remove facebook field if its null.
Sorry for late reply, i have somewhat similar models and it is working, by overriding save_object method, the is_valid() must be returning false (have you checked it), anyway saving objects using a serializer is for very basic use cases thus limited, best way would be to save those objects explicitly in the view.
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.