Pytest fails when trying to pass list of dictionaries in POST request - django-rest-framework

We have a model with only one fields (Name). We want to send a list of dictionaries and create multiple objects for this model in one POST request. The model, serializer, view and mixin is mentioned below:
models.py
class Manufacturer(models.Model):
"""
The Measure Unit Category Table contains the categories
of measurement units (example: weight, length, temperature,etc)
"""
name = models.CharField(verbose_name=_("Manufacturer Name"),
help_text=_("Required. Example: Ford, Tesla, Hyundai"),
max_length=63,
unique=True)
class Meta:
verbose_name = _("Manufacturer")
verbose_name_plural = _("Manufacturers")
ordering = ('name',)
def __str__(self):
return self.name
serializers.py
class ManufacturerSerializer(serializers.ModelSerializer):
class Meta:
model = Manufacturer
fields = '__all__'
mixins.py
class CreateModelMixin(object):
def get_serializer(self, *args, **kwargs):
# add many=True if the data is of type list
if isinstance(kwargs.get("data", {}), list):
kwargs["many"] = True
return super().get_serializer(*args, **kwargs)
views.py
class ManufacturerCreateView(CreateModelMixin, generics.CreateAPIView):
"""
Create View for Manufacturer
"""
queryset = Manufacturer.objects.all()
serializer_class = ManufacturerSerializer
permission_classes = (IsAuthenticated,)
With the above code, I'm able to create multiple objects with one POST request.
I've written the below test case using pytest:
def test_create_manufacturer_list_of_objects(self, authenticated_client):
data = [
{
"name": "Test11"
},
{
"name": "Test12"
}
]
response = authenticated_client.post(reverse('manufacturer-create'), data)
assert response.status_code == 201
urls.py
path('create-manufacturer/',
ManufacturerCreateView.as_view(),
name='manufacturer-create'),
When I run the above, I get the error - AttributeError: 'list' object has no attribute 'items'.
This is a little confusing as the API works without any errors but the test case fails. Can you please guide on correcting this issue?
I tried overriding the POST method, and including the above code in serializers instead of mixins, but it gives the same error.

Passing the "format=json" when calling the post method resolved the issue.
Modified the test url to below:
response = authenticated_client.post(reverse('manufacturer-create'), data, format=json)

Related

How to serialize multiple query set to json Response in django rest frame work

I am getting a list of committees by user using a get method in which I am sending a user id but I am getting an error Committee object is not serialize I have serializer created but I dont't know how to serializer the queryset of that particular user id result.
below is my views.py file
def get(self, request, user_id):
get_committees = Committee.objects.filter(user=Profile.objects.get(id=user_id))
data = {
"status": "success",
"data":get_committees
}
res = Response(serializer.data, status=status.HTTP_200_OK)
below is my serializer.py
class MyCommitteesSerializer(serializers.ModelSerializer):
def get_queryset(self, user_id):
my_committees =
Committee.objects.filter(user=Profile.objects.get(id=user_id))
return my_committees
from your code in get method
"data":get_committees
this get_committees is a list and you are trying to return a list but you can only return json, and so you are getting the message.
You can do this in a better way
define your serializer as -
class MyCommitteesSerializer(serializers.ModelSerializer):
class Meta:
model = Committee
fields = ""_all__"
and define your views as -
class MyCommitteesView(generics.ListAPIView):
serializer_class = MyCommitteesSerializer
def get_queryset(self):
queryset = Committee.objects.filter(user=Profile.objects.get(id=self.request.user_id))
return queryset
the serializer will take care of serialization and you can customize this according to your needs.
or else you will have to manually convert your get_committes into json format before returning.

Django: customizing the field types needed for create and retrieve serializers

