Update objects with DRF Serializer with data from JSON - django

I get a JSON response with data from multiple objects, run everything through the serializer and call serializer.save() which creates new objects but doesnt update the existing ones.
view
data = JSONParser().parse(request)
serializer = CellCESaveSerializer(data=data, many = True, context = {'descriptoridlist' : descriptoridlist}, partial=True)
if serializer.is_valid():
serializer.save()
return JsonResponse(serializer.data, safe=False)
the serializer
class StringArrayField(ListField):
"""
String representation of an array field.
"""
def to_representation(self, obj):
obj = super().to_representation(obj)
# convert list to string
return ",".join([str(element) for element in obj])
def to_internal_value(self, data):
data = data.rstrip(",") # Needed because of JSON response
data = data.split(",") # Split string to list
data = [int(i) for i in data] # convert str to int
did = self.context['descriptoridlist']
x = (did,data) # Create Array
return super().to_internal_value(x)
class CellCESaveSerializer(serializers.ModelSerializer):
row = serializers.SlugRelatedField(slug_field="name",queryset=Descriptor.objects.all() ) # foreignkey mit namen
val = StringArrayField()
class Meta:
model = CellCE
fields = ('row','val')
How can i update the existing objects without creating new objects?
EDIT 1:
After adding #neverwalkaloner changes i realised i need to use ListSerializer to update multiple objects. I adjusted my code with help of the DRF documentation. (I changed the serializer to a ListSerializer, added the id Field and the update method.)
serializer:
class CellCESaveSerializer(serializers.ListSerializer):
row = serializers.SlugRelatedField(slug_field="name",queryset=Descriptor.objects.all()) # foreignkey mit namen
val = StringArrayField()
id = serializers.IntegerField()
class Meta:
model = CellCE
fields = ('row','val')
def update(self, instance, validated_data):
CellCE_mapping = {CellCE.id: CellCE for CellCE in instance}
data_mapping = {item['id']: item for item in validated_data}
# Perform creations and updates.
ret = []
for CellCE_id, data in data_mapping.items():
CellCE = CellCE_mapping.get(CellCE_id, None)
if CellCE is None:
ret.append(self.child.create(data))
else:
ret.append(self.child.update(CellCE, data))
# Perform deletions.
for CellCE_id, CellCE in CellCE_mapping.items():
if CellCE_id not in data_mapping:
CellCE.delete()
return ret
I get the error:
AssertionError: `child` is a required argument.
What am i missing?

Serializer's save() method check if serializer's self.instance is empty or not. If self.instance is empty save() will call create() method, otherwise it will call update() method.
So to update instance with serializer, you need to pass as it's first argument updating object:
obj = CellCE.objects.get(id='some_id') # Object wich will be updated
serializer = CellCESaveSerializer(obj, data=data, many = True, context = {'descriptoridlist' : descriptoridlist}, partial=True)
if serializer.is_valid():
serializer.save()
return JsonResponse(serializer.data, safe=False)

No need to use ListSerializer because when you pass "many=True", it also use ListSerializer class. If you want to update once object, please include "id" field and pass its value in input data.

Related

How to obtain the field from the parent serializer while serializing a nested json? [duplicate]

