Why is invalid data saved to database anyway when custom `create` method is used? - django-rest-framework

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.

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.

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)

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 get request user data in django rest to representation function

I need to access request.user in to_representation function, I tried self.context['request'] but self.context is empty dictionary. Is there anyway to access request.user or any way that I can push this data to this function?
def to_representation(self, obj):
print(self.context)
#output is an empty dictionary {}
UPDATE
class RetrieveView(generics.RetrieveAPIView):
def get(self, request, *args, **kwargs):
uid = kwargs.get('uid')
try:
item = self.model.nodes.get(uid=uid)
except Exception as e:
# error response
serializer = self.serializer_class(item)
return HttpSuccessResponse(SuccessResponse(serializer.data).to_json(), status=status.HTTP_200_OK).send()
class TopicRetrieveView(single_result.RetrieveView):
model = Topic
serializer_class = topic.TopicSerializer
ALL CODES are from django rest framwork Generic views generic.py
serializer_class is attribute we set in class definition or we need to override get_serializer_class function. It will handle in this function:
def get_serializer_class(self):
"""
Return the class to use for the serializer.
Defaults to using `self.serializer_class`.
You may want to override this if you need to provide different
serializations depending on the incoming request.
(Eg. admins get full serialization, others get basic serialization)
"""
assert self.serializer_class is not None, (
"'%s' should either include a `serializer_class` attribute, "
"or override the `get_serializer_class()` method."
% self.__class__.__name__
)
return self.serializer_class
get_serializer_class will used in get_serializer function:
def get_serializer(self, *args, **kwargs):
"""
Return the serializer instance that should be used for validating and
deserializing input, and for serializing output.
"""
serializer_class = self.get_serializer_class()
kwargs['context'] = self.get_serializer_context()
return serializer_class(*args, **kwargs)
and context will fill by get_serializer_context function.
def get_serializer_context(self):
"""
Extra context provided to the serializer class.
"""
return {
'request': self.request,
'format': self.format_kwarg,
'view': self
}
So correct usage is serializer = self.get_serializer(item) because it will use serializer_class for serializing item and fill context with extra information that may be helpful. serializer = self.serializer_class(item) can be used for just serializing item with no more extra information.

How can one return 400 status code with DRF APIView get method and ModelSerializer?

I have the following code (for the endpoint /things/{id}/permission-to-do/
views.py
class PermissionsToDo(APIView):
authentication_classes = (TokenAuthentication,)
permission_classes = (IsAuthenticated,)
def get(self, request,*args,**kwargs):
thing_id = kwargs.get('pk')
thing = Thing.objects.filter(pk=thing_id,is_active=True)
serializer = serializers.GetDoPermissionSerializer(thing[0],context={'request': request})
return Response(serializer.data, status=status.HTTP_200_OK)
serializers.py
class serializers.GetDoPermissionSerializer(serializers.ModelSerializer):
def _can_do(self, thing):
return thing.can_be_done_by(self.context['request'].user)
can_do = serializers.SerializerMethodField('_can_do')
class Meta:
model = Thing.objects.filter
fields = ('can_do',)
extra_kwargs = {
'can_do': {'read_only': True},
}
The thing.can_be_done_by(user) method returns a Boolean. This works fine with a correct request but I want to add a way to validate the request and send appropriate status code for client errors, such as status.HTTP_400_BAD_REQUEST
My idea was to just add in views.py a:
if serializer.is_valid():
return Response(serializer.data, status=status.HTTP_200_OK)
else:
return Response(SOMETHING?,status.HTTP_400_BAD_REQUEST)
if I evaluate serializer.is_valid() I obtain an error message saying:
'Cannot call `.is_valid()` as no `data=` keyword argument was '
AssertionError: Cannot call `.is_valid()` as no `data=` keyword argument was passed when instantiating the serializer instance.
if I change the line:
serializer = serializers.GetDoPermissionSerializer(thing[0],context={'request': request})
into:
serializer = serializers.GetDoPermissionSerializer(data=thing[0],context={'request': request})
but then, I get an error suggesting the I should be passing a dictionary as data and not an object. but then I'm not sure how to implement the validate method and how to change the _can_do method to get it to work.
Any idea?
Thanks for your time if you have some to spare!
.validate() method takes a single argument, which is a dictionary of field values. It should raise a ValidationError if necessary, or just return the validated values.
One of the ways you can achieve this is to convert the model object to a dict. So try the following snippet,
class PermissionsToDo(APIView):
authentication_classes = (TokenAuthentication,)
permission_classes = (IsAuthenticated,)
def get(self, request, *args, **kwargs):
thing_id = kwargs.get('pk')
thing = Thing.objects.filter(pk=thing_id, is_active=True)
serializer = serializers.GetDoPermissionSerializer(data=thing[0].__dict__, context={'request': request}) # Change is here <<<<
if serializer.is_valid():
return Response(serializer.data, status=status.HTTP_200_OK)
return Response(SOMETHING?, status.HTTP_400_BAD_REQUEST)
My suggestion
If you are serializing the object from DB, most of the time it won't raise any validation error. I would suggest that, try to show error message if thing object become a empty queryset.So,
class PermissionsToDo(APIView):
authentication_classes = (TokenAuthentication,)
permission_classes = (IsAuthenticated,)
def get(self, request, *args, **kwargs):
thing_id = kwargs.get('pk')
try:
serializer = serializers.GetDoPermissionSerializer(Thing.objects.get(id=thing_id), context={'request': request})
return Response(serializer.data, status=status.HTTP_200_OK)
except Thing.DoesNotExist:
return Response(data="object not found", status.HTTP_400_BAD_REQUEST)
I used your "suggestion" to the exception that I had to modify the last line. I replaced:
return Response(data="object not found", status.HTTP_400_BAD_REQUEST)
by :
return Response("object not found", status.HTTP_400_BAD_REQUEST)
or I get the following error:
SyntaxError: positional argument follows keyword argument

Resources