I currently have the following serializer:
serializers.py
class SurfGroupSerializer(serializers.ModelSerializer):
instructor = SurfInstructorSerializer(many=False)
surfers = SurferSerializer(many=True)
class Meta:
model = SurfGroup
fields = ['uuid', 'instructor', 'date', 'starting_time', 'ending_time', 'surfers']
def create(self, validated_data):
return SurfGroup(**validated_data)
And the following viewset create method (viewset inherited from viewsets.ViewSet as we need some bespoke customization, extra signals and actions etc):
viewsets.py
# Surf Group Create View:
def create(self, request, format=None):
serializer = SurfGroupSerializer(data=request.data)
serializer.is_valid(raise_exception=True)
self.perform_create(serializer)
headers = self.get_success_headers(serializer.data)
response = responses.standardized_json_response(
message='Surf Group Objects Have Been Successfully Created',
data=serializer.data
)
return Response(data=response, status=status.HTTP_201_CREATED, headers=headers)
For the retrieve action, the serializer works well, and we have a nested instructor object in the response. However, I want to perform a create by passing in the instructor uuid attrbiute like (see content in the POST textarea):
Rather than a whole object...I was wondering how we achieve this? Is it best to just have two Serializers, one for performing the create, and one the retrieval?
def create(self, validated_data):
surf_group = SurfGroup(
instructor__uuid=validated_data['instructor'],
)
surf_group.save()
return surf_group
It is good question.
I work with this situations many times and it looks like one option is to have two serializers as you mean: 1 for list/retrieve and 1 for save.
Another option (for me) is to set serializer field input as UUID and output as another serializer data like this:
class SurfGroupSerializer(serializers.ModelSerializer):
instructor = serializers.UUIDField()
surfers = SurferSerializer(many=True, read_only=True)
class Meta:
model = SurfGroup
fields = ['uuid', 'instructor', 'date', 'starting_time', 'ending_time', 'surfers']
# I use this validate method to transform uuid to object which will
# be bypassed to create method for easly save
def validate_instructor(self, instructor_uuid):
try:
return Instructor.objects.get(uuid=instructor_uuid)
except Instructor.DoesNotExist:
# Remember that you dont need to pass field_key: [errors] to ValidationError
# because validate_FIELD will automatically pass field_key for you !
raise ValidationError(['Instructor with the given uuid does not exist.'])
# Overwrite output data
def to_representation(self, instance):
ret = super().to_representation(instance)
ret['instructor'] = SurfInstructorSerializer(instance=instance.instructor).data
return ret

Cannot generate post request for multiple data

I am trying to take input multiple data object in post request, but getting such error.
non_field_errors: [ Invalid data. Expected a dictionary, but got a list. ]
models.py
class OrderProduct(BaseModel):
product = models.ForeignKey(Product,on_delete=models.CASCADE)
order = models.ForeignKey(Order,on_delete=models.CASCADE)
order_product_price = models.FloatField(blank=False,null=False,default=0) # product may belong to offer do the price
order_product_qty = models.FloatField(default=1)
serializers.py
class OrderProductSerializer(serializers.ModelSerializer):
def update(self,instance,validated_data):
product = self.validated_data.pop('product')
order = self.validated_data.pop('order')
instance.orderproduct_qty =
self.validated_data.get('orderproduct_qty',instance.orderproduct_qty)
instance.product = product
instance.order = order
instance.save()
return instance
class Meta:
model = OrderProduct
fields = '__all__'
views.py
def post(self,request,*args,**kwargs):
if request.data['contact_number'] == '':
request.POST._mutable =True
request.data['contact_number'] = request.user.mobile_number
request.POST._mutable = False
serializer = OrderSerializer(data=request.data,many=isinstance(request.data,list),context={'request': request})
print(serializer)
if serializer.is_valid():
serializer.save(user = request.user,created_by = request.user)
return Response(serializer.data,status=status.HTTP_200_OK)
else:
return Response(serializer.errors,status=status.HTTP_400_BAD_REQUEST)
urls.py
path('orderproduct/',views.OrderProductList.as_view()),
When you call serializer.save(). It's only perform create() action which is only create one and accept dictionary data type only. If you want to save multiple data like that, you will have to override the create function of the serializer class. You can do something similar like this or run a for loop.
serializers.py
def create(self, validate_data):
# Get the data objects you need to perform bulk create
order_products = OrderProduct.objects.bulk_create(validate_data)
return order_products
views.py
if serializer.is_valid(raise_exception=True):
# Replace the serializer.save() by this line to trigger the create method in serializer
self.perform_create(serializer)
return Response(...)