I'm working with Django-Rest-Framework's serializers. I have two serializers one nested with the other.
class NestedSerializer(serializers.Serializer):
value = AttributeValueField(required=True)
name = serializers.CharField(required=True)
class OuterSerializer(serializers.Serializer):
info = serializers.CharField()
nested = NestedSerializer()
In order to validate the nested serializer's data I need to retrieve input data from the parent serializer, something like this:
class NestedSerializer(serializers.Serializer):
...
def validate(self, data):
# of course, it doesn't work, but thats the idea.
info = self.parent.info
# then validate the NestedSerializer with info.
I can't find any way to get access to those input data from the validate method. Any suggestions? Thanks for your help :).
Before validate() method, DRF serializers call to_internal_value(self, data). You will get all data of parent serializer there. So as you defined validate() method in serializer, define to_internal_value() method and catch parent serializer's data.
You can access initial_data on the parent serializer from the nested serializers validate() method. I've also added some code for using the parent fields run_validation() method, which would validate and return the internal value from to_internal_value(), which might be a better than dealing with the initial data.
class NestedSerializer(serializers.Serializer):
def validate(self, data):
# Retrieve the initial data, perhaps this is all you need.
parent_initial_data = self.parent.initial_data
info = parent_initial_data.get("info", None)
# Get the corresponding field and use `run_validation` or `to_internal_value` if needed
if info:
info_field = self.parent.fields["info"]
info = info_field.run_validation(info)
# info = info_field.to_internal_value(info) # If you don't want validation, but do want the internal value
# Do your thing
return data
Try self.root.instance to get the parent instance in a nested serializer.
It might not be the best idea to do it this way, NestedSerializer should not be aware of the parent object. It would make your code difficult to maintain, also it would make NestedSerializer dependent on OuterSerializer.
Instead, define a validate(self, data) method in the OuterSerializer and run the mutual validation there.
Here's what I'm doing now but I'm interested to see other answers..
Basically I've created a custom field for the field in the parent serializer that needs to be accessed in the child serializer - in this case "customer". Then override to_internal_value() to add the field's validated data as an attribute on the parent serializer.
Once it's been added as an attribute it can be accessed on the child serializer through self.parent.<attribute_name> or on child serializer fields by self.root.<attribute_name>
class CustomerField(serializers.PrimaryKeyRelatedField):
def to_internal_value(self, data):
# Set the parent serializer's `customer` attribute to the validated
# Customer object.
ret = super().to_internal_value(data)
self.parent.customer = ret
return ret
class DebitField(serializers.PrimaryKeyRelatedField):
default_related_name = {
'OnAccount': 'onaccounts',
'Order': 'orders'
}
def get_queryset(self):
# Method must be overridden so the `queryset` argument is not required.
return super().get_queryset()
def set_queryset_from_context(self, model_name):
# Override the queryset depending on the model name.
queryset = self.default_related_name[model_name]
self.queryset = getattr(self.parent.customer, queryset)
def to_internal_value(self, data):
# Get the model from the `debit_type` and the object id from `debit`
# then validate that the object exists in the related queryset.
debit_type = data.pop('debit_type')
self.set_queryset_from_context(debit_type)
super().to_internal_value(data)
class PaymentLineSerializer(serializers.ModelSerializer):
debit = DebitField()
class Meta:
model = PaymentLine
fields = (
'id',
'payment',
'debit_type',
'debit', # GenericForeignKey
'amount',
)
def to_internal_value(self, data, *args):
data['debit'] = {
'debit': data.pop('debit'),
'debit_type': data.pop('debit_type'),
}
ret = super().to_internal_value(data)
return ret
def to_representation(self, instance):
data = super().to_representation(instance)
data['debit'] = instance.debit._meta.object_name
return data
class PaymentSerializer(serializers.ModelSerializer):
customer = CustomerField(queryset=Customer.objects.all())
class Meta:
model = Payment
fields = (
'id',
'customer',
'method',
'type',
'date',
'num_ref',
'comment',
'amount',
)
def __init__(self, *args, **kwargs):
self.customer = None
super().__init__(*args, **kwargs)
self.fields['lines'] = PaymentLineSerializer(
context=self.context,
many=True,
write_only=True,
)
You are almost there!!!
Use self.parent.initial_data to access the data given to the parent serializer.
class NestedSerializer(serializers.Serializer):
value = AttributeValueField(required=True)
name = serializers.CharField(required=True)
def validate(self, attrs):
attrs = super().validate(attrs)
the_input_data = self.parent.initial_data
info = the_input_data['info'] # this will not be the "validated data
# do something with your "info"
return attrs
Do not hardcode the field_name
self.parent.initial_data[self.field_name]

instance.save() is not saving the model in ListSerializer Django Rest Framework

