validating json fields - django-rest-framework

This is my serializer file
from rest_framework import serializers
#All validations here
class BudgetSerializer(serializers.Serializer):
start_date = serializers.DateTimeField(format="%Y-%m-%d %H:%M:%S")
end_date = serializers.DateTimeField(format="%Y-%m-%d %H:%M:%S")
lifetime_budget = serializers.FloatField(max_value=None, min_value=None,allow_null=True)
def validate(self, attrs):
if attrs['start_date'] > attrs['end_date']:
raise serializers.ValidationError("start_date must be less than end date")
return attrs
class IoSerializer(serializers.Serializer):
name = serializers.CharField()
state = serializers.CharField()
currency = serializers.CharField(read_only=True)
budget_type = serializers.CharField(read_only=True)
budget_intervals = BudgetSerializer(many=True)
Json which i am serializing has 1000's of fields but i only want to validate few fields and return all json fields.But problem with serialzer is that it only outputs json consisting of mentioned field in serialiser.
Like i am sending json with 1000s of object names but it only return json which consist of start_date,end_date,lifetime_budget,name because it is mentioned in serializer file.I wanted to only validate few fields so i wrote them in serializer file but only this fields are returned.
My views.py code
class InsertionOrderViewset(viewsets.ViewSet):
def create(self, request, format=None):
advertiser_id = request.query_params.get('advertiser_id', None)
data = json.dumps(request.data)
io_object = json.loads(data, object_hook=lambda x: namedtuple('io_object', x.keys())(*x.values()))
serializer = IoSerializer(data=request.data, instance=io_object)
serializer.is_valid(raise_exception=True)
response_data = IO.create(data=request.data, params={"advertiser_id": advertiser_id })
return Response(request.data)

serializers.data method will only return values which are supplied to it unless you declare some read_only fields and or do any of these - supply an instance during serializer initialization, return an instance from serializer.save method. When serializer.instance has a value then read_only fields are returned with serializer.data. Your use case suggests that the incoming JSON object has 1000s of data of which only a small portion requires validation. My suggestion will be not to pass all the data, pluck only the necessary portion of the data, send them to serializer and do validation, return serializer.errors if any else rejoin the serializer.data with the incoming JSON and return the JSON. Since, majority of the data has nothing to do with validation, passing them in to the serializer will cause significant performance issue due to the way drf-serializer validates and maps values.
Still if you still want to supply all of the JSON to serializer and validate only a few consider supplying an instance when initializing serializer, dynamically declare ReadOnlyField in your serializer.__init__
serializers.py
from rest_framework import serializers
# default attributes of objects
default_attrs = ['__doc__', '__module__', '__slots__', '_fields', '_source']
class BudgetSerializer(serializers.Serializer):
start_date = serializers.DateTimeField(format="%Y-%m-%d %H:%M:%S")
end_date = serializers.DateTimeField(format="%Y-%m-%d %H:%M:%S")
lifetime_budget = serializers.FloatField(max_value=None, min_value=None,allow_null=True)
def __init__(self, *args, **kwargs):
super(BudgetSerializer, self).__init__(*args, **kwargs)
instance = kwargs.get('instance', None)
for attr in dir(instance):
if attr not in self.fields and not callable(getattr(instance, attr)) and attr not in default_attrs:
self.fields[attr] = serializers.ReadOnlyField()
def validate(self, attrs):
if attrs['start_date'] > attrs['end_date']:
raise serializers.ValidationError("start_date must be less than end date")
return attrs
class IoSerializer(serializers.Serializer):
name = serializers.CharField()
state = serializers.CharField()
currency = serializers.CharField(read_only=True)
budget_type = serializers.CharField(read_only=True)
budget_intervals = BudgetSerializer(many=True)
def __init__(self, *args, **kwargs):
super(IoSerializer, self).__init__(*args, **kwargs)
instance = kwargs.get('instance', None)
for attr in dir(instance):
if attr not in self.fields and not callable(getattr(instance, attr)) and attr not in default_attrs:
self.fields[attr] = serializers.ReadOnlyField()
inside views.py
import json
from collections import namedtuple
# inside view
data = json.dumps(request.data)
io_object = json.loads(data, object_hook=lamdba x: namedtuple('io_object', x.keys())(*x.values()))
serializer = IoSerializer(data=request.data, instance=io_object)
serializer.is_valid(raise_exception=True)
print(serializer.data)

Related

AttributeError: 'collections.OrderedDict' object has no attribute 'model_id' and 'model_id' is missing from visible fields

