I am trying to partially update a record using (partial=True) via my serializer, however, when I look at the sql update statement, it's showing that all fields are being updated when only a subset of fields are being submitted.
class Setting(models.Model):
comments_enabled = models.BooleanField(default=True)
visibility = models.CharField(max_length=50, choices=VISIBILITIES,
blank=False, null=False,
default=VISIBILITY_CHOICE_PARTICIPANTS)
modified = models.DateTimeField(auto_now=True, blank=True, null=True)
class SettingsSerializer(serializers.ModelSerializer):
class Meta:
model = Setting
fields = ('id', 'comments_enabled', 'visibility', 'modified')
class SomeSerializer(serializers.ModelSerializer):
def update(self, instance, validated_data):
settings_serializer = SettingsSerializer(instance.settings, data=validated_data.get('settings'), partial=True)
settings_serializer.is_valid(raise_exception=True)
settings_serializer.save()
I have doubled checked that the validated_data dictionary being passed in only has one field.
I'm using DRF 3.3 and Django 1.9
I think I got your question now. As stated in the docs you can use the partial keyword to allow partial updates.
By default, serializers must be passed values for all required fields or they will raise validation errors. You can use the partial argument in order to allow partial updates.
This only means that you don't have to post the full object as JSON. As far as I can see the partial argument is only used for validation. In the end the update method of the serializer gets called:
def update(self, instance, validated_data):
raise_errors_on_nested_writes('update', self, validated_data).
for attr, value in validated_data.items():
setattr(instance, attr, value)
instance.save()
return instance
Here the values from validated_data are assigned to the instance. In the end the objects save method is called. And in this method django creates the SQL statement to update all values, because django doesn't know which value changed.
Related
I have two models, one with a foreignkey relationship to the other. I am wanting to be able to create or update multiple entries at the same time. I would not be creating and updating simultaneously.
I keep getting a bit confused about what is going on with people's examples. I was hoping someone could not only show me what I need to do, but also give a bit of an explanation about what is going on at each step. I'm specifically not understanding what the validated_data.get() method is doing, or what exactly is an instance, and what is being done with them.
class ExtraFieldsSerializer(serializers.ModelSerializer):
id = serializers.ReadOnlyField()
class Meta:
model = ExtraFields
fields = [
'id',
'job',
'custom_data',
]
class JobWriteSerializer(serializers.ModelSerializer):
extra_fields = ExtraFieldsSerializer(many=True)
class Meta:
model = Job
fields = [
"extra_fields",
"custom_data_fields",
]
# Make JobSerializer able to create custom fields.
def create(self, validated_data):
extra_fields = validated_data.pop("extra_fields")
user = User.objects.get(id=self.context['request'].user.id)
client = Client.objects.get(user=self.context['request'].user)
new_job = Job.objects.create(user=user, client=client, **validated_data)
new_job.save()
for field in extra_fields:
new_field = ExtraFields.objects.create(job=new_job, custom_data=field['custom_data'])
new_field.save()
return new_job
# Make JobSerializer able to update custom fields
def update(self, instance, validated_data):
pass
First of all, you could probably use drf-writeable-nested for that, as it pretty much does exactly what you want.
But it never hurts to understand what's going on:
def create(self, validated_data):
extra_fields = validated_data.pop("extra_fields")
user = User.objects.get(id=self.context['request'].user.id)
client = Client.objects.get(user=self.context['request'].user)
new_job = Job.objects.create(user=user, client=client, **validated_data)
new_job.save()
for field in extra_fields:
new_field = ExtraFields.objects.create(job=new_job, custom_data=field['custom_data'])
new_field.save()
return new_job
Since JobWriteSerializer is the parent serializer, we will start with that.
the validated_data argument that is passed to your create function contains, well, as the name suggests, the validated data. This means, you can assume that all the constraints you defined in your serializer (required fields, max_length, min_length etc) hold, and you don't need to check them.
The code looks fine, it seems you are popping all the extra_fields from your serializer and create ExtraField objects from them.
You specifically ask what's going on when using validated_data.get but you are using validated_data.pop. the difference is, that when using get, the retrieved data stays in the dictionary, while pop removes it.
This is especially handy for cases where you also have to create nested objects, consider this (some things omitted as they are not relevant):
class MyModel(models.Model):
text = models.CharField(max_length=10, related_name='children')
class MyChildModel(models.Model):
someVal = models.BooleanField()
model = models.ForeignKey(MyModel)
class MyChildSerializer(serializers.ModelSerializer):
someVal = serializers.BooleanField()
class MyModelSerializer(serializers.ModelSerializer):
text = serializers.TextField(...)
childen = ChildrenSerializer(many=True)
def create(self, validated_data):
children = validated_data.pop('children', []) #POP!
instance = super().create(validated_data)
for c in children:
MyChildSerializer.objects.create(model=instance, **c)
return instance
You can test this yourself, if you use get instead of pop here, your serializer will rightfully complain that there are children objects inside the validated_data object, and drf cannot create nested relations out of the box. When you pop them, the serializer does not have those fields anymore and it works.
Note that this approach would not be efficient for your case, as you manually pass data to your Job object (like the user and the client) which you do not get via the passed data, but from your request. You can, if you want, get around that by using get_serializer_context, but lets just say that this is out of the questions scope.
On to your update method, I suggest something like that (not tested, but you get the idea):
def update(self, instance, validated_data):
extra_fields = validated_data.pop('extra_fields', []) # POP the extra fields
instance = super().update(instance, validated_data) # !!!!!
for extra in extra_fields:
#retrieve your extra fields and update them
myExtra = ExtraFields.objects.get(id=extra['id])
....
The passed argument instance is actually the already existing instance of your Job model. This is the object you want to update. Note that again, I popped the extra_fields, precisely for doing what I described above: I used drf itself to modify/update the object, since I only have to implement the update for the child elements.
I'm using Django 2.0.
I have a model like
class MyModel(models.Model):
update_new = models.CharField(blank=True, max_length=200)
modified = models.DateTimeField(auto_now=True)
created = models.DateTimeField(auto_now_add=True)
and updating model data using Model Manager
class MyModelManager(models.Manager):
def get_queryset(self):
return MyModelQueryset(self.model, self._db)
def update_or_create(self, pk, **save_data):
record = MyModel.objects.filter(
pk=pk
)
if record.exists():
# setting field manually for testing
save_data['update_new'] = 'anuj'
uc = record.update(**save_data)
print(uc) # prints 1
return record.first(), True
record, created = self.get_queryset().get_or_create(
pk=pk
**save_data
)
return record, created
This works fine and value is updated. But modified field is not updated. Value in created and modified fields remain same (timestamp when record was created)
This behaviour described in the docs:
The field is only automatically updated when calling Model.save(). The
field isn’t updated when making updates to other fields in other ways
such as QuerySet.update(), though you can specify a custom value for
the field in an update like that.
I basically have the following model in my project:
class ShellMessage(TimeStampedModel):
# There is a hidden created and modified field in this model.
ACTION_TYPE = (
('1' , 'Action 1'),
('2' , 'Action 2')
)
type = models.CharField(max_length=2,choices=ACTION_TYPE,default='1')
action = models.CharField(max_length=100)
result = models.CharField(max_length=300, blank=True)
creator = models.ForeignKey(User)
I created a serializer:
class ShellMessageSerializer(serializers.ModelSerializer):
class Meta:
model = ShellMessage
fields = ('action', 'type', 'result', 'creator')
And a ModelViewSet:
class ShellListViewSet(viewsets.ModelViewSet):
serializer_class = ShellMessageSerializer
queryset = ShellMessage.objects.all()
My issue is the following:
When I create a new ShellMessage with a POST to my API, I don't want to provide the foreignKey of 'creator' but instead just the username of the guy and then process it in my ViewSet to find the user associated with this username and save it in my ShellMessage object.
How can I achieve this using Django rest Framework? I wanted to supercharge create() or pre_save() methods but I'm stuck as all my changes overwrite 'normal' framework behavior and cause unexpected errors.
Thank you.
I finally find my solution just after posting my question :)
So I did the following:
class ShellListViewSet(viewsets.ModelViewSet):
serializer_class = ShellMessageSerializer
queryset = ShellMessage.objects.all()
def pre_save(self, obj):
obj.creator = self.request.user
return super(ShellListViewSet, self).pre_save(obj)
This is working as expected. I hope I did well.
UPDATE: This topic seems to be a duplicate to Editing django-rest-framework serializer object before save
If you intend to intercept and perform some processing before the object gets saved in the model database, then what you're looking for is overriding the method "perform_create" (for POST) or "perform_update" (for PUT/PATCH) which is present within the viewsets.ModelViewSet class.
This reference http://www.cdrf.co/3.1/rest_framework.viewsets/ModelViewSet.html lists all available methods within viewsets.ModelViewSet where you can see that the "create" method calls "perform_create" which in turn performs the actual saving through the serializer object (the object that has access to the model):
def perform_create(self, serializer):
serializer.save()
We can override this functionality that is present in the base class (viewsets.ModelViewSet) through the derived class (the ShellListViewSet in this example) and modify the model attribute(s) that you want to be changed upon saving:
class ShellListViewSet(viewsets.ModelViewSet):
serializer_class = ShellMessageSerializer
queryset = ShellMessage.objects.all()
def findCreator(self):
# You can perform additional processing here to find proper creator
return self.request.user
def perform_create(self, serializer):
# Save with the new value for the target model fields
serializer.save(creator = self.findCreator())
You can also opt to modify the model fields separately and then save (probably not advisable but is possible):
serializer.validated_data['creator'] = self.findCreator()
serializer.save()
Later if the object is already created and you also want to apply the same logic during an update (PUT, PATCH), then within "perform_update" you can either do the same as above through the "serializer.validated_data['creator']" or you could also change it directly through the instance:
serializer.instance.creator = self.findCreator()
serializer.save()
But beware with such updating directly through the instance as from https://www.django-rest-framework.org/api-guide/serializers/ :
class MyModelSerializer(serializers.Serializer):
field_name = serializers.CharField(max_length=200)
def create(self, validated_data):
return MyModel(**validated_data)
def update(self, instance, validated_data):
instance.field_name = validated_data.get('field_name', instance.field_name)
return instance
This means that whatever you assign to the "instance.field_name" object could be overriden if there is a "field_name" data set within the "validated_data" (so in other terms, if the HTTP Body of the PUT/PATCH Request contains that particular "field_name" resulting to it being present in the "validated_data" and thus overriding whatever value you set to the "instance.field_name").
I'm having some issue with django-rest-framework, and nested objects.
I have a Cart object, as well as CartItem, which links back to a Cart:
class Cart(models.Model):
customer = models.ForeignKey(Customer)
date_created = models.DateTimeField(auto_now_add=True)
date_modified = models.DateTimeField(auto_now=True)
class CartItem(models.Model):
cart = models.ForeignKey(Cart, related_name='cartitems')
product = models.ForeignKey(Product, help_text='Product in a cart')
quantity = models.PositiveIntegerField(default=1, help_text='Quantity of this product.')
date_added = models.DateTimeField(auto_now_add=True, help_text='Date that this product was added to the cart.')
I've created serializers for both:
class CartItemSerializer(serializers.ModelSerializer):
product = serializers.HyperlinkedRelatedField(view_name='product-detail')
class Meta:
model = CartItem
class CartSerializer(serializers.ModelSerializer):
customer = serializers.HyperlinkedRelatedField(view_name='customer-detail')
cartitems = CartItemSerializer(required=False)
total_price = serializers.CharField(source='total_price', read_only=True)
shipping_cost = serializers.CharField(source='shipping_cost', read_only=True)
class Meta:
model = Cart
fields = ('id', 'customer', 'date_created', 'date_modified', 'cartitems', 'total_price', 'shipping_cost')
However, whenever I try to POST to create a new cart, I get an error, assumedly when it tries to set the non-existent CartItem:
TypeError at /api/v1/carts/
add() argument after * must be a sequence, not NoneType
However, a Cart isn't required to actually have CartItems.
Is there any way to get DRF to respect the required=False flag I get on Cart.cartitems?
Cheers,
Victor
EDIT:
I took a stab at tracing it through again:
It's calling BaseSerializer.save() in rest_framework/serializers.py with a CartSerializer object.
def save(self, **kwargs):
"""
Save the deserialized object and return it.
"""
if isinstance(self.object, list):
[self.save_object(item, **kwargs) for item in self.object]
if self.object._deleted:
[self.delete_object(item) for item in self.object._deleted]
else:
self.save_object(self.object, **kwargs)
return self.object
It then calls save_object() on the same class:
def save_object(self, obj, **kwargs):
"""
Save the deserialized object and return it.
"""
if getattr(obj, '_nested_forward_relations', None):
# Nested relationships need to be saved before we can save the
# parent instance.
for field_name, sub_object in obj._nested_forward_relations.items():
if sub_object:
self.save_object(sub_object)
setattr(obj, field_name, sub_object)
obj.save(**kwargs)
if getattr(obj, '_m2m_data', None):
for accessor_name, object_list in obj._m2m_data.items():
setattr(obj, accessor_name, object_list)
del(obj._m2m_data)
if getattr(obj, '_related_data', None):
for accessor_name, related in obj._related_data.items():
if isinstance(related, RelationsList):
# Nested reverse fk relationship
for related_item in related:
fk_field = obj._meta.get_field_by_name(accessor_name)[0].field.name
setattr(related_item, fk_field, obj)
self.save_object(related_item)
# Delete any removed objects
if related._deleted:
[self.delete_object(item) for item in related._deleted]
elif isinstance(related, models.Model):
# Nested reverse one-one relationship
fk_field = obj._meta.get_field_by_name(accessor_name)[0].field.name
setattr(related, fk_field, obj)
self.save_object(related)
else:
# Reverse FK or reverse one-one
setattr(obj, accessor_name, related)
del(obj._related_data)
The Cart object has a _related_data field that is set to a dict:
{'cartitems': None}
Hence, on the second-last line, it calls setattr in django/db/models/fields/related.py:
def __set__(self, instance, value):
if instance is None:
raise AttributeError("Manager must be accessed via instance")
manager = self.__get__(instance)
# If the foreign key can support nulls, then completely clear the related set.
# Otherwise, just move the named objects into the set.
if self.related.field.null:
manager.clear()
manager.add(*value)
It's this last liner (manager.add(*value)) that causes the:
TypeError: add() argument after * must be a sequence, not NoneType
Checking the Serializer Relation Docs, first you need to add many=True to your cartitems field.
Unfortunately this is read-only. The docs just say "For read-write relationships, you should use a flat relational style" — you can find a question about that here (although that's only dealing with the 1-1 case).
Current strategies involve making cartitems read-only and then either: doing something post_save, using a second serializer or making a separate request to a separate endpoint to set the related entities. Given that better support for Nested Writes is coming I'd probably be inclined towards a separate request to a separate endpoint for the moment (though that will obviously depend on your constraints).
I hope that helps.
EDIT: (After update to question & discussion in comments).
If you're using a separate endpoint for adding CartItems then making cartitems read-only should eliminate the error.
However (if you're not making it read-only) looking at the DRF code you posted from save_object it occurs that in the related_item in related block you really do need a list. The appropriate dict (fragment) for a Cart with no CartItems is not {'cartitems': None} but rather {'cartitems': []}. — This of course means your required=False flag isn't doing anything. (So perhaps the short answer is "No" — Will now defer to the mailing list discussion
I have a model
class Survey(models.Model):
created_by = models.ForeignKey(User)
question = models.CharField(max_length=150)
active = models.NullBooleanField()
def __unicode__(self):
return self.question
and now I want to update only the active field. So I do this:
survey = get_object_or_404(Survey, created_by=request.user, pk=question_id)
survey.active = True
survey.save(["active"])
Now I get an error IntegrityError: PRIMARY KEY must be unique.
Am I right with this method to update?
To update a subset of fields, you can use update_fields:
survey.save(update_fields=["active"])
The update_fields argument was added in Django 1.5. In earlier versions, you could use the update() method instead:
Survey.objects.filter(pk=survey.pk).update(active=True)
Usually, the correct way of updating certain fields in one or more model instances is to use the update() method on the respective queryset. Then you do something like this:
affected_surveys = Survey.objects.filter(
# restrict your queryset by whatever fits you
# ...
).update(active=True)
This way, you don't need to call save() on your model anymore because it gets saved automatically. Also, the update() method returns the number of survey instances that were affected by your update.