In Django Rest Framework ListSerializer when I want to save the validated data to the database by calling instance.save() I'm getting an error saying queryset object has no attribute save.
ListSerializer class:
class NoAccessDetailsListSerializer(serializers.ListSerializer):
# This will be called when there is list of objects
#here instance is list of queryset and validated_data is the list of json object
def update(self, instance, validated_data):
ret = []
for index, data in enumerate(validated_data):
#checking if row is already chosen
if(instance[index].row_chosen):
# do not update the info to db
# just append the data to ret
ret.append(instance[index])
else:
instance.id = instance[index].id
instance.row_chosen = validated_data[index].get(
'row_chosen')
instance.user_working = validated_data[index].get(
'user_working')
ret.append(instance)
instance.save()
return ret
Serializer Class
class NoAccessDetailsSerializer(serializers.ModelSerializer):
id = serializers.IntegerField(read_only=False)
class Meta:
model = NoAccessDetails
list_serializer_class = NoAccessDetailsListSerializer
fields = ("id", "row_chosen",
"user_working")
def update(self, instance, validated_data):
instance.id = instance.id
instance.row_chosen = validated_data.get(
'row_chosen')
instance.user_working = validated_data.get(
'user_working ')
instance.save()
return instance
Basically in ListSerializer I'm checking if the row is chosen already in the DB. If True then I just append the instance data to a dictionary else I want to update the data to the DB and append the updated data to a list and return it.
Here in the ListSerializer I'm passing filtered queryset from the APIView class as instance and validated_data is a list of validated data.
Sample JSON data which I will pass to the APIView class:
[
{
"id": 1,
"row_chosen": true,
"user_working": "John"
},
{
"id": 1,
"row_chosen": true,
"user_working": "David"
},
]
When I pass the JSON data, it will properly filter out the rows from DB and pass the queryset as instance and JSON data to the serializer class.
# here list_of_id is the ids which are there in the JSON object. i.e [1,2]
filtered_id_data= NoAccessDetails.objects.filter(
id__in=list_of_id)
serializer = NoAccessDetailsSerializer(filtered_id_data,
data=request.data,
many=True,
)
The ListSerializer update() is working but when it runs else block and tries to update the data it gives me an error queryset object has no attribute save. Whereas in the serializer's update() it runs the instance.save() and updates the data for the single object. I'm not sure where I'm making the mistake.
Please help me with this.
Update:
I changed instance.save() to instance[index].save() in ListSerializer class. Now the queryset object has no attribute save has been fixed. Even though when I use instance[index].save() I'm unable to save the data in the data base.
Models:
class NoAccessDetails(models.Model):
20 CharFields
...
...
user_working = models.ForeignKey(
UserProfile, on_delete=models.CASCADE, blank=True, null=True)
row_chosen = models.BooleanField(default=False)
class UserProfile(models.Model):
user_id = models.CharField(primary_key=True, max_length=10)
user_name = models.CharField(max_length=100)
user_email = models.EmailField()
is_admin = models.BooleanField(default=False)
Here in the NoAccessDetail model, I've kept user_working null true because the data to this model will be coming from a different source. Initially while importing the data the user_working will be null. While updating the data from an API call, I'm validating the JSON data.
To call the .save() method, you have to call it on an instance of a Model, not on a QuerySet of the model. According to DRF Docs,
class BookListSerializer(serializers.ListSerializer):
def update(self, instance, validated_data):
# Maps for id->instance and id->data item.
book_mapping = {book.id: book for book in instance}
data_mapping = {item['id']: item for item in validated_data}
# Perform creations and updates.
ret = []
for book_id, data in data_mapping.items():
book = book_mapping.get(book_id, None)
if book is None:
ret.append(self.child.create(data))
else:
ret.append(self.child.update(book, data))
# Perform deletions.
for book_id, book in book_mapping.items():
if book_id not in data_mapping:
book.delete()
return ret
You can see they are using a book_mapping. This is creating a dictionary where the key is the book's id and the value is the instance of the book.
Hope this helps!
EDIT
Check the line just below the 'else:' block. You see you need to use .get() to get the object of the model you want to update, and then use the .save() method.
RE-EDIT
Using instance[index].save() should also work. I think you need to call obj.save() before appending to ret.
class NoAccessDetailsListSerializer(serializers.ListSerializer):
def update(self, instance, validated_data):
...
...
else:
obj = NoAccessDetails.objects.get(id=instance[index].id)
# or obj = instance[index]
obj.row_chosen = validated_data[index].get(
'row_chosen')
obj.user_working = validated_data[index].get(
'user_working')
print('Instance data ',
obj.row_chosen,
obj.user_working)
obj.save() # call this before appending to ret
ret.append(obj)
return ret
RE-RE-EDIT
I updated the snippet according to docs.
class NoAccessDetailsListSerializer(serializers.ListSerializer):
# This will be called when there is list of objects
# here instance is list of queryset and validated_data is the list of json object
def update(self, instance, validated_data):
ret = []
obj_mapping = {obj.id: obj for obj in instance}
data_mapping = {item['id']: item for item in validated_data}
for obj_id, data in data_mapping.items():
obj = obj_mapping.get(obj_id, None)
if not obj:
continue
if obj.row_chosen:
ret.append(obj)
else:
obj.row_chosen = data['row_chosen']
obj.user_working = data['user_working']
obj.save()
ret.append(obj)
return ret