How to dynamically remove fields from serializer output

I'm developing an API with Django Rest framework, and I would like to dynamically remove the fields from a serializer. The problem is that I need to remove them depending on the value of another field. How could I do that?
I have a serializer like:
class DynamicSerliazer(serializers.ModelSerializer):
type = serializers.SerializerMethodField()
url = serializers.SerializerMethodField()
title = serializers.SerializerMethodField()
elements = serializers.SerializerMethodField()
def __init__(self, *args, **kwargs):
super(DynamicSerliazer, self).__init__(*args, **kwargs)
if self.fields and is_mobile_platform(self.context.get('request', None)) and "url" in self.fields:
self.fields.pop("url")
As you can see, I'm already removing the field "url" depending whether the request has been done from a mobile platform. But, I would like to remove the "elements" field depending on the "type" value. How should I do that?
Thanks in advance
You can customize the serialization behavior by overriding the to_representation() method in your serializer.
class DynamicSerliazer(serializers.ModelSerializer):
def to_representation(self, obj):
# get the original representation
ret = super(DynamicSerializer, self).to_representation(obj)
# remove 'url' field if mobile request
if is_mobile_platform(self.context.get('request', None)):
ret.pop('url')
# here write the logic to check whether `elements` field is to be removed
# pop 'elements' from 'ret' if condition is True
# return the modified representation
return ret
You can create multiple serializers and choose the proper one in view
class IndexView(APIView):
def get_serializer_class(self):
if self.request.GET['flag']:
return SerializerA
return SerializerB
use inheritance to make serializers DRY.
My problem was somewhat similar to yours and I solved it with inheritance.
class StaticSerializer(serializers.ModelSerializer):
class Meta:
model = StaticModel
fields = (
'first_name', 'last_name', 'password', 'username',
'email'
)
class DynamicSerializer(StaticSerializer):
class Meta:
model = StaticModel
fields = (
'first_name',
)
Solution (ViewSet mixin)
I have solved this problem by writing my own ViewSet mixin. It provides quite easy and DRY way to override serializers depending on request action.
class ActionBasedSerializerClassMixin(viewsets.ModelViewSet):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
def get_serializer_class(self):
attr_name = f'{self.action}_serializer_class'
if hasattr(self, attr_name):
serializer_class = getattr(self, attr_name)
self.serializer_class = serializer_class
return super().get_serializer_class()
Usage
To use this mixin inherit from it at your viewset (It must be before ModelViewSet parent).
The default serializer is always used as fallback
To use different serializer on list action just set attribute list_serializer_class at your viewset:
class MyViewSet(ViewSet):
serializer_class = MySerializer
list_serializer_class = MyListSerializer
With this code you will have MyListSerializer when action is 'list' and MySerializer for all other actions.
The same patterns works for all other action types: list, create, retrieve, update, partial_update, destroy.
You just need to append _serializer_class to get desired attribute name.
How serailizers should look like
class MySerializer(serializers.ModelSerializer):
some_reverse_rel = MyOtherSerializer(many=True, read_only=True)
class Meta:
model = MyModel
fields = ['field1', 'field2', 'foo', 'bar', 'some_reverse_rel']
class MyListSerailizer(MySerializer): # Note that we inherit from previous serializer
some_reverse_rel = None # Getting rid of reverse relationship
class Meta(MySerializer.Meta):
fields = ['foo', 'bar', 'field1']

Django REST Framework: passing context to a nested serializer

