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

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

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.

DRF name undefined in custom serializer

When I try to hit my api/atoms/ endpoint in the browser, I am getting a name undefined error in the views.py file, but it has a base name in urls.
Note: this is a non-model serializer and a ViewSet.
error
...views.py", line 74, in list
instance = atom.values(), many=True)
NameError: name 'atoms' is not defined
views.py
class AtomViewSet(viewsets.ViewSet):
serializer_class = AtomSerializer
def list(self, request):
serializer = AtomSerializer(
instance = atoms.values(), many=True) #<-------------
return Response(serializer.data)
urls.py
# for viewsets in views.py
router = DefaultRouter()
router.register(r'snippets', views.SnippetViewSet)
router.register(r'atoms', views.AtomViewSet, base_name='atoms')
urlpatterns = [
path('', views.api_root),
path('', include(router.urls)),
]
serializer.py
class AtomSerializer(serializers.Serializer):
uid = UniqueIdProperty()
created_at = DateTimeProperty()
updated_at = DateTimeProperty()
charge = IntegerProperty()
mass = FloatProperty()
def create(self, validated_data):
return Atom(id=None, **validated_data)
def update(self, instance, validated_data):
for field, value in validated_data.items():
setattr(instance, field, value)
return instance
This is a basic python NameError exception raised when a local or global name is not found.
The variable atoms is not defined in the list() method or globally, that's why the python interpreter raised the exception.
In your code, you'd write atoms.values(), which forces me to think that you are dealing with a QuerySet, which might be an Atom model.
class AtomViewSet(viewsets.ViewSet):
serializer_class = AtomSerializer
def list(self, request):
serializer = AtomSerializer(instance=Atom.objects.all(), many=True)
return Response(serializer.data)
Note: this is a non-model serializer and a ViewSet.
You are doing create and update operations in your AtomSerializer class, and those are directly connected to the model. I don't see any particular reason that pulls you back from using a ModelSerializer here. Apart from that, you are using the routers, which become a good choice when you deal with the CRUD operations, hence I strongly suggest you use the combination of ModelViewset and ModelSerializer in your code.
In your views.py you did not define atom, you need to define it first before using it or else you will get that error.
class AtomViewSet(viewsets.ViewSet):
serializer_class = AtomSerializer
def list(self, request):
# You need to define the atom first before passing it to your AtomSerializer
atoms = [] # or atom = Atom.objects.all()
serializer = AtomSerializer(
data=atoms,
many=True
)
return Response(serializer.data)

Getting "AttributeError: 'QuerySet' object has no attribute '_meta'" on django rest "PUT" method

I am trying to update the record with PUT method, Getting AttributeError: 'QuerySet' object has no attribute '_meta'.
My models.py:
class TableInfo(models.Model):
table_name = models.CharField(max_length=10)
columns = JSONField(null=False)
serializer.py:
class TableInfoSerializer(serializers.ModelSerializer):
class Meta:
model = TableInfo
fields = '__all__'
views.py :
#api_view(['GET','PUT'])
def table_info(request):
try:
queryset = TableInfo.objects.all()
print("1")
except TableInfo.DoesNotExist:
return Response(status=status.HTTP_404_NOT_FOUND)
if request.method == 'GET':
print("2")
serializer_class = TableInfoSerializer(queryset, many=True)
return Response(serializer_class.data)
elif request.method == 'PUT':
print(request.data)
serializer = TableInfoSerializer(queryset, data=request.data)
if serializer.is_valid():
serializer.save()
print("4")
return HttpResponse(serializer.data)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
code is breaking at if serializer.is_valid():
On "GET" I am getting the result. Please help me with "PUT" method.
This error happens with PUT because the serializer tries to access the Meta class on the model instance it is updating, but fails because you are not passing a model instance - you're passing a queryset as indicated in the comments.
So you need to pass an instance, and to specify which instance you would normally pass the instance id via the URL. For that you would be best to separate out your views, and create a table_detail view for retrieving and updating a specific instance.
#api_view(['GET','PUT'])
def table_detail(request, pk):
try:
table_info = TableInfo.objects.get(pk=pk) # Lookup a specific object
except TableInfo.DoesNotExist:
return Response(status=status.HTTP_404_NOT_FOUND)
if request.method == 'GET':
serializer_class = TableInfoSerializer(table_info)
return Response(serializer_class.data)
elif request.method == 'PUT':
serializer = TableInfoSerializer(table_info, data=request.data)
if serializer.is_valid():
serializer.save()
return Response(serializer.data)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
Your table_info view can then just handle the list operation.
#api_view(['GET'])
def table_info(request):
if request.method == 'GET':
queryset = TableInfo.objects.all()
serializer_class = TableInfoSerializer(queryset, many=True)
return Response(serializer_class.data)

How to make QueryDict instance is mutable in django

I have a serialized classs like this:
class EmployeeSerializer(serializers.ModelSerializer):
# TODO: Define serializer fields here
bio = BioSerializer()
designation = GroupListSerializer()
department = GroupListSerializer()
#user = UserSerializer()
class Meta:
model = Employee
fields = '__all__'
# fields = ['user','tax_id_number','account_number','joining_date','designation','department','gender','marital_status','id_type','birthday','ethnicity','preferred_language','phone_number','em_contact','address']
def create(self,validated_data):
bio = validated_data.pop('bio')
designation = validated_data.pop('designation')
department = validated_data.pop('department')
new_bio = Bio.objects.create(**bio)
new_designation = Groups.objects(**designation)
new_department = Groups.objects(**department)
employee = list(Employee.objects.create(designation=new_designation,department=new_department,
bio=new_bio,**validated_data))
employee = json.dumps(employee)
return employee
And my views are as follows:
class EmployeeRecordView(generics.CreateAPIView):
queryset=Employee.objects.all()
serializer_class=EmployeeSerializer
def post(self, request):
serializer = EmployeeSerializer(data=request.data)
if serializer.is_valid(raise_exception=ValueError):
serializer.create(validated_data=request.data)
return Response(serializer.data, status=status.HTTP_201_CREATED)
return Response(serializer.error_messages,
status=status.HTTP_400_BAD_REQUEST)
The problem is when I try to make a 'POST' request I get the following logs below:
Exception Type: AttributeError
Exception Value:
This QueryDict instance is immutable
How do I make POSTing successfull. Where am I going wrong with this approach I have implemented above?
You shouldn't have to call serializer.create yourself - I believe the line that is throwing the error is serializer.create(validated_data=request.data). You can try this:
...
serializer.is_valid(raise_exception=True)
serializer.save()
return Response(serializer.data, status=status.HTTP_201_CREATED)
Another thing is when you set raise_exception=True, it already throws an error response so you don't need to put it in an if/else block.

Resources