Something strange happened: I was defining an endpoint and initially two fields were visible in the API form: model_id and payload, as given in the model definition:
### models.py:
class CarModel(models.Model):
model_id = models.CharField(max_length=10, primary_key=True)
name = models.CharField(max_length=40)
active = models.BooleanField(default=True)
def __str__(self):
return self.model_id
class Calculator(models.Model):
model = models.ForeignKey(CarModel, on_delete=models.CASCADE)
payload = models.TextField()
def model_id(self):
return self.model.model_id
def __str__(self):
return f"Calculations for {self.model.name}"
### serializers.py:
class CalculatorSerializer(serializers.ModelSerializer):
model_id = serializers.SerializerMethodField()
class Meta:
model = Calculator
fields = ['model_id', 'payload']
def get_model_id(self, obj):
return obj.model_id()
### views.py:
class CalculatorViewSet(viewsets.ModelViewSet):
serializer_class = CalculatorSerializer
queryset = Calculator.objects.all()
def create(self, request, *args, **kwargs):
serializer = self.get_serializer(data=request.data)
serializer.is_valid(raise_exception=True)
return Response(f"{serializer.data.upper()}", status=status.HTTP_200_OK)
So, both fields were visible, but POST requests ended in the AttributeError: 'collections.OrderedDict' object has no attribute 'model_id'. Trying to fix that, I eventually and accidentally removed model_id from view - it doesn't display in DRF's forms. And the AttributeError still persists.
What is wrong with this piece of code?
OK, it turns out that defining fields in this manner:
fields = '__all__'
makes also the model_id visible. Still, no idea why explicit insert doesn't work.
In case of the other issue, the AttributeError, I had to pull the value out of an OrderedDict. Modified method looks like this:
def get_model_id(self, obj):
return obj["model"].model_id
Beside that, I found one more error inside views.py's create method: serializer.data won't implement upper() method; some key, in my case serializer.data['payload'], has to be referenced, so for example:
def create(self, request, *args, **kwargs):
serializer = self.get_serializer(data=request.data)
serializer.is_valid(raise_exception=True)
res = {
"payload": f"{serializer.data['payload'].upper()}"
}
return Response(res, status=status.HTTP_200_OK)

Why is invalid data saved to database anyway when custom `create` method is used?

I'm creating a custom create method to process incoming data. It gets validated by a serializer:
# serializers.py:
class ItemVersionSerializer(serializers.ModelSerializer):
item_ver_id = serializers.RegexField(regex='^r\d{2}$', allow_blank=False)
session_id = serializers.RegexField(regex='^s\d{2}$', allow_blank=False)
config = serializers.CharField(min_length=6)
item_ver = serializers.CharField(min_length=6)
(...)
The method itself looks like this:
# views.py:
class ItemVersionViewSet(viewsets.ModelViewSet):
serializer_class = ItemVersionSerializer
lookup_field = 'item_ver_id'
def create(self, request, *args, **kwargs):
data = request.data
model = ItemModel.objects.get(item_id=data["model"])
finished = True if "finished" in data else False
item_version = ItemVersion.objects.create(
model=model,
item_ver_id=data["item_ver_id"],
config=data["config"],
item_ver=data["item_ver"],
session_id=data["session_id"],
finished=finished
)
serializer = ItemVersionSerializer(data=request.data)
if serializer.is_valid():
# item_version.save() # data gets saved even with this line off
return Response(serializer.validated_data)
else:
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
When passing incorrect data, e.g. too short item_ver string, I receive error 400 and this info:
{
"item_ver": [
"Ensure this field has at least 6 characters."
]
}
However, when I correct the data and send it again, I receive IntegrityError: UNIQUE constraint failed: core_itemversion.item_ver_id and looking at the list of instances I can see the new entry saved to the database anyway.
Which method is responsible for saving the data?
Make sure that you validate your serializer BEFORE creating. Like this:
def create(self, request, *args, **kwargs):
serializer = self.get_serializer(data=request.data)
serializer.is_valid(raise_exception=True)
serializer.save()
headers = self.get_success_headers(serializer.data)
return Response(serializer.data, status=status.HTTP_201_CREATED, headers=headers)
As you can see, there is no need for if else when doing serializer.is_valid(), just use raise_exception=True, and the viewset will make sure that the response will be a 400_BAD_REQUEST.
Second, don't do Model.objects.create(), then save. Do ModelSerializer(request.data), and then follow the convention.
Hopefully this makes sense to you, do let me know if you need me to expand.

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(...)

django rest_framework serializer parameter