I've a pair of parent-child models/serializers/viewsets - Tool and ToolInput:
models.py:
class Tool(models.Model):
id = models.CharField(max_length=10000, primary_key=True, default=uuid.uuid4, editable=False)
base_command = jsonfield.JSONField(verbose_name="baseCommand")
class ToolInput(models.Model):
tool = models.ForeignKey(Tool, related_name="inputs", on_delete=models.CASCADE)
id = models.CharField(max_length=10000, primary_key=True)
label = models.CharField(max_length=10000, null=True, blank=True)
description = models.CharField(max_length=10000, null=True, blank=True)
type = jsonfield.JSONField()
serializers.py
class ToolSerializer(WritableNestedModelSerializerMixin,
serializers.HyperlinkedModelSerializer):
id = serializers.CharField()
inputs = ToolInputSerializer(many=True)
baseCommand = serializers.JSONField(source="base_command")
class Meta:
model = Tool
fields = ('id', 'inputs', 'baseCommand')
class ToolInputSerializer(WritableNestedModelSerializerMixin,
serializers.HyperlinkedModelSerializer):
class Meta:
model = ToolInput
fields = ('id', 'label', 'description', 'type')
views.py:
class ToolViewSet(viewsets.ModelViewSet):
queryset = Tool.objects.all()
lookup_field = 'id'
serializer_class = ToolSerializer
class ToolInputViewSet(viewsets.ModelViewSet):
lookup_field = 'id'
serializer_class = ToolInputSerializer
def get_queryset(self):
tool_id = self.kwargs['tool_id']
return ToolInput.objects.filter(tool_id=tool_id)
def get_serializer_context(self):
context = super(ToolInputViewSet, self).get_serializer_context()
context["tool"] = Tool.objects.get(id=self.kwargs['tool_id'])
return context
As you can see, I use ToolInputSerializer both as a standalone serializer for ToolInputViewSet and as a nested serializer within ToolViewSet.
When ToolInputSerializer is used as a nested serializer in ToolViewSet, it somehow automagically receives the value of tool argument and assigns it to ToolInput model's tool field (by the way, I feel that it's a totally wrong behavior from architectural point of view - there's no such field as tool on ToolInputSerializer at all and DRF's filling the respective model's field - it should bail out with a Field Does Not Exist error IMO and at least require a write-only field tool on serializer).
But when I use it as a standalone serializer in ToolInputViewSet, I want to assign the value of ToolInput model's tool field to Tool instance, determined by tool_id url parameter, received by ToolInputViewSet in kwargs.
I'm trying to pass the value of that field with serializer context, overriding ToolInputViewSet.get_serializer_context() method, but it's not working. How to do that properly?
Sidenote: I'm pretty tired of the messy and inconsistent, non-uniform automagic of DRF's context handling that pierces layers of Model-Serializer-Field-View architecture. It really needs to be more explicit and customizable.
As for the context, I still don't know how to make it work.
As for how nested serializers work, this is my bad: as you can see, I inherit all the ViewSets from my custom WritableNestedModelSerializerMixin, where I've overridden create() and update() methods to work with nested data structures, so this is my tinkering.
So, as a workaround, I created a separate StandaldonToolInputSerializer and modified ToolInputViewSet, adding the missing tool field to serializer and automatically patching request.data with the Tool reference:
serializers.py
class StandaloneToolInputSerializer(serializers.HyperlinkedModelSerializer):
tool = serializers.PrimaryKeyRelatedField(
write_only=True,
many=False,
queryset=Tool.objects.all()
)
inputBinding = serializers.JSONField(source="input_binding")
class Meta:
model = ToolInput
fields = ('id', 'tool', 'label', 'description', 'type', 'inputBinding')
views.py
class ToolInputViewSet(viewsets.ModelViewSet):
'''
Describes a Tool input.
'''
lookup_field = 'id'
serializer_class = StandaloneToolInputSerializer
def get_queryset(self):
tool_id = self.kwargs['tool_id']
return ToolInput.objects.filter(tool_id=tool_id)
def create(self, request, *args, **kwargs):
request.data["tool"] = self.kwargs['tool_id']
return super(ToolInputViewSet, self).create(request, *args, **kwargs)
def update(self, request, *args, **kwargs):
request.data["tool"] = self.kwargs['tool_id']
return super(ToolInputViewSet, self).update(request, *args, **kwargs)

Resources