Django SerializerMethodField can't save a decimal

So, according to the docs, SerializerMethodField is a read-only field.
Well in my case, it's interfering with my write:
# old value is 2.5
data={'score': 1.7}
serializer = ScoreTraitSerializer(
score_trait, data=data, partial=True)
if serializer.is_valid():
new_score_trait = serializer.save()
Now if I inspect the new_score_trait, my score is still 2.5.
The serializer looks as such:
score = serializers.SerializerMethodField()
def get_score(self, obj):
if isinstance(obj.score, decimal.Decimal):
return float(obj.score)
else:
return obj.score
If I comment out my SerializerMethodField, I can save the new decimal value (but can't serialize it).
So ... am I using my serializer correctly? Why does my write to the serializer hitting the SerializerMethodField?
Thanks in advance
SerializerMethodField is a read-only field.Only used for to_representation, it's used for list/retrieve not create/update.
the serializer field score must conflict with model field score,try change it to:
float_score = serializers.SerializerMethodField(required=False)
def get_float_score (self, obj):
if isinstance(obj.score, decimal.Decimal):
return float(obj.score)
else:
return obj.score
See the source code you will know why:
class SerializerMethodField(Field):
"""
A read-only field that get its representation from calling a method on the
parent serializer class. The method called will be of the form
"get_{field_name}", and should take a single argument, which is the
object being serialized.
For example:
class ExampleSerializer(self):
extra_info = SerializerMethodField()
def get_extra_info(self, obj):
return ... # Calculate some data to return.
"""
def __init__(self, method_name=None, **kwargs):
self.method_name = method_name
kwargs['source'] = '*'
kwargs['read_only'] = True
super(SerializerMethodField, self).__init__(**kwargs)

Django REST, Accessing methods of the selected object

this part of my code fills the queryset with [category_object].subcats.all(). let subcats be a method of category object:
serializer:
class CatSrlz(serializers.ModelSerializer):
class Meta:
model = Category
fields = ('id', 'label', )
View:
class CatsViewSet(viewsets.ReadOnlyModelViewSet):
queryset = Category.objects.filter(parent=None)
serializer_class = CatSrlz
def retrieve(self, request, *args, **kwargs):
# return Response({'res': self.kwargs})
queryset = Category.objects.get(pk=str(self.kwargs['pk'])).subCats.all()
dt = CatSrlz(queryset, many=True)
return Response(dt.data)
and url:
router.register(r'cats', views.CatsViewSet)
it works but i'm pretty sure that there must be a more correct way of doing so
Is there one?
thanks
When retrieving a single object, you can use the get_object method in your view, which look like this in DRF without modifications :
def get_object(self):
"""
Returns the object the view is displaying.
You may want to override this if you need to provide non-standard
queryset lookups. Eg if objects are referenced using multiple
keyword arguments in the url conf.
"""
queryset = self.filter_queryset(self.get_queryset())
# Perform the lookup filtering.
lookup_url_kwarg = self.lookup_url_kwarg or self.lookup_field
assert lookup_url_kwarg in self.kwargs, (
'Expected view %s to be called with a URL keyword argument '
'named "%s". Fix your URL conf, or set the `.lookup_field` '
'attribute on the view correctly.' %
(self.__class__.__name__, lookup_url_kwarg)
)
filter_kwargs = {self.lookup_field: self.kwargs[lookup_url_kwarg]}
obj = get_object_or_404(queryset, **filter_kwargs)
# May raise a permission denied
self.check_object_permissions(self.request, obj)
return obj
So you could adapt the part where you get your object :
filter_kwargs = {self.lookup_field: self.kwargs[lookup_url_kwarg]}
obj = get_object_or_404(queryset, **filter_kwargs)
and add your subcat logic there.
By the way, I don't get why you are using
dt = CatSrlz(queryset, many=True)
Shouldn't "retrieve" return a single object?

Add request/context to django in writable serializer

I want to add the request context to my serializer in the Django REST framework. In particular to a nested serializer, i (successfully) tried to do that with a SerializerMethodField ( as my solution per: context in nested serializers django rest framework ). This is the setup i use:
class VehicleTypeSerializer(RsModelSerializer):
class Meta:
model = VehicleType
class VehicleSerializer(RsModelSerializer):
vehicletype = SerializerMethodField()
class Meta:
model = Vehicle
fields = ('vehiclename', 'vehicledescription', 'vehicletype')
def get_vehicletype(self, obj):
return self.get_serializermethodfield_data(obj, VehicleType, VehicleTypeSerializer, 'vehicle')
def get_serializermethodfield_data(self, obj, model_class, serializer_class, filter_field):
filter = {filter_field: obj}
objs = model_class.objects.all().filter(**filter)
# We need the request-context for checking field permissions in the serializer
s = serializer_class(objs, many=True, context={'request': self.context.get('request')})
return s.data
Problem : I need a SerializerMethodField to pass the request-context to the nested-serializer (VehicleTypeSerializer)
But now i am stuck dealing with POST's since the SerializerMethodField is read-only. I can't POST an object to /api/v1/vehicle with:
{
"vehiclename": "test",
"vehicledescription": "test"
"vehicletype": "1" <---- get's ignored since SerializerMethodField is read-only
}
Question : Can someone point me in the right direction to add the request-context (especially the user information) to a nested serializer which i can write to?
I need the request context (request.user) in the VehicleSerializer as well as in the VechileTypeSerializer, because in the RsModelSerializer that i have defined, i check on a per-field-basis if the user that is doing the request has permission to read or update a field.
In the RsModelSerializer:
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
# Make sure that there is a user mapped in the context (we need a user
# for checking permissions on a field). If there is no user, we set
# the user to None.
if not self.context:
self._context = getattr(self.Meta, 'context', {})
try:
self.user = self.context['request'].user
except (KeyError, AttributeError):
print('No request')
self.user = None
def get_fields(self):
"""
Override get_fields to ensure only fields that are allowed
by model-field-permissions are returned to the serializer
:return: Dict with allowed fields
"""
ret = OrderedDict()
fields = super().get_fields()
# If no user is associated with the serializer, return no fields
if self.user == None:
return None
# A superuser bypasses the permissions-check and gets all
# available fields
if self.user.is_superuser:
print_without_test("user is superuser, bypassing permissions")
return fields
# Walk through all available fields and check if a user has permission for
# it. If he does, add them to a return-array. This way all fields that
# are not allowed to 'read' will be dropped. Note: this is only used
# for read access. Write access is handled in the views (modelviewsets).
for f in fields:
if has_permission(user=self.user, app_label=self.Meta.model._meta.app_label,
table=self.Meta.model.__name__.lower(),
field=f,
permission='read'):
ret[f] = fields[f]
return ret
Method-1: Overriding the __init__() method of parent serializer
You can add the context to nested/child serializer in the __init__() method of parent serializer.
class RsModelSerializer(serializers.ModelSerializer):
def __init__(self, *args, **kwargs):
super(RsModelSerializer, self).__init__(*args, **kwargs)
request_obj = self.context.get('request') # get the request from parent serializer's context
# assign request object to nested serializer context
self.fields['nested_serializer_field'].context['request'] = request_obj
We cannot pass the context to nested serializer at the time of their __init__() because they get initialized at the time of declaration in the parent serializer.
class SomeParentSerializer(serializers.Serializer):
some_child = SomeChildSerializer() # gets initialized here
Method-2: Passing context when child serializer gets binded to its parent
Another option is to add the context when a child/nested serializer gets binded to the parent.
class SomeChildSerializer(Serializer):
def bind(self, field_name, parent):
super(SomeChildSerializer, self).bind(field_name, parent) # child gets binded to parent
request_obj = parent.context.get('request') # get the request from parent serializer context
self.context['request'] = request_obj
Quoting the DRF author's suggested option in the related ticket:
This should be considered private API, and the parent
__init__ style listed above should be preferred.
So, the better option is to override the __init__() method of ParentSerializer and pass the context to child/nested serializer.
(Source: check this related ticket on Github.)
If you need to pass a context to Serializer class. You can use Serializer's context
And you will be able to use it in a SerializerMethodField
class MySerializer(serializer.Serializer)
field = serializer.SerializerMethodField()
def get_field(self, obj):
return self.context.get('my_key')
You call it from view:
...
s = MySerializer(data=data, context={'my_key': 'my_value'})
...
EDIT:
If you need use this context in another Serializer class, pass to the first serializer in the pass to the nexted serializer:
# views.py
...
s = MySerializer(data=data, context={'my_key': 'my_value'})
...
# serializers.py
class MySerializer(serializer.Serializer):
field = serializer.SerializerMethodField()
def get_field(self, obj):
return MySecondSerializer(..., context=self.context)