In my restful CreateAPIView I mutate my request.data dictionary.
Occasionally I receive an error not caught by my tests:
This QueryDict instance is immutable
For e.g. this:
class CreateView(CreateAPIView):
serializer_class = ...
queryset = ...
def post(self, request, *args, **kwargs):
request.data['user'] = request.user.pk
return self.create(request, *args, **kwargs)
request.data seems to be a normal dict in my tests. Why is it sometimes a QueryDict? How should this be dealt with? Should request.data not be mutated in general? How should you use the ModelSerializer class, when you need to populate some fields yourself?
Why this occasional behavior?
When we look into the SC of Request (as #Kenny Ackerman mentioned), it return a QueryDict object if you are passing a form media type ('application/x-www-form-urlencoded' or 'multipart/form-data') data to the view class. This check being execute within the is_form_media_type() method of Request class.
If you are passing a application/json data to the view, the request.data will be a dict object.
How to reproduce the behaviour?
It can be reproduce by using sending different ContentType data into view. (In POSTMAN tool, use form-data and raw JSON to get the behaviour)
How to get current logged-in user in serializer?
Method-1 pass extra argument to .save() (as #Linovia mentioned) by overriding the perform_create() method
class CreateView(CreateAPIView):
serializer_class = ...
queryset = ...
def post(self, request, *args, **kwargs):
request.data['user'] = request.user.pk
return self.create(request, *args, **kwargs)
def perform_create(self, serializer):
serializer.save(user=self.request.user)
Method-2 Use CurrentUserDefault() class as below
from django.contrib.auth import get_user_model
User = get_user_model()
class MySerializer(serializers.ModelSerializer):
user = serializers.PrimaryKeyRelatedField(queryset=User.objects.all(), default=serializers.CurrentUserDefault())
class Meta:
# your code
When you have to modify a QueryDict object received from a request, it is a immutable object, instead use this line of code if you wanna add attributes:
myNewRequest = request.GET.copy()
myNewRequest.data['some_attr'] = float(something)
based on the source code parser returns a querydict for data when the stream is empty(request.data calls _load_data_and_files method and _load_data_and_files calls _parse method).
and I think you can populate the fields using HiddenField or you can override the create or update method. for example
class TestSerializer(serializers.ModelSerializer):
user = serializers.HiddenField(default=serializers.CurrentUserDefault())
class Meta:
model = Test
fields = ('id', 'text', 'user')
def create(self, validated_data):
validated_data['populate_field'] = 'value'
return super().create(validated_data)
Note that this really depends on your selected parser specified in rest_framework's DEFAULT_PARSER_CLASSES and the content type of your request:
JSONParser is implemented as follows:
return json.load(decoded_stream, parse_constant=parse_constant)
FormParser as follows:
return QueryDict(stream.read(), encoding=encoding)
Related
I have two models: ModelA and ModelB, with their corresponding serializers ModelASerializer and ModelBSerializer
In a specific viewset, called MyViewSet i have the follwing structure:
class MyViewSetRoot(viewsets.ModelViewSet):
http_method_names = ["get"]
# The returned values are of type "ModelA", so I need it to use that serializer
serializer_class = ModelASerializer
queryset = ""
Finally, in my actual view, I do something like this:
class MyViewSet(MyViewSetRoot):
get(self, request: HttpRequest, *args, **kwargs) -> Response:
ModelA_queryset = ModelA.objects.all()
return Response(
data=ModelA_queryset,
status=status.HTTP_200_OK,
)
I would expect in that case for the queryset to be serialized using the ModelASerializer that I specified in the serializer_class field. However, I get the error
Object of type ModelA is not JSON serializable
If I do this instead:
class MyViewSet(MyViewSetRoot):
get(self, request: HttpRequest, *args, **kwargs) -> Response:
ModelA_queryset = ModelA.objects.all()
serialized_queryset = ModelASerializer(ModelA_queryset, many=True)
return Response(
data=serialized_queryset.data,
status=status.HTTP_200_OK,
)
It works just fine, but I want to avoid serializing explicitly in the view.
Any ideas on what could be actually going on? Am I forced to serialize explicitly in this case?
I think you don't need to customize the get function. In ModelViewSet, the function for the GET API, is list or retrieve. But you don't need to redefine it.
class MyViewSetRoot(viewsets.ModelViewSet):
http_method_names = ["get"]
serializer_class = ModelASerializer
queryset = ModelA.objects.all()
class MyViewSet(MyViewSetRoot):
pass
In DRF the serializer validate() method is not calling by default.
i am using the serializer like this:
class SampleListView(ListAPIView):
queryset = Sample.objects.all()
serializer_class = SampleSerializer
def list(self, request, *args, **kwargs):
queryset = self.filter_queryset(self.get_queryset())
serializer = self.get_serializer(queryset, many=True)
return Response(sorted_result)
class SampleSerializer(serializers.ModelSerializer):
custom_data = serializers.SerializerMethodField()
class Meta:
model = SampleModel
fields = ('field_1', 'field_2')
def validate(self, data):
return data
Execution not enter into the validate() method in serializer.
Anyone have an idea about this?
Shijo Validate() only be called when you are using save method of serializer hence only be used for create and update method of API.
As refer in Validation in DRF documentation
you always need to call is_valid() before attempting to access the validated data, or save an object instance.
I have a ModelSerializer. I was trying to set a user foreign key when saving a model, instantiated via the create() and update() methods of a ModelViewSet class. Eg:
ModelViewSet:
def create(self, request):
serializer = self.get_serializer(data=request.data, many=isinstance(request.data, list))
if not serializer.is_valid():
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
serializer.save()
return Response(serializer.data, status=status.HTTP_201_CREATED)
Serializer
def process_foreign_keys(self, validated_data):
""" Simplified for SO example """
profile = get_object_or_404(Profile, fk_user=CurrentUserDefault())
validated_data['profile'] = profile
return validated_data
def create(self, validated_data):
""" Create a Product instance. Routed from POST via DRF's serializer """
validated_data = self.process_foreign_keys(validated_data)
return Product.objects.create(**validated_data)
That code doesn't work - it throws an exception on the get_object_or_404 line:
int() argument must be a string, a bytes-like object or a number, not 'CurrentUserDefault'
If I put a few debugging statements in the ModelSerializer.create() method, I get weird stuff:
currentuser = CurrentUserDefault()
# Expect <class django.contrib.auth.models.User>, get <class 'rest_framework.fields.CurrentUserDefault'>
print("currentuser type " + str(type(currentuser)))
# Causes AttributeError: 'CurrentUserDefault' object has no attribute 'user'
print("currentuser is " + str(currentuser.__call__()))
# Causes AttributeError: 'ProductSerializer' object has no attribute 'request'
print("currentuser is " + str(self.request.user))
All this was done while a user was logged in, so it's not an AnonymousUser problem.
What am I screwing up? How do I get the current user in a serializer instantiated within the create/update methods of a ModelViewSet via self.get_serializer()?
Edit: Attempting with a HiddenField doesn't seem to work either. From the docs :
"HiddenField: This field will be present in validated_data but will not be used in the serializer output representation."
So I set as a ModelSerializer class field:
currentuser = serializers.HiddenField(default=serializers.CurrentUserDefault())
... and then attempt validated_data.get('currentuser') in the update method, and that returns None.
CurrentUserDefault is not a magic method that gets the user out of the void. It has to be within the context of a field a shown in the documentation
As #pramod pointed out, you need to either:
get_object_or_404(Profile, fk_user=self.request.user)
or set a CurrentUserDefault as a default value for a field.
def create(self, request):
serializer = self.get_serializer(data=request.data, many=isinstance(request.data, list), context={"request": request})
can you change you serializer instantiation to above code and use it as below:
profile = get_object_or_404(Profile, fk_user=self.request.user)
Lets say i have a model like so:
class MyModel(models.Model):
first_field = models.CharField()
second_field = models.CharField()
and an API view like so:
class MyModelDetailAPI(GenericAPIView):
serializer_class = MyModelSerializer
def patch(self, request, *args, **kwargs):
# Do the update
def post(self, request, *args, **kwargs):
# Do the post
The first_field is a field that is only inserted in the POST method (and is mandatory) but on each update, the user can't change its value so the field in the PATCH method is not mandatory.
How can i write my serializer so that the first_field is required on POST but not required on PATCH. Is there any way of dynamically setting the required field so i can still use the DRF validation mechanism? Some sort of validator dispatcher per request method?
I want something like this for example:
class MyModelSerializer(serializers.ModelSerializer):
class Meta:
model = MyModel
fields = {
'POST': ['first_field']
'PATCH': []
}
I need more space than comments provide to make my meaning clear. So here is what I suggest:
Different formatting means different serializers.
So here you have, for instance a MyModelSerializer and a MyModelCreationSerializer. Either create them independently, or have one inherit the other and specialize it (if it makes sense).
Use the appropriate GenericAPIView hook to return the correct serializer class depending on self.action. A very basic example could be:
class MyModelDetailAPI(GenericAPIView):
# serializer_class = unneeded as we override the hook below
def get_serializer_class(self):
if self.action == 'create':
return MyModelCreationSerializer
return MyModelSerializer
Default actions in regular viewsets are documented here, they are:
create: POST method on base route url
list: GET method on base route url
retrieve: GET method on object url
update: PUT method on object url
partial_update: PATCH method on object url
destroy: DELETE method on object url
I am using django-rest-framework generic views to create objects in a model via POST request. I would like to know how can I return the id of the object created after the POST or more general, any additional information about the created object.
This is the view class that creates (and lists) the object:
class DetectorAPIList(generics.ListCreateAPIView):
serializer_class = DetectorSerializer
permission_classes = (permissions.IsAuthenticatedOrReadOnly,)
parser_classes = (MultiPartParser, FileUploadParser,)
def pre_save(self, obj):
obj.created_by = self.request.user.get_profile()
def get_queryset(self):
return (Detector.objects
.filter(get_allowed_detectors(self.request.user))
.order_by('-created_at'))
The model serializer:
class DetectorSerializer(serializers.ModelSerializer):
class Meta:
model = Detector
fields = ('id', 'name', 'object_class',
'created_by', 'public', 'average_image', 'hash_value')
exclude = ('created_by',)
Thanks!
Here, DetectorSerializer inherits from ModelSerializer as well as your view inherits from generics ListCreateAPIView so when a POST request is made to the view, it should return the id as well as all the attributes defined in the fields of the Serializer.
Because it took me a few minutes to parse this answer when I had the same problem, I thought I'd summarize for posterity:
The generic view ListCreateApiView does return the created object.
This is also clear from the documentation listcreateapiview: the view extends createmodelmixin, which states:
If an object is created this returns a 201 Created response, with a serialized representation of the object as the body of the response.
So if you have this problem take a closer look at your client side!
post$.pipe(tap(res => console.log(res)))
should print the newly created object (assuming rxjs6 and ES6 syntax)
As mentioned above, To retrieve the id for the new created object, We need to override the post method, find the the update code for more details:
class DetectorAPIList(generics.ListCreateAPIView):
serializer_class = DetectorSerializer
permission_classes = (permissions.IsAuthenticatedOrReadOnly,)
parser_classes = (MultiPartParser, FileUploadParser,)
def post(self, request, format=None):
serializer = DetectorSerializer(data=request.data)
if serializer.is_valid():
obj = serializer.save()
return Response(obj.id, status=status.HTTP_201_CREATED)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)