I am using django rest_framework to provide jsGrid json data.
As the rest_framwork example, I create a object fit the jsGrid format
class jsGridResp(object):
def __init__(self, data, itemsCount):
self.data = data
self.itemsCount = itemsCount
and the class based view, create a get function
class RateListViewSet(mixins.CreateModelMixin,
mixins.UpdateModelMixin,
mixins.DestroyModelMixin,
generics.GenericAPIView):
"""
API endpoint that allows user to be viewed or edited
"""
queryset = RateList.objects.all().order_by('-create_date')
serializer_class = RateListSerializer
def get_object(self):
queryset = self.filter_queryset(self.get_queryset())
obj = queryset.get(pk=self.request.POST["id"])
self.check_object_permissions(self.request, obj)
return obj
def get(self, request, format=None):
pageIndex = request.GET.get('pageIndex')
pageSize = request.GET.get('pageSize')
sortField = request.GET.get('sortField', 'id')
sortOrder = request.GET.get('sortOrder', 'asc')
sortOrder = "" if sortOrder == "asc" else "-"
rows = RateList.objects.all().order_by("{}{}".format(sortOrder, sortField))
itemsCount = rows.count()
paginator = Paginator(rows, pageSize)
try:
rows = paginator.page(pageIndex)
except PageNotAnInteger:
rows = paginator.page(1)
except EmptyPage:
rows = paginator.page(paginator.num_pages)
result = jsGridResp(data=rows, itemsCount=itemsCount)
serializer = RateListGetSerializer(result)
json = JSONRenderer().render(serializer.data)
return Response(json)
then I create two serializer to serialize the data
class RateListSerializer(serializers.ModelSerializer):
class Meta:
model = RateList
fields = ('id', 'rate_code', 'hr01', 'hr02', 'hr03', 'hr04', 'hr05', 'hr06',
'hr07', 'hr08', 'hr09', 'hr10', 'hr11', 'hr12', 'hr13', 'hr14',
'hr15', 'note', 'create_date', 'update_date')
read_only_fields = ('create_date', 'update_date')
def update(self, instance, validated_data):
result = instance.update(id=instance.id, **validated_data)
return result
def destroy(self, instace, validated_data):
return "{seccuess: true}"
class RateListGetSerializer(serializers.Serializer):
itemsCount = serializers.IntegerField()
data = RateListSerializer(many=True)
but I have many model need to do like these.
can I use just one serializer to serialize all model.
I want to create a jsGridGetSerializer can pass in a model parameter, so I don't need to create many simple serialzer to do the same thing.
Is this possible?
If I understand your problem correctly, you want a generic serializer which accepts a Model variable as a parameter in its Meta class.
One way to do this is to pass in the model name as a url keyword argument, then catching it in views.py. Then you can override the Meta model via overriding the get_serializer_class:
serializers.py
class GenericSerializer(serializers.ModelSerializer):
class Meta:
model = None
views.py
class GenericViewSet(viewsets.ModelViewSet):
def get_queryset(self):
model = self.kwargs.get('model')
return model.objects.all()
def get_serializer_class(self):
GenericSerializer.Meta.model = self.kwargs.get('model')
return GenericSerializer

Where to add default value in CreateAPIView?

I have a ListCreateAPIView, on which I want to populate a field with a default value in case of not provided by request.DATA.
Problem is: where should I do that ?
I can't modify the request.DATA because it is immutable and I don't want to copy/paste the CreateMixin implementation.
Here is my code:
class ObjectiveList(generics.ListCreateAPIView):
model = Objective
serializer_class = ObjectiveSerializer
permission_classes = (permissions.IsAuthenticated,)
def create(self, request, *args, **kwargs):
# provide a default value
objective_definition_id = request.DATA.get('objective_definition',-1)
data = request.DATA.copy()
if objective_definition_id == -1:
# support for 0.9.1 version of iOS and below
logger.info(str(self.request.DATA))
mission_url = request.DATA["mission"]
objectivedefinition_pk = self.default_objectivedefinition_id(mission_url)
data["objective_definition"]=objectivedefinition_pk
# I would want to do something like this but I can't
# request.DATA = data
# super(ObjectiveList,self).create(request, *args, **kwargs)
# copy/paste of the super class implementation
serializer = self.get_serializer(data=data, files=request.FILES)
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)
Here is my serializer:
class ObjectiveSerializer(serializers.HyperlinkedModelSerializer):
objective_definition = serializers.PrimaryKeyRelatedField(many=False, read_only=False, required=False, default=toto)
class Meta:
model = Objective
fields = (
'url',
'objective_definition',
)
You can use the default= argument on the field, don't know if that meets your use case